511 lines
12 KiB
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)
|
|
}
|