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 }