eintopf.info@v0.13.16/service/osm/osm.go (about)

     1  // Copyright (C) 2024 The Eintopf authors
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    15  
    16  package osm
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"io"
    22  	"log"
    23  	"net/http"
    24  	"os"
    25  	"path"
    26  	"strconv"
    27  	"strings"
    28  	"time"
    29  
    30  	"eintopf.info/internal/xhttp"
    31  	"github.com/go-chi/chi/v5"
    32  )
    33  
    34  type Tiler interface {
    35  	GetTile(ctx context.Context, z, x, y int) ([]byte, error)
    36  }
    37  
    38  type FileCache struct {
    39  	cacheDir string
    40  }
    41  
    42  func (f FileCache) GetTile(ctx context.Context, z, x, y int) ([]byte, error) {
    43  	p := path.Join(f.cacheDir, strconv.Itoa(z), strconv.Itoa(x), strconv.Itoa(y))
    44  	if _, err := os.Stat(p); errors.Is(err, os.ErrNotExist) {
    45  		return nil, err
    46  	}
    47  	tile, err := os.ReadFile(p)
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  	return tile, nil
    52  }
    53  
    54  func (f FileCache) SetTile(ctx context.Context, z, x, y int, tile []byte) error {
    55  	dir := path.Join(f.cacheDir, strconv.Itoa(z), strconv.Itoa(x))
    56  	err := os.MkdirAll(dir, os.ModePerm)
    57  	if err != nil {
    58  		return err
    59  	}
    60  	p := path.Join(f.cacheDir, strconv.Itoa(z), strconv.Itoa(x), strconv.Itoa(y))
    61  	return os.WriteFile(p, tile, os.ModePerm)
    62  }
    63  
    64  func (f FileCache) Clear() error {
    65  	return os.Remove(f.cacheDir)
    66  }
    67  
    68  type Client struct {
    69  	serverURL string
    70  }
    71  
    72  func (c Client) GetTile(ctx context.Context, z, x, y int) ([]byte, error) {
    73  	u := c.serverURL
    74  	u = strings.Replace(u, "{z}", strconv.Itoa(z), 1)
    75  	u = strings.Replace(u, "{x}", strconv.Itoa(x), 1)
    76  	u = strings.Replace(u, "{y}", strconv.Itoa(y), 1)
    77  
    78  	r, err := http.NewRequest("GET", u, nil)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  	r.Header.Set("User-Agent", "eintopf")
    83  	hc := &http.Client{}
    84  
    85  	response, err := hc.Do(r)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	data, err := io.ReadAll(response.Body)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	return data, nil
    95  }
    96  
    97  type Service struct {
    98  	cache        FileCache
    99  	client       Client
   100  	lastModified time.Time
   101  }
   102  
   103  func NewService(cacheDir string, tileServer string) *Service {
   104  	return &Service{
   105  		cache:        FileCache{cacheDir: cacheDir},
   106  		client:       Client{serverURL: tileServer},
   107  		lastModified: time.Now(),
   108  	}
   109  }
   110  
   111  func (s *Service) GetTile(ctx context.Context, z, x, y int) ([]byte, error) {
   112  	tile, err := s.cache.GetTile(ctx, z, x, y)
   113  	if errors.Is(err, os.ErrNotExist) {
   114  		log.Printf("osm cache miss: %d/%d/%d", z, x, y)
   115  		var err2 error
   116  		tile, err2 = s.client.GetTile(ctx, z, x, y)
   117  		if err2 != nil {
   118  			return nil, err2
   119  		}
   120  		err2 = s.cache.SetTile(ctx, z, x, y, tile)
   121  		if err2 != nil {
   122  			return nil, err2
   123  		}
   124  	} else if err != nil {
   125  		return nil, err
   126  	}
   127  	return tile, nil
   128  }
   129  
   130  func (s *Service) ClearCache(ctx context.Context) error {
   131  	err := s.cache.Clear()
   132  	if err != nil {
   133  		return err
   134  	}
   135  	s.lastModified = time.Now()
   136  	return nil
   137  }
   138  
   139  func (s *Service) LastModified() time.Time {
   140  	return s.lastModified
   141  }
   142  
   143  func Router(service *Service) func(chi.Router) {
   144  	return func(r chi.Router) {
   145  		r.With(xhttp.LastModified(service)).Get("/tile/{z}/{x}/{y}.png", func(w http.ResponseWriter, r *http.Request) {
   146  			z, err := xhttp.ReadParamInt(r, "z")
   147  			if err != nil {
   148  				xhttp.WriteBadRequest(r.Context(), w, err)
   149  				return
   150  			}
   151  			x, err := xhttp.ReadParamInt(r, "x")
   152  			if err != nil {
   153  				xhttp.WriteBadRequest(r.Context(), w, err)
   154  				return
   155  			}
   156  
   157  			y, err := xhttp.ReadParamInt(r, "y")
   158  			if err != nil {
   159  				xhttp.WriteBadRequest(r.Context(), w, err)
   160  				return
   161  			}
   162  
   163  			tile, err := service.GetTile(r.Context(), z, x, y)
   164  			if err != nil {
   165  				xhttp.WriteInternalError(r.Context(), w, err)
   166  				return
   167  			}
   168  			w.Header().Add("Content-Type", "image/png")
   169  			w.Write(tile)
   170  
   171  		})
   172  	}
   173  }