UPDATE: Add raster tiles
This commit is contained in:
BIN
data/map.mbtiles
BIN
data/map.mbtiles
Binary file not shown.
BIN
data/raster.mbtiles
Normal file
BIN
data/raster.mbtiles
Normal file
Binary file not shown.
113
internal/controllers/rasterTileController.go
Normal file
113
internal/controllers/rasterTileController.go
Normal 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
|
||||||
|
}
|
||||||
99
internal/repositories/rasterTileRepository.go
Normal file
99
internal/repositories/rasterTileRepository.go
Normal 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
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ func NewTileRepository(db *sql.DB, c cache.Cache) TileRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *tileRepository) GetMetadata(ctx context.Context) (map[string]string, error) {
|
func (r *tileRepository) GetMetadata(ctx context.Context) (map[string]string, error) {
|
||||||
cacheId := "mbtiles:metadata"
|
cacheId := "tile:metadata"
|
||||||
|
|
||||||
var cached map[string]string
|
var cached map[string]string
|
||||||
err := r.c.Get(ctx, cacheId, &cached)
|
err := r.c.Get(ctx, cacheId, &cached)
|
||||||
|
|||||||
13
internal/routes/rasterTileRoute.go
Normal file
13
internal/routes/rasterTileRoute.go
Normal 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)
|
||||||
|
}
|
||||||
57
internal/services/rasterTileService.go
Normal file
57
internal/services/rasterTileService.go
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
@@ -40,12 +40,15 @@ func (t *tileService) GetTile(ctx context.Context, z, x, y int) ([]byte, map[str
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, contentType, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
return nil, contentType, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
contentType["Content-Type"] = "image/png"
|
switch format {
|
||||||
if format == "jpg" {
|
case "pbf":
|
||||||
contentType["Content-Type"] = "image/jpeg"
|
|
||||||
}
|
|
||||||
if format == "pbf" {
|
|
||||||
contentType["Content-Type"] = "application/x-protobuf"
|
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 {
|
if isPBF {
|
||||||
|
|||||||
Reference in New Issue
Block a user