From 09804d7640765ab72159906db04d3faf1e966847 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Tue, 6 Jul 2021 21:06:39 +0200 Subject: [PATCH] Add map feature --- config.go | 6 +++ geo.go | 2 + geoMap.go | 64 ++++++++++++++++++++++++++++++ http.go | 14 ++++++- original-assets/styles/styles.scss | 4 ++ postsDb.go | 24 +++++++++-- render.go | 1 + templates/assets/css/styles.css | 4 ++ templates/assets/js/geomap.js | 22 ++++++++++ templates/geomap.gohtml | 31 +++++++++++++++ templates/strings/de.yaml | 1 + templates/strings/default.yaml | 1 + 12 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 geoMap.go create mode 100644 templates/assets/js/geomap.js create mode 100644 templates/geomap.gohtml diff --git a/config.go b/config.go index 995f4f5..6337583 100644 --- a/config.go +++ b/config.go @@ -70,6 +70,7 @@ type configBlog struct { PostAsHome bool `mapstructure:"postAsHome"` RandomPost *randomPost `mapstructure:"randomPost"` Comments *comments `mapstructure:"comments"` + Map *configMap `mapstructure:"map"` } type section struct { @@ -145,6 +146,11 @@ type comments struct { Enabled bool `mapstructure:"enabled"` } +type configMap struct { + Enabled bool `mapstructure:"enabled"` + Path string `mapstructure:"path"` +} + type configUser struct { Nick string `mapstructure:"nick"` Name string `mapstructure:"name"` diff --git a/geo.go b/geo.go index 9664975..a8de5c9 100644 --- a/geo.go +++ b/geo.go @@ -12,6 +12,8 @@ import ( "github.com/thoas/go-funk" ) +const geoParam = "location" + func (a *goBlog) geoTitle(g *gogeouri.Geo, lang string) string { if name, ok := g.Parameters["name"]; ok && len(name) > 0 && name[0] != "" { return name[0] diff --git a/geoMap.go b/geoMap.go new file mode 100644 index 0000000..50f9175 --- /dev/null +++ b/geoMap.go @@ -0,0 +1,64 @@ +package main + +import ( + "encoding/json" + "net/http" +) + +func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) { + blog := r.Context().Value(blogContextKey).(string) + + allPostsWithLocation, err := a.db.getPosts(&postsRequestConfig{ + blog: blog, + status: statusPublished, + parameter: geoParam, + withOnlyParameters: []string{geoParam}, + }) + if err != nil { + a.serveError(w, r, err.Error(), http.StatusInternalServerError) + return + } + + if len(allPostsWithLocation) == 0 { + a.render(w, r, templateGeoMap, &renderData{ + BlogString: blog, + Data: map[string]interface{}{ + "nolocations": true, + }, + }) + return + } + + type templateLocation struct { + Lat float64 + Lon float64 + Post string + } + + var locations []*templateLocation + for _, p := range allPostsWithLocation { + if g := p.GeoURI(); g != nil { + locations = append(locations, &templateLocation{ + Lat: g.Latitude, + Lon: g.Longitude, + Post: p.Path, + }) + } + } + + jb, err := json.Marshal(locations) + if err != nil { + a.serveError(w, r, err.Error(), http.StatusInternalServerError) + return + } + + // Manipulate CSP header + w.Header().Set(cspHeader, w.Header().Get(cspHeader)+" https://unpkg.com/ https://tile.openstreetmap.org") + + a.render(w, r, templateGeoMap, &renderData{ + BlogString: blog, + Data: map[string]interface{}{ + "locations": string(jb), + }, + }) +} diff --git a/http.go b/http.go index 4b7a6f8..cc80ac8 100644 --- a/http.go +++ b/http.go @@ -538,6 +538,16 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) { r.Get(brPath+".opml", a.serveBlogrollExport) }) } + + // Geo map + if mc := blogConfig.Map; mc != nil && mc.Enabled { + mapPath := mc.Path + if mc.Path == "" { + mapPath = "/map" + } + r.With(a.privateModeHandler...).With(a.cache.cacheMiddleware, sbm).Get(blogConfig.getRelativePath(mapPath), a.serveGeoMap) + } + } // Sitemap @@ -573,6 +583,8 @@ func (a *goBlog) refreshCSPDomains() { } } +const cspHeader = "Content-Security-Policy" + func (a *goBlog) securityHeaders(next http.Handler) http.Handler { a.refreshCSPDomains() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -581,7 +593,7 @@ func (a *goBlog) securityHeaders(next http.Handler) http.Handler { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "SAMEORIGIN") w.Header().Set("X-Xss-Protection", "1; mode=block") - w.Header().Set("Content-Security-Policy", "default-src 'self'"+a.cspDomains) + w.Header().Set(cspHeader, "default-src 'self'"+a.cspDomains) if a.cfg.Server.Tor && a.torAddress != "" { w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI)) } diff --git a/original-assets/styles/styles.scss b/original-assets/styles/styles.scss index 34bd81d..05f9029 100644 --- a/original-assets/styles/styles.scss +++ b/original-assets/styles/styles.scss @@ -231,6 +231,10 @@ footer { transition: transform 2s ease; } +#map { + height: 400px; +} + /* Print */ @media print { html { diff --git a/postsDb.go b/postsDb.go index 9d3819b..2c30822 100644 --- a/postsDb.go +++ b/postsDb.go @@ -227,6 +227,7 @@ type postsRequestConfig struct { publishedYear, publishedMonth, publishedDay int randomOrder bool withoutParameters bool + withOnlyParameters []string } func buildPostsQuery(c *postsRequestConfig, selection string) (query string, args []interface{}) { @@ -303,11 +304,28 @@ func buildPostsQuery(c *postsRequestConfig, selection string) (query string, arg return query, args } -func (d *database) getPostParameters(path string) (params map[string][]string, err error) { - rows, err := d.query("select parameter, value from post_parameters where path = @path order by id", sql.Named("path", path)) +func (d *database) getPostParameters(path string, parameters ...string) (params map[string][]string, err error) { + var sqlArgs []interface{} + // Parameter filter + paramFilter := "" + if len(parameters) > 0 { + paramFilter = " and parameter in (" + for i, p := range parameters { + if i > 0 { + paramFilter += ", " + } + named := fmt.Sprintf("param%v", i) + paramFilter += "@" + named + sqlArgs = append(sqlArgs, sql.Named(named, p)) + } + paramFilter += ")" + } + // Query + rows, err := d.query("select parameter, value from post_parameters where path = @path"+paramFilter+" order by id", append(sqlArgs, sql.Named("path", path))...) if err != nil { return nil, err } + // Result var name, value string params = map[string][]string{} for rows.Next() { @@ -343,7 +361,7 @@ func (d *database) getPosts(config *postsRequestConfig) (posts []*post, err erro Status: postStatus(status), } if !config.withoutParameters { - if p.Parameters, err = d.getPostParameters(path); err != nil { + if p.Parameters, err = d.getPostParameters(path, config.withOnlyParameters...); err != nil { return nil, err } } diff --git a/render.go b/render.go index 8a374dc..73925d3 100644 --- a/render.go +++ b/render.go @@ -39,6 +39,7 @@ const ( templateNotificationsAdmin = "notificationsadmin" templateWebmentionAdmin = "webmentionadmin" templateBlogroll = "blogroll" + templateGeoMap = "geomap" ) func (a *goBlog) initRendering() error { diff --git a/templates/assets/css/styles.css b/templates/assets/css/styles.css index 3d4efb3..ba6ec9c 100644 --- a/templates/assets/css/styles.css +++ b/templates/assets/css/styles.css @@ -195,6 +195,10 @@ footer * { transition: transform 2s ease; } +#map { + height: 400px; +} + /* Print */ @media print { html { diff --git a/templates/assets/js/geomap.js b/templates/assets/js/geomap.js new file mode 100644 index 0000000..494f6d9 --- /dev/null +++ b/templates/assets/js/geomap.js @@ -0,0 +1,22 @@ +(function () { + let mapEl = document.getElementById('map') + let locations = JSON.parse(mapEl.dataset.locations) + + let map = L.map('map') + + L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }).addTo(map) + + let markers = [] + locations.forEach(loc => { + let marker = [loc.Lat, loc.Lon] + L.marker(marker).addTo(map).on('click', function () { + window.open(loc.Post, '_blank').focus() + }) + markers.push(marker) + }) + + map.fitBounds(markers) + map.zoomOut(2, { animate: false }) +})() \ No newline at end of file diff --git a/templates/geomap.gohtml b/templates/geomap.gohtml new file mode 100644 index 0000000..cae2489 --- /dev/null +++ b/templates/geomap.gohtml @@ -0,0 +1,31 @@ +{{ define "title" }} + {{ .Blog.Title }} + {{ if not .Data.nolocations }} + + + {{ end }} +{{ end }} + +{{ define "main" }} +
+ {{ if .Data.nolocations }} +

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

+ {{ else }} +
+ + {{ end }} +
+{{ end }} + +{{ define "geomap" }} + {{ template "base" . }} +{{ end }} \ No newline at end of file diff --git a/templates/strings/de.yaml b/templates/strings/de.yaml index 0e27e4a..a29b15b 100644 --- a/templates/strings/de.yaml +++ b/templates/strings/de.yaml @@ -23,6 +23,7 @@ locationnotsupported: "Die Standort-API wird von diesem Browser nicht unterstüt mediafiles: "Medien-Dateien" next: "Weiter" nofiles: "Keine Dateien" +nolocations: "Keine Posts mit Standorten" noposts: "Hier sind keine Posts." oldcontent: "⚠️ Dieser Eintrag ist bereits über ein Jahr alt. Er ist möglicherweise nicht mehr aktuell. Meinungen können sich geändert haben." posts: "Posts" diff --git a/templates/strings/default.yaml b/templates/strings/default.yaml index 0c3da33..3a7d43d 100644 --- a/templates/strings/default.yaml +++ b/templates/strings/default.yaml @@ -33,6 +33,7 @@ mediafiles: "Media files" nameopt: "Name (optional)" next: "Next" nofiles: "No files" +nolocations: "No posts with locations" noposts: "There are no posts here." notifications: "Notifications" oldcontent: "⚠️ This entry is already over one year old. It may no longer be up to date. Opinions may have changed."