go-staticmaps/tile_fetcher.go

196 lines
4.1 KiB
Go

// Copyright 2016, 2017 Florian Pigorsch. All rights reserved.
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package sm
import (
"bytes"
"errors"
"fmt"
"image"
_ "image/jpeg" // to be able to decode jpegs
_ "image/png" // to be able to decode pngs
"io"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
)
var errTileNotFound = errors.New("error 404: tile not found")
// TileFetcher downloads map tile images from a TileProvider
type TileFetcher struct {
tileProvider *TileProvider
cache TileCache
userAgent string
online bool
}
// Tile defines a single map tile
type Tile struct {
Img image.Image
X, Y, Zoom int
}
// NewTileFetcher creates a new Tilefetcher struct
func NewTileFetcher(tileProvider *TileProvider, cache TileCache, online bool) *TileFetcher {
t := new(TileFetcher)
t.tileProvider = tileProvider
t.cache = cache
t.userAgent = "Mozilla/5.0+(compatible; go-staticmaps/0.1; https://github.com/flopp/go-staticmaps)"
t.online = online
return t
}
// SetUserAgent sets the HTTP user agent string used when downloading map tiles
func (t *TileFetcher) SetUserAgent(a string) {
t.userAgent = a
}
func (t *TileFetcher) url(zoom, x, y int) string {
shard := ""
ss := len(t.tileProvider.Shards)
if len(t.tileProvider.Shards) > 0 {
shard = t.tileProvider.Shards[(x+y)%ss]
}
return t.tileProvider.getURL(shard, zoom, x, y, t.tileProvider.APIKey)
}
func cacheFileName(cache TileCache, providerName string, zoom, x, y int) string {
return path.Join(
cache.Path(),
providerName,
strconv.Itoa(zoom),
strconv.Itoa(x),
strconv.Itoa(y),
)
}
// Fetch download (or retrieves from the cache) a tile image for the specified zoom level and tile coordinates
func (t *TileFetcher) Fetch(tile *Tile) error {
if t.cache != nil {
fileName := cacheFileName(t.cache, t.tileProvider.Name, tile.Zoom, tile.X, tile.Y)
cachedImg, err := t.loadCache(fileName)
if err == nil {
tile.Img = cachedImg
return nil
}
}
if !t.online {
return errTileNotFound
}
url := t.url(tile.Zoom, tile.X, tile.Y)
data, err := t.download(url)
if err != nil {
return err
}
img, _, err := image.Decode(bytes.NewBuffer(data))
if err != nil {
return err
}
if t.cache != nil {
fileName := cacheFileName(t.cache, t.tileProvider.Name, tile.Zoom, tile.X, tile.Y)
if err := t.storeCache(fileName, data); err != nil {
log.Printf("Failed to store map tile as '%s': %s", fileName, err)
}
}
tile.Img = img
return nil
}
func (t *TileFetcher) download(url string) ([]byte, error) {
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("User-Agent", t.userAgent)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
switch resp.StatusCode {
case http.StatusOK:
// Great! Nothing to do.
case http.StatusNotFound:
return nil, errTileNotFound
default:
return nil, fmt.Errorf("GET %s: %s", url, resp.Status)
}
defer resp.Body.Close()
contents, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return contents, nil
}
func (t *TileFetcher) loadCache(fileName string) (image.Image, error) {
file, err := os.Open(fileName)
if err != nil {
return nil, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
return img, nil
}
func (t *TileFetcher) createCacheDir(path string) error {
src, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return os.MkdirAll(path, t.cache.Perm())
}
return err
}
if src.IsDir() {
return nil
}
return fmt.Errorf("file exists but is not a directory: %s", path)
}
func (t *TileFetcher) storeCache(fileName string, data []byte) error {
dir, _ := filepath.Split(fileName)
if err := t.createCacheDir(dir); err != nil {
return err
}
// Create file using the configured directory create permission with the
// 'x' bit removed.
file, err := os.OpenFile(
fileName,
os.O_RDWR|os.O_CREATE|os.O_TRUNC,
t.cache.Perm()&0666,
)
if err != nil {
return err
}
defer file.Close()
if _, err = io.Copy(file, bytes.NewBuffer(data)); err != nil {
return err
}
return nil
}