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, offset, length uint64) ([]pmtiles.EntryV3, error) { pmtilesheader, err := t.getPMTilesHeader(name, key) if err != nil { return nil, err } if offset == pmtilesheader.RootOffset && length == pmtilesheader.RootLength { cacheKey := PMTilesEntries + key if pmtilesentries, ok := t.tileCache.Get(cacheKey); ok { return pmtilesentries.([]pmtiles.EntryV3), nil } } bucket, err := t.loadPMTiles(name) if err != nil { return nil, err } entriesReader, err := bucket.NewRangeReader(context.Background(), key+".pmtiles", int64(offset), int64(length)) 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) if offset == pmtilesheader.RootOffset && length == pmtilesheader.RootLength { cacheKey := PMTilesEntries + key 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 } pmtilesheader, err := t.getPMTilesHeader(name, key) if err != nil { return nil, err } bucket, err := t.loadPMTiles(name) if err != nil { return nil, err } zValue, _ := strconv.ParseUint(strconv.Itoa(z), 10, 8) xValue, _ := strconv.ParseUint(strconv.Itoa(x), 10, 32) yValue, _ := strconv.ParseUint(strconv.Itoa(y), 10, 32) keyID := pmtiles.ZxyToID(uint8(zValue), uint32(xValue), uint32(yValue)) offset := pmtilesheader.RootOffset length := pmtilesheader.RootLength for depth := 0; depth <= 3; depth++ { entries, err := t.getPMTilesEntries(name, key, offset, length) if err != nil { return nil, err } entry, ok := pmtiles.FindTile(entries, keyID) if !ok { return nil, errors.New("tile not found") } if entry.RunLength > 0 { tileReader, err := bucket.NewRangeReader(context.Background(), key+".pmtiles", int64(pmtilesheader.TileDataOffset+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 } offset = pmtilesheader.LeafDirectoryOffset + entry.Offset length = uint64(entry.Length) } return nil, errors.New("tile not found") } 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) }