package repositories import ( "context" "database/sql" "fmt" "history-api/pkg/cache" "history-api/pkg/constants" "strconv" "sync" "time" ) const tileCacheDuration = 5 * time.Minute type TileRepository interface { GetMetadata(ctx context.Context) (map[string]string, error) GetTile(ctx context.Context, z, x, y int) ([]byte, string, bool, error) } type tileRepository struct { db *sql.DB c cache.Cache metadataMu sync.RWMutex metadata map[string]string } func NewTileRepository(db *sql.DB, c cache.Cache) TileRepository { return &tileRepository{ db: db, c: c, } } func (r *tileRepository) GetMetadata(ctx context.Context) (map[string]string, error) { r.metadataMu.RLock() if r.metadata != nil { metadata := r.metadata r.metadataMu.RUnlock() return metadata, nil } r.metadataMu.RUnlock() r.metadataMu.Lock() defer r.metadataMu.Unlock() if r.metadata != nil { return r.metadata, nil } cacheId := "tile:metadata" var cached map[string]string err := r.c.Get(ctx, cacheId, &cached) if err == nil { r.metadata = cached 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, 8) for rows.Next() { var name, value string if err := rows.Scan(&name, &value); err != nil { return nil, err } metadata[name] = value } if err := rows.Err(); err != nil { return nil, err } _ = r.c.Set(ctx, cacheId, metadata, constants.NormalCacheDuration) r.metadata = metadata return metadata, nil } func (r *tileRepository) GetTile(ctx context.Context, z, x, y int) ([]byte, string, bool, error) { if z < 0 || x < 0 || y < 0 { return nil, "", false, fmt.Errorf("invalid tile coordinates") } cacheId := "tile:raw:" + strconv.Itoa(z) + ":" + strconv.Itoa(x) + ":" + strconv.Itoa(y) cached, err := r.c.GetRawClient().Get(ctx, cacheId).Bytes() if err == nil { meta, err := r.GetMetadata(ctx) if err != nil { return nil, "", false, err } format := meta["format"] return cached, format, format == "pbf", 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, "", false, err } meta, err := r.GetMetadata(ctx) if err != nil { return nil, "", false, err } _ = r.c.GetRawClient().Set(ctx, cacheId, tileData, tileCacheDuration).Err() return tileData, meta["format"], meta["format"] == "pbf", nil }