mirror of https://github.com/jlelse/GoBlog
Add map feature
This commit is contained in:
parent
9369305c7d
commit
09804d7640
|
@ -70,6 +70,7 @@ type configBlog struct {
|
||||||
PostAsHome bool `mapstructure:"postAsHome"`
|
PostAsHome bool `mapstructure:"postAsHome"`
|
||||||
RandomPost *randomPost `mapstructure:"randomPost"`
|
RandomPost *randomPost `mapstructure:"randomPost"`
|
||||||
Comments *comments `mapstructure:"comments"`
|
Comments *comments `mapstructure:"comments"`
|
||||||
|
Map *configMap `mapstructure:"map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type section struct {
|
type section struct {
|
||||||
|
@ -145,6 +146,11 @@ type comments struct {
|
||||||
Enabled bool `mapstructure:"enabled"`
|
Enabled bool `mapstructure:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type configMap struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
Path string `mapstructure:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
type configUser struct {
|
type configUser struct {
|
||||||
Nick string `mapstructure:"nick"`
|
Nick string `mapstructure:"nick"`
|
||||||
Name string `mapstructure:"name"`
|
Name string `mapstructure:"name"`
|
||||||
|
|
2
geo.go
2
geo.go
|
@ -12,6 +12,8 @@ import (
|
||||||
"github.com/thoas/go-funk"
|
"github.com/thoas/go-funk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const geoParam = "location"
|
||||||
|
|
||||||
func (a *goBlog) geoTitle(g *gogeouri.Geo, lang string) string {
|
func (a *goBlog) geoTitle(g *gogeouri.Geo, lang string) string {
|
||||||
if name, ok := g.Parameters["name"]; ok && len(name) > 0 && name[0] != "" {
|
if name, ok := g.Parameters["name"]; ok && len(name) > 0 && name[0] != "" {
|
||||||
return name[0]
|
return name[0]
|
||||||
|
|
|
@ -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),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
14
http.go
14
http.go
|
@ -538,6 +538,16 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
|
||||||
r.Get(brPath+".opml", a.serveBlogrollExport)
|
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
|
// Sitemap
|
||||||
|
@ -573,6 +583,8 @@ func (a *goBlog) refreshCSPDomains() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cspHeader = "Content-Security-Policy"
|
||||||
|
|
||||||
func (a *goBlog) securityHeaders(next http.Handler) http.Handler {
|
func (a *goBlog) securityHeaders(next http.Handler) http.Handler {
|
||||||
a.refreshCSPDomains()
|
a.refreshCSPDomains()
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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-Content-Type-Options", "nosniff")
|
||||||
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
||||||
w.Header().Set("X-Xss-Protection", "1; mode=block")
|
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 != "" {
|
if a.cfg.Server.Tor && a.torAddress != "" {
|
||||||
w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI))
|
w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI))
|
||||||
}
|
}
|
||||||
|
|
|
@ -231,6 +231,10 @@ footer {
|
||||||
transition: transform 2s ease;
|
transition: transform 2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Print */
|
/* Print */
|
||||||
@media print {
|
@media print {
|
||||||
html {
|
html {
|
||||||
|
|
24
postsDb.go
24
postsDb.go
|
@ -227,6 +227,7 @@ type postsRequestConfig struct {
|
||||||
publishedYear, publishedMonth, publishedDay int
|
publishedYear, publishedMonth, publishedDay int
|
||||||
randomOrder bool
|
randomOrder bool
|
||||||
withoutParameters bool
|
withoutParameters bool
|
||||||
|
withOnlyParameters []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildPostsQuery(c *postsRequestConfig, selection string) (query string, args []interface{}) {
|
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
|
return query, args
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *database) getPostParameters(path string) (params map[string][]string, err error) {
|
func (d *database) getPostParameters(path string, parameters ...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))
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Result
|
||||||
var name, value string
|
var name, value string
|
||||||
params = map[string][]string{}
|
params = map[string][]string{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
|
@ -343,7 +361,7 @@ func (d *database) getPosts(config *postsRequestConfig) (posts []*post, err erro
|
||||||
Status: postStatus(status),
|
Status: postStatus(status),
|
||||||
}
|
}
|
||||||
if !config.withoutParameters {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ const (
|
||||||
templateNotificationsAdmin = "notificationsadmin"
|
templateNotificationsAdmin = "notificationsadmin"
|
||||||
templateWebmentionAdmin = "webmentionadmin"
|
templateWebmentionAdmin = "webmentionadmin"
|
||||||
templateBlogroll = "blogroll"
|
templateBlogroll = "blogroll"
|
||||||
|
templateGeoMap = "geomap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *goBlog) initRendering() error {
|
func (a *goBlog) initRendering() error {
|
||||||
|
|
|
@ -195,6 +195,10 @@ footer * {
|
||||||
transition: transform 2s ease;
|
transition: transform 2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Print */
|
/* Print */
|
||||||
@media print {
|
@media print {
|
||||||
html {
|
html {
|
||||||
|
|
|
@ -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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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 })
|
||||||
|
})()
|
|
@ -0,0 +1,31 @@
|
||||||
|
{{ define "title" }}
|
||||||
|
<title>{{ .Blog.Title }}</title>
|
||||||
|
{{ if not .Data.nolocations }}
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
|
||||||
|
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
|
||||||
|
crossorigin=""
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
|
||||||
|
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
|
||||||
|
crossorigin=""
|
||||||
|
></script>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "main" }}
|
||||||
|
<main>
|
||||||
|
{{ if .Data.nolocations }}
|
||||||
|
<p>{{ string .Blog.Lang "nolocations" }}</p>
|
||||||
|
{{ else }}
|
||||||
|
<div class="p" id="map" data-locations="{{ .Data.locations }}"></div>
|
||||||
|
<script defer src="{{ asset "js/geomap.js" }}"></script>
|
||||||
|
{{ end }}
|
||||||
|
</main>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "geomap" }}
|
||||||
|
{{ template "base" . }}
|
||||||
|
{{ end }}
|
|
@ -23,6 +23,7 @@ locationnotsupported: "Die Standort-API wird von diesem Browser nicht unterstüt
|
||||||
mediafiles: "Medien-Dateien"
|
mediafiles: "Medien-Dateien"
|
||||||
next: "Weiter"
|
next: "Weiter"
|
||||||
nofiles: "Keine Dateien"
|
nofiles: "Keine Dateien"
|
||||||
|
nolocations: "Keine Posts mit Standorten"
|
||||||
noposts: "Hier sind keine Posts."
|
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."
|
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"
|
posts: "Posts"
|
||||||
|
|
|
@ -33,6 +33,7 @@ mediafiles: "Media files"
|
||||||
nameopt: "Name (optional)"
|
nameopt: "Name (optional)"
|
||||||
next: "Next"
|
next: "Next"
|
||||||
nofiles: "No files"
|
nofiles: "No files"
|
||||||
|
nolocations: "No posts with locations"
|
||||||
noposts: "There are no posts here."
|
noposts: "There are no posts here."
|
||||||
notifications: "Notifications"
|
notifications: "Notifications"
|
||||||
oldcontent: "⚠️ This entry is already over one year old. It may no longer be up to date. Opinions may have changed."
|
oldcontent: "⚠️ This entry is already over one year old. It may no longer be up to date. Opinions may have changed."
|
||||||
|
|
Loading…
Reference in New Issue