maptile/util/maptool/tileReader.go

511 lines
12 KiB
Go

package maptool
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"git.zhouxhere.com/zhouxhere/maptile/model"
"github.com/hashicorp/golang-lru/v2/expirable"
_ "github.com/mattn/go-sqlite3"
"github.com/pkg/errors"
"github.com/protomaps/go-pmtiles/pmtiles"
)
const MBTilesName = "mbtiles@"
const PMTilesName = "pmtiles@"
const PMTilesHeader = "pmtilesheader@"
const PMTilesEntries = "pmtilesentries@"
const MetadataName = "metadata@"
const TileName = "tile@"
type TileReader struct {
tilesPools *sync.Map
tileCache *expirable.LRU[string, any]
cacheLock sync.RWMutex
mbtilesPath string
pmtilesPath string
}
func NewTileReader(cacheSize int, cacheTTL time.Duration, mbtilesPath, pmtilesPath string) (*TileReader, error) {
// tileCache, _ := lru.NewARC(cacheSize)
tileCache := expirable.NewLRU[string, any](cacheSize, nil, cacheTTL)
reader := &TileReader{
tilesPools: new(sync.Map),
tileCache: tileCache,
}
ex, err := os.Executable()
if err != nil {
return nil, err
}
dir := filepath.Dir(ex)
reader.mbtilesPath = filepath.Join(dir, mbtilesPath)
reader.pmtilesPath = filepath.Join(dir, pmtilesPath)
mbtilesFiles, err := filepath.Glob(filepath.Join(reader.mbtilesPath, "*.mbtiles"))
if err != nil {
return nil, err
}
for _, mbtilesFile := range mbtilesFiles {
fileName := filepath.Base(mbtilesFile)
fileName = strings.TrimSuffix(fileName, ".mbtiles")
reader.loadMBTiles(fileName)
}
reader.loadPMTiles("pmtiles")
return reader, nil
}
func (t *TileReader) CheckPool(tileType, name string) bool {
// switch tileType {
// case MBTilesName:
// _, err := t.loadMBTiles(name)
// return err == nil
// case PMTilesName:
// _, err := t.loadPMTiles(name)
// return err == nil
// default:
// return false
// }
switch tileType {
case MBTilesName:
_, ok := t.tilesPools.Load(MBTilesName + name)
return ok
case PMTilesName:
_, ok := t.tilesPools.Load(PMTilesName + "pmtiles")
return ok
default:
return false
}
}
func (t *TileReader) Close() error {
var err error
t.tilesPools.Range(func(key, value any) bool {
switch v := value.(type) {
case *sql.DB:
if e := v.Close(); e != nil {
err = e
}
case *pmtiles.FileBucket:
if e := v.Close(); e != nil {
err = e
}
}
return true
})
return err
}
func (t *TileReader) GetTileJSON(tileType, name string) (*model.TileJSON, error) {
var baseData map[string]interface{}
var err error
switch tileType {
case MBTilesName:
baseData, err = t.getMBTilesMetadata(name)
case PMTilesName:
baseData, err = t.getPMTilesMetadata("pmtiles", name)
default:
return nil, errors.New("tile type not supported")
}
if err != nil {
return nil, err
}
tileJSON := &model.TileJSON{
TileJSON: "3.0.0",
}
for name, value := range baseData {
switch name {
case "name":
tileJSON.Name = value.(string)
case "description":
tileJSON.Description = value.(string)
case "version":
tileJSON.Version = parseVersion(value.(string))
case "maxzoom":
zoom, _ := strconv.Atoi(value.(string))
tileJSON.MaxZoom = &zoom
case "minzoom":
zoom, _ := strconv.Atoi(value.(string))
tileJSON.MinZoom = &zoom
case "fillzoom":
zoom, _ := strconv.Atoi(value.(string))
tileJSON.FillZoom = &zoom
case "format":
tileJSON.Format = value.(string)
case "bounds":
bounds := strings.Split(value.(string), ",")
minX, _ := strconv.ParseFloat(bounds[0], 64)
minY, _ := strconv.ParseFloat(bounds[1], 64)
maxX, _ := strconv.ParseFloat(bounds[2], 64)
maxY, _ := strconv.ParseFloat(bounds[3], 64)
tileJSON.Bounds = []float64{minX, minY, maxX, maxY}
case "center":
center := strings.Split(value.(string), ",")
x, _ := strconv.ParseFloat(center[0], 64)
y, _ := strconv.ParseFloat(center[1], 64)
zoom, _ := strconv.Atoi(center[2])
tileJSON.Center = []float64{x, y, float64(zoom)}
case "json":
var layerData map[string]interface{}
err := json.Unmarshal([]byte(value.(string)), &layerData)
if err != nil {
continue
}
vectorLayersData, ok := layerData["vector_layers"].([]interface{})
if !ok {
// fmt.Println("Error asserting vector_layers to []interface{}")
continue
}
vectorLayers := make([]model.VectorLayer, len(vectorLayersData))
for i, layerData := range vectorLayersData {
layerMap, ok := layerData.(map[string]interface{})
if !ok {
// fmt.Println("Error asserting layer data to map[string]interface{}")
continue
}
layerJSON, err := json.Marshal(layerMap)
if err != nil {
// fmt.Println("Error marshalling layer data to JSON:", err)
continue
}
var vectorLayer model.VectorLayer
err = json.Unmarshal(layerJSON, &vectorLayer)
if err != nil {
// fmt.Println("Error unmarshalling layer data to VectorLayer:", err)
continue
}
vectorLayers[i] = vectorLayer
}
tileJSON.VectorLayers = vectorLayers
case "vector_layers":
vectorLayersData, ok := value.([]interface{})
if !ok {
continue
}
vectorLayers := make([]model.VectorLayer, len(vectorLayersData))
for i, layerData := range vectorLayersData {
layerMap, ok := layerData.(map[string]interface{})
if !ok {
continue
}
layerJSON, err := json.Marshal(layerMap)
if err != nil {
continue
}
var vectorLayer model.VectorLayer
err = json.Unmarshal(layerJSON, &vectorLayer)
if err != nil {
continue
}
vectorLayers[i] = vectorLayer
}
tileJSON.VectorLayers = vectorLayers
}
}
return tileJSON, nil
}
func (t *TileReader) GetTile(tileType, name string, z, x, y int) ([]byte, error) {
switch tileType {
case MBTilesName:
return t.getMBTilesTile(name, z, x, y)
case PMTilesName:
return t.getPMTilesTile("pmtiles", name, z, x, y)
default:
return nil, errors.New("tile type not supported")
}
}
func (t *TileReader) loadMBTiles(name string) (*sql.DB, error) {
poolName := MBTilesName + name
if pool, ok := t.tilesPools.Load(poolName); ok {
return pool.(*sql.DB), nil
}
db, err := sql.Open("sqlite3", filepath.Join(t.mbtilesPath, name+".mbtiles"))
if err != nil {
return nil, err
}
// defer db.Close()
t.tilesPools.Store(poolName, db)
return db, nil
}
func (t *TileReader) loadPMTiles(name string) (*pmtiles.FileBucket, error) {
poolName := PMTilesName + name
if bucket, ok := t.tilesPools.Load(poolName); ok {
return bucket.(*pmtiles.FileBucket), nil
}
bucket := pmtiles.NewFileBucket(t.pmtilesPath)
t.tilesPools.Store(poolName, bucket)
return bucket, nil
}
func (t *TileReader) getMBTilesMetadata(name string) (map[string]interface{}, error) {
cacheKey := MetadataName + name
if metadata, ok := t.tileCache.Get(cacheKey); ok {
return metadata.(map[string]interface{}), nil
}
pool, err := t.loadMBTiles(name)
if err != nil {
return nil, err
}
rows, err := pool.Query("SELECT name, value FROM metadata")
if err != nil {
return nil, err
}
defer rows.Close()
metadata := make(map[string]interface{})
for rows.Next() {
var name, value string
if err := rows.Scan(&name, &value); err != nil {
return nil, err
}
metadata[name] = value
}
t.cacheLock.Lock()
defer t.cacheLock.Unlock()
t.tileCache.Add(cacheKey, metadata)
return metadata, nil
}
func (t *TileReader) getPMTilesMetadata(name, key string) (map[string]interface{}, error) {
cacheKey := MetadataName + key
if pmtilesmetadata, ok := t.tileCache.Get(cacheKey); ok {
return pmtilesmetadata.(map[string]interface{}), nil
}
bucket, err := t.loadPMTiles(name)
if err != nil {
return nil, err
}
pmtilesheader, err := t.getPMTilesHeader(name, key)
if err != nil {
return nil, err
}
metadataReader, err := bucket.NewRangeReader(context.Background(), key+".pmtiles", int64(pmtilesheader.MetadataOffset), int64(pmtilesheader.MetadataLength))
if err != nil {
return nil, err
}
defer metadataReader.Close()
pmtilesmetadata, err := pmtiles.DeserializeMetadata(metadataReader, pmtilesheader.InternalCompression)
if err != nil {
return nil, err
}
t.cacheLock.Lock()
defer t.cacheLock.Unlock()
t.tileCache.Add(cacheKey, pmtilesmetadata)
return pmtilesmetadata, nil
}
func (t *TileReader) getPMTilesHeader(name, key string) (*pmtiles.HeaderV3, error) {
cacheKey := PMTilesHeader + key
if pmtilesheader, ok := t.tileCache.Get(cacheKey); ok {
header := pmtilesheader.(pmtiles.HeaderV3)
return &header, nil
}
bucket, err := t.loadPMTiles(name)
if err != nil {
return nil, err
}
headerReader, err := bucket.NewRangeReader(context.Background(), key+".pmtiles", 0, pmtiles.HeaderV3LenBytes)
if err != nil {
return nil, err
}
defer headerReader.Close()
headerData, err := io.ReadAll(headerReader)
if err != nil {
return nil, err
}
pmtilesheader, err := pmtiles.DeserializeHeader(headerData)
if err != nil {
return nil, err
}
t.cacheLock.Lock()
defer t.cacheLock.Unlock()
t.tileCache.Add(cacheKey, pmtilesheader)
return &pmtilesheader, nil
}
func (t *TileReader) getPMTilesEntries(name, key string) ([]pmtiles.EntryV3, error) {
cacheKey := PMTilesEntries + key
if pmtilesentries, ok := t.tileCache.Get(cacheKey); ok {
return pmtilesentries.([]pmtiles.EntryV3), nil
}
pmtilesheader, err := t.getPMTilesHeader(name, key)
if err != nil {
return nil, err
}
bucket, err := t.loadPMTiles(name)
if err != nil {
return nil, err
}
entriesReader, err := bucket.NewRangeReader(context.Background(), key+".pmtiles", int64(pmtilesheader.LeafDirectoryOffset), int64(pmtilesheader.LeafDirectoryLength))
if err != nil {
return nil, err
}
defer entriesReader.Close()
entriesData, err := io.ReadAll(entriesReader)
if err != nil {
return nil, err
}
pmtilesentries := pmtiles.DeserializeEntries(bytes.NewBuffer(entriesData), pmtilesheader.InternalCompression)
t.cacheLock.Lock()
defer t.cacheLock.Unlock()
t.tileCache.Add(cacheKey, pmtilesentries)
return pmtilesentries, nil
}
func (t *TileReader) getMBTilesTile(name string, z, x, y int) ([]byte, error) {
cacheKey := TileName + name + "_" + strconv.Itoa(z) + "_" + strconv.Itoa(x) + "_" + strconv.Itoa(y)
if tileData, ok := t.tileCache.Get(cacheKey); ok {
return tileData.([]byte), nil
}
pool, err := t.loadMBTiles(name)
if err != nil {
return nil, err
}
realY := (1 << uint(z)) - y - 1
rows, err := pool.Query("SELECT tile_data FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?", z, x, realY)
if err != nil {
return nil, err
}
defer rows.Close()
if !rows.Next() {
return nil, nil
}
var tileData []byte
if err := rows.Scan(&tileData); err != nil {
return nil, err
}
t.cacheLock.Lock()
defer t.cacheLock.Unlock()
t.tileCache.Add(cacheKey, tileData)
return tileData, nil
}
func (t *TileReader) getPMTilesTile(name, key string, z, x, y int) ([]byte, error) {
cacheKey := TileName + key + "_" + strconv.Itoa(z) + "_" + strconv.Itoa(x) + "_" + strconv.Itoa(y)
if tileData, ok := t.tileCache.Get(cacheKey); ok {
return tileData.([]byte), nil
}
bucket, err := t.loadPMTiles(name)
if err != nil {
return nil, err
}
pmtilesentries, err := t.getPMTilesEntries(name, key)
if err != nil {
return nil, err
}
keyID := pmtiles.ZxyToID(uint8(z), uint32(x), uint32(y))
entry, ok := pmtiles.FindTile(pmtilesentries, keyID)
if !ok {
return nil, errors.New("tile not found")
}
tileReader, err := bucket.NewRangeReader(context.Background(), key+".pmtiles", int64(entry.Offset), int64(entry.Length))
if err != nil {
return nil, err
}
defer tileReader.Close()
tileData, err := io.ReadAll(tileReader)
if err != nil {
return nil, err
}
t.cacheLock.Lock()
defer t.cacheLock.Unlock()
t.tileCache.Add(cacheKey, tileData)
return tileData, nil
}
func parseVersion(version string) string {
// 将版本号字符串按点分割
parts := strings.SplitN(version, ".", 3)
// 初始化默认值
major, minor, patch := 0, 0, 0
// 解析每个部分并转换为整数
if len(parts) > 0 {
major, _ = strconv.Atoi(parts[0])
}
if len(parts) > 1 {
minor, _ = strconv.Atoi(parts[1])
}
if len(parts) > 2 {
patch, _ = strconv.Atoi(parts[2])
}
return fmt.Sprintf("%d.%d.%d", major, minor, patch)
}