From 783afc1c3edc59c7e7b0598d35561f9f139e41ba Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Mon, 22 Nov 2021 16:36:17 +0100 Subject: [PATCH] Make map tiles configurable Closes #5 --- config.go | 8 +++ example-config.yml | 7 +++ geo.go | 43 --------------- geoMap.go | 7 ++- geoTiles.go | 82 ++++++++++++++++++++++++++++ geoTiles_test.go | 95 +++++++++++++++++++++++++++++++++ geoTrack.go | 33 +++++++----- httpRouters.go | 2 +- templates/assets/js/geomap.js | 9 ++-- templates/assets/js/geotrack.js | 9 ++-- templates/geomap.gohtml | 8 ++- templates/trackdetails.gohtml | 8 ++- 12 files changed, 243 insertions(+), 68 deletions(-) create mode 100644 geoTiles.go create mode 100644 geoTiles_test.go diff --git a/config.go b/config.go index f0aff05..e1b820a 100644 --- a/config.go +++ b/config.go @@ -26,6 +26,7 @@ type config struct { PrivateMode *configPrivateMode `mapstructure:"privateMode"` EasterEgg *configEasterEgg `mapstructure:"easterEgg"` Debug bool `mapstructure:"debug"` + MapTiles *configMapTiles `mapstructure:"mapTiles"` } type configServer struct { @@ -279,6 +280,13 @@ type configWebmention struct { DisableReceiving bool `mapstructure:"disableReceiving"` } +type configMapTiles struct { + Source string `mapstructure:"source"` + Attribution string `mapstructure:"attribution"` + MinZoom int `mapstructure:"minZoom"` + MaxZoom int `mapstructure:"maxZoom"` +} + func (a *goBlog) initConfig(file string) error { log.Println("Initialize configuration...") if file != "" { diff --git a/example-config.yml b/example-config.yml index 3043afa..a2be07c 100644 --- a/example-config.yml +++ b/example-config.yml @@ -131,6 +131,13 @@ pathRedirects: to: "/$1$2" type: 301 # custom redirect type +# Map tiles +mapTiles: + source: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" # (Optional) URL to use for map tiles + attribution: "© OpenStreetMap contributors" # (Optional) Attribution for map tiles + minZoom: 0 # (Optional) Minimum zoom level + maxZoom: 20 # (Optional) Maximum zoom level + # Blogs defaultBlog: en # Default blog (needed because you can define multiple blogs) blogs: diff --git a/geo.go b/geo.go index 5aa4d75..b93d370 100644 --- a/geo.go +++ b/geo.go @@ -5,8 +5,6 @@ import ( "fmt" "io" "net/http" - "net/http/httptest" - "net/http/httputil" "net/url" "strings" @@ -76,44 +74,3 @@ func geoOSMLink(g *gogeouri.Geo) string { //go:embed leaflet/* var leafletFiles embed.FS - -func (a *goBlog) proxyTiles(basePath string) http.HandlerFunc { - osmUrl, _ := url.Parse("https://tile.openstreetmap.org/") - tileProxy := http.StripPrefix(basePath, httputil.NewSingleHostReverseProxy(osmUrl)) - return func(w http.ResponseWriter, r *http.Request) { - targetUrl := *osmUrl - targetUrl.Path = r.URL.Path - proxyRequest, _ := http.NewRequest(http.MethodGet, targetUrl.String(), nil) - // Copy request headers - for _, k := range []string{ - "Accept-Encoding", - "Accept-Language", - "Accept", - "Cache-Control", - "If-Modified-Since", - "If-None-Match", - "User-Agent", - } { - proxyRequest.Header.Set(k, r.Header.Get(k)) - } - rec := httptest.NewRecorder() - tileProxy.ServeHTTP(rec, proxyRequest) - res := rec.Result() - // Copy result headers - for _, k := range []string{ - "Accept-Ranges", - "Access-Control-Allow-Origin", - "Age", - "Cache-Control", - "Content-Length", - "Content-Type", - "Etag", - "Expires", - } { - w.Header().Set(k, res.Header.Get(k)) - } - w.WriteHeader(res.StatusCode) - _, _ = io.Copy(w, res.Body) - _ = res.Body.Close() - } -} diff --git a/geoMap.go b/geoMap.go index fcaa878..25601ec 100644 --- a/geoMap.go +++ b/geoMap.go @@ -88,8 +88,11 @@ func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) { BlogString: blog, Canonical: a.getFullAddress(mapPath), Data: map[string]interface{}{ - "locations": locationsJson, - "tracks": tracksJson, + "locations": locationsJson, + "tracks": tracksJson, + "attribution": a.getMapAttribution(), + "minzoom": a.getMinZoom(), + "maxzoom": a.getMaxZoom(), }, }) } diff --git a/geoTiles.go b/geoTiles.go new file mode 100644 index 0000000..3d7f20d --- /dev/null +++ b/geoTiles.go @@ -0,0 +1,82 @@ +package main + +import ( + "io" + "net/http" + "strings" + + "github.com/go-chi/chi/v5" +) + +func (a *goBlog) proxyTiles(basePath string) http.HandlerFunc { + tileSource := "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" + if c := a.cfg.MapTiles; c != nil && c.Source != "" { + tileSource = c.Source + } + return func(w http.ResponseWriter, r *http.Request) { + // Create a new request to proxy to the tile server + targetUrl := tileSource + targetUrl = strings.ReplaceAll(targetUrl, "{s}", chi.URLParam(r, "s")) + targetUrl = strings.ReplaceAll(targetUrl, "{z}", chi.URLParam(r, "z")) + targetUrl = strings.ReplaceAll(targetUrl, "{x}", chi.URLParam(r, "x")) + targetUrl = strings.ReplaceAll(targetUrl, "{y}", chi.URLParam(r, "y")) + proxyRequest, _ := http.NewRequestWithContext(r.Context(), http.MethodGet, targetUrl, nil) + proxyRequest.Header.Set(userAgent, appUserAgent) + // Copy request headers + for _, k := range []string{ + "Accept-Encoding", + "Accept-Language", + "Accept", + "Cache-Control", + "If-Modified-Since", + "If-None-Match", + "User-Agent", + } { + proxyRequest.Header.Set(k, r.Header.Get(k)) + } + // Do the request + res, err := a.httpClient.Do(proxyRequest) + if err != nil { + a.serveError(w, r, err.Error(), http.StatusInternalServerError) + return + } + // Copy result headers + for _, k := range []string{ + "Accept-Ranges", + "Access-Control-Allow-Origin", + "Age", + "Cache-Control", + "Content-Length", + "Content-Type", + "Etag", + "Expires", + } { + w.Header().Set(k, res.Header.Get(k)) + } + // Copy result + w.WriteHeader(res.StatusCode) + _, _ = io.Copy(w, res.Body) + _ = res.Body.Close() + } +} + +func (a *goBlog) getMinZoom() int { + if c := a.cfg.MapTiles; c != nil { + return c.MinZoom + } + return 0 +} + +func (a *goBlog) getMaxZoom() int { + if c := a.cfg.MapTiles; c != nil && c.MaxZoom > 0 { + return c.MaxZoom + } + return 20 +} + +func (a *goBlog) getMapAttribution() string { + if c := a.cfg.MapTiles; c != nil { + return c.Attribution + } + return `© OpenStreetMap contributors` +} diff --git a/geoTiles_test.go b/geoTiles_test.go new file mode 100644 index 0000000..dda8927 --- /dev/null +++ b/geoTiles_test.go @@ -0,0 +1,95 @@ +package main + +import ( + "net/http" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_proxyTiles(t *testing.T) { + app := &goBlog{ + cfg: &config{}, + } + + hc := &fakeHttpClient{ + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello, World!")) + }), + } + app.httpClient = hc + + // Default tile source + + m := chi.NewMux() + m.Get("/x/tiles/{s}/{z}/{x}/{y}.png", app.proxyTiles("/x/tiles")) + + req, err := http.NewRequest(http.MethodGet, "https://example.org/x/tiles/c/8/134/84.png", nil) + require.NoError(t, err) + resp, err := doHandlerRequest(req, m) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "https://c.tile.openstreetmap.org/8/134/84.png", hc.req.URL.String()) + + // Custom tile source + + app.cfg.MapTiles = &configMapTiles{ + Source: "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", + } + + m = chi.NewMux() + m.Get("/x/tiles/{s}/{z}/{x}/{y}.png", app.proxyTiles("/x/tiles")) + + req, err = http.NewRequest(http.MethodGet, "https://example.org/x/tiles/c/8/134/84.png", nil) + require.NoError(t, err) + resp, err = doHandlerRequest(req, m) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "https://c.tile.opentopomap.org/8/134/84.png", hc.req.URL.String()) +} + +func Test_getMinZoom(t *testing.T) { + app := &goBlog{ + cfg: &config{}, + } + + assert.Equal(t, 0, app.getMinZoom()) + + app.cfg.MapTiles = &configMapTiles{ + MinZoom: 1, + } + + assert.Equal(t, 1, app.getMinZoom()) +} + +func Test_getMaxZoom(t *testing.T) { + app := &goBlog{ + cfg: &config{}, + } + + assert.Equal(t, 20, app.getMaxZoom()) + + app.cfg.MapTiles = &configMapTiles{ + MaxZoom: 10, + } + + assert.Equal(t, 10, app.getMaxZoom()) +} + +func Test_getMapAttribution(t *testing.T) { + app := &goBlog{ + cfg: &config{}, + } + + assert.Equal(t, `© OpenStreetMap contributors`, app.getMapAttribution()) + + app.cfg.MapTiles = &configMapTiles{ + Attribution: "attribution", + } + + assert.Equal(t, "attribution", app.getMapAttribution()) +} diff --git a/geoTrack.go b/geoTrack.go index 30aa615..883d188 100644 --- a/geoTrack.go +++ b/geoTrack.go @@ -18,14 +18,16 @@ func (p *post) HasTrack() bool { } type trackResult struct { - HasPoints bool - Paths [][]*trackPoint - PathsJSON string - Points []*trackPoint - PointsJSON string - Kilometers string - Hours string - Name string + HasPoints bool + Paths [][]*trackPoint + PathsJSON string + Points []*trackPoint + PointsJSON string + Kilometers string + Hours string + Name string + MapAttribution string + MinZoom, MaxZoom int } func (a *goBlog) getTrack(p *post) (result *trackResult, err error) { @@ -56,12 +58,15 @@ func (a *goBlog) getTrack(p *post) (result *trackResult, err error) { } result = &trackResult{ - HasPoints: len(parseResult.paths) > 0 && len(parseResult.paths[0]) > 0, - Paths: parseResult.paths, - PathsJSON: string(pathsJSON), - Points: parseResult.points, - PointsJSON: string(pointsJSON), - Name: parseResult.gpxData.Name, + HasPoints: len(parseResult.paths) > 0 && len(parseResult.paths[0]) > 0, + Paths: parseResult.paths, + PathsJSON: string(pathsJSON), + Points: parseResult.points, + PointsJSON: string(pointsJSON), + Name: parseResult.gpxData.Name, + MapAttribution: a.getMapAttribution(), + MinZoom: a.getMinZoom(), + MaxZoom: a.getMaxZoom(), } if parseResult.md != nil { diff --git a/httpRouters.go b/httpRouters.go index 46b51f2..48d1379 100644 --- a/httpRouters.go +++ b/httpRouters.go @@ -99,7 +99,7 @@ func (a *goBlog) mediaFilesRouter(r chi.Router) { // Various other routes func (a *goBlog) xRouter(r chi.Router) { r.Use(a.privateModeHandler) - r.Get("/tiles/{z}/{x}/{y}.png", a.proxyTiles("/x/tiles")) + r.Get("/tiles/{s}/{z}/{x}/{y}.png", a.proxyTiles("/x/tiles")) r.With(cacheLoggedIn, a.cacheMiddleware).HandleFunc("/leaflet/*", a.serveFs(leafletFiles, "/x/")) } diff --git a/templates/assets/js/geomap.js b/templates/assets/js/geomap.js index 71122d9..999dabc 100644 --- a/templates/assets/js/geomap.js +++ b/templates/assets/js/geomap.js @@ -3,10 +3,13 @@ let locations = (mapEl.dataset.locations == "") ? [] : JSON.parse(mapEl.dataset.locations) let tracks = (mapEl.dataset.tracks == "") ? [] : JSON.parse(mapEl.dataset.tracks) - let map = L.map('map') + let map = L.map('map', { + minZoom: mapEl.dataset.minzoom, + maxZoom: mapEl.dataset.maxzoom + }) - L.tileLayer("/x/tiles/{z}/{x}/{y}.png", { - attribution: '© OpenStreetMap contributors' + L.tileLayer("/x/tiles/{s}/{z}/{x}/{y}.png", { + attribution: mapEl.dataset.attribution, }).addTo(map) let features = [] diff --git a/templates/assets/js/geotrack.js b/templates/assets/js/geotrack.js index 25325d2..559def3 100644 --- a/templates/assets/js/geotrack.js +++ b/templates/assets/js/geotrack.js @@ -3,10 +3,13 @@ let paths = (mapEl.dataset.paths == "") ? [] : JSON.parse(mapEl.dataset.paths) let points = (mapEl.dataset.points == "") ? [] : JSON.parse(mapEl.dataset.points) - let map = L.map('map') + let map = L.map('map', { + minZoom: mapEl.dataset.minzoom, + maxZoom: mapEl.dataset.maxzoom + }) - L.tileLayer("/x/tiles/{z}/{x}/{y}.png", { - attribution: '© OpenStreetMap contributors' + L.tileLayer("/x/tiles/{s}/{z}/{x}/{y}.png", { + attribution: mapEl.dataset.attribution, }).addTo(map) let features = [] diff --git a/templates/geomap.gohtml b/templates/geomap.gohtml index 3811142..d93d363 100644 --- a/templates/geomap.gohtml +++ b/templates/geomap.gohtml @@ -11,7 +11,13 @@ {{ if .Data.nolocations }}

{{ string .Blog.Lang "nolocations" }}

{{ else }} -
+
{{ end }} diff --git a/templates/trackdetails.gohtml b/templates/trackdetails.gohtml index 05cf90a..a879409 100644 --- a/templates/trackdetails.gohtml +++ b/templates/trackdetails.gohtml @@ -5,7 +5,13 @@ {{ if $track.HasPoints }} {{ $lang := .Blog.Lang }}

{{ with $track.Name }}{{ . }} {{ end }}{{ with $track.Kilometers }}🏁 {{ . }} {{ string $lang "kilometers" }} {{ end }}{{ with $track.Hours }}⌛ {{ . }}{{ end }}

-
+
{{ end }} {{ end }}