UPDATE: Add raster tiles

This commit is contained in:
2026-04-14 09:34:56 +07:00
parent 72efb480ac
commit 34c97e803a
8 changed files with 291 additions and 6 deletions

View File

@@ -0,0 +1,113 @@
package controllers
import (
"context"
"fmt"
"history-api/internal/dtos/response"
"history-api/internal/services"
"strconv"
"time"
"github.com/gofiber/fiber/v3"
)
type RasterTileController struct {
service services.RasterTileService
}
func NewRasterTileController(svc services.RasterTileService) *RasterTileController {
return &RasterTileController{service: svc}
}
// GetMetadata godoc
// @Summary Get raster tile metadata
// @Description Retrieve map metadata
// @Tags Tile
// @Accept json
// @Produce json
// @Success 200 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /raster-tiles/metadata [get]
func (h *RasterTileController) GetMetadata(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
res, err := h.service.GetMetadata(ctx)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
// GetTile godoc
// @Summary Get a map raster tile
// @Description Fetch vector or raster map tile data by Z, X, Y coordinates
// @Tags Tile
// @Produce application/octet-stream
// @Param z path int true "Zoom level (0-22)"
// @Param x path int true "X coordinate"
// @Param y path int true "Y coordinate"
// @Success 200 {file} byte
// @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /raster-tiles/{z}/{x}/{y} [get]
func (h *RasterTileController) GetTile(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
z, x, y, err := h.parseTileParams(c)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
data, headers, err := h.service.GetTile(ctx, z, x, y)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
for k, v := range headers {
c.Set(k, v)
}
return c.Status(fiber.StatusOK).Send(data)
}
func (h *RasterTileController) parseTileParams(c fiber.Ctx) (int, int, int, error) {
z, err := strconv.Atoi(c.Params("z"))
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid z")
}
x, err := strconv.Atoi(c.Params("x"))
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid x")
}
y, err := strconv.Atoi(c.Params("y"))
if err != nil {
return 0, 0, 0, fmt.Errorf("invalid y")
}
if z < 0 || x < 0 || y < 0 {
return 0, 0, 0, fmt.Errorf("coordinates must be positive")
}
if z > 22 {
return 0, 0, 0, fmt.Errorf("zoom level too large")
}
return z, x, y, nil
}

View File

@@ -0,0 +1,99 @@
package repositories
import (
"context"
"database/sql"
"fmt"
"history-api/pkg/cache"
"history-api/pkg/constants"
"time"
)
type RasterTileRepository interface {
GetMetadata(ctx context.Context) (map[string]string, error)
GetTile(ctx context.Context, z, x, y int) ([]byte, string, error)
}
type rasterTileRepository struct {
db *sql.DB
c cache.Cache
}
func NewRasterTileRepository(db *sql.DB, c cache.Cache) RasterTileRepository {
return &rasterTileRepository{
db: db,
c: c,
}
}
func (r *rasterTileRepository) GetMetadata(ctx context.Context) (map[string]string, error) {
cacheId := "rasterTile:metadata"
var cached map[string]string
err := r.c.Get(ctx, cacheId, &cached)
if err == nil {
return cached, nil
}
rows, err := r.db.QueryContext(ctx, "SELECT name, value FROM metadata")
if err != nil {
return nil, err
}
defer rows.Close()
metadata := make(map[string]string)
for rows.Next() {
var name, value string
if err := rows.Scan(&name, &value); err != nil {
return nil, err
}
metadata[name] = value
}
_ = r.c.Set(ctx, cacheId, metadata, constants.NormalCacheDuration)
return metadata, nil
}
func (r *rasterTileRepository) GetTile(ctx context.Context, z, x, y int) ([]byte, string, error) {
if z < 0 || x < 0 || y < 0 {
return nil, "", fmt.Errorf("invalid tile coordinates")
}
// cache key
cacheId := fmt.Sprintf("rasterTile:%d:%d:%d", z, x, y)
var cached []byte
err := r.c.Get(ctx, cacheId, &cached)
if err == nil {
meta, _ := r.GetMetadata(ctx)
return cached, meta["format"], nil
}
// XYZ -> TMS
tmsY := (1 << z) - 1 - y
var tileData []byte
err = r.db.QueryRowContext(ctx, `
SELECT tile_data
FROM tiles
WHERE zoom_level = ?
AND tile_column = ?
AND tile_row = ?
`, z, x, tmsY).Scan(&tileData)
if err != nil {
return nil, "", err
}
meta, err := r.GetMetadata(ctx)
if err != nil {
return nil, "", err
}
_ = r.c.Set(ctx, cacheId, tileData, 5*time.Minute)
return tileData, meta["format"], nil
}

View File

@@ -27,7 +27,7 @@ func NewTileRepository(db *sql.DB, c cache.Cache) TileRepository {
}
func (r *tileRepository) GetMetadata(ctx context.Context) (map[string]string, error) {
cacheId := "mbtiles:metadata"
cacheId := "tile:metadata"
var cached map[string]string
err := r.c.Get(ctx, cacheId, &cached)

View File

@@ -0,0 +1,13 @@
package routes
import (
"history-api/internal/controllers"
"github.com/gofiber/fiber/v3"
)
func RasterTileRoutes(app *fiber.App, controller *controllers.RasterTileController) {
route := app.Group("/raster-tiles")
route.Get("/metadata", controller.GetMetadata)
route.Get("/:z/:x/:y", controller.GetTile)
}

View File

@@ -0,0 +1,57 @@
package services
import (
"context"
"history-api/internal/repositories"
"github.com/gofiber/fiber/v3"
)
type RasterTileService interface {
GetMetadata(ctx context.Context) (map[string]string, error)
GetTile(ctx context.Context, z, x, y int) ([]byte, map[string]string, error)
}
type rasterTileService struct {
tileRepo repositories.RasterTileRepository
}
func NewRasterTileService(
TileRepo repositories.RasterTileRepository,
) RasterTileService {
return &rasterTileService{
tileRepo: TileRepo,
}
}
func (t *rasterTileService) GetMetadata(ctx context.Context) (map[string]string, error) {
metaData, err := t.tileRepo.GetMetadata(ctx)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return metaData, nil
}
func (t *rasterTileService) GetTile(ctx context.Context, z, x, y int) ([]byte, map[string]string, error) {
contentType := make(map[string]string)
data, format, err := t.tileRepo.GetTile(ctx, z, x, y)
if err != nil {
return nil, contentType, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
switch format {
case "png":
contentType["Content-Type"] = "image/png"
case "jpg", "jpeg":
contentType["Content-Type"] = "image/jpeg"
case "webp":
contentType["Content-Type"] = "image/webp"
default:
contentType["Content-Type"] = "application/octet-stream"
}
return data, contentType, nil
}

View File

@@ -40,12 +40,15 @@ func (t *tileService) GetTile(ctx context.Context, z, x, y int) ([]byte, map[str
if err != nil {
return nil, contentType, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
contentType["Content-Type"] = "image/png"
if format == "jpg" {
contentType["Content-Type"] = "image/jpeg"
}
if format == "pbf" {
switch format {
case "pbf":
contentType["Content-Type"] = "application/x-protobuf"
case "png":
contentType["Content-Type"] = "image/png"
case "jpg", "jpeg":
contentType["Content-Type"] = "image/jpeg"
default:
contentType["Content-Type"] = "application/octet-stream"
}
if isPBF {