1
0
Fork 0
mirror of https://github.com/Luzifer/staticmap.git synced 2024-12-05 14:14:02 +00:00

Initial version

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2017-06-27 22:49:53 +02:00
commit 4414239854
Signed by: luzifer
GPG key ID: DC2729FDD34BE99E
5 changed files with 339 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
staticmap
cache

22
cache.go Normal file
View file

@ -0,0 +1,22 @@
package main
import (
"crypto/sha256"
"fmt"
"io"
"strings"
"github.com/golang/geo/s2"
)
type cacheFunction func(center s2.LatLng, zoom int, marker []marker, x, y int) (io.ReadCloser, error)
func cacheKeyHelper(center s2.LatLng, zoom int, marker []marker, x, y int) string {
markerString := []string{}
for _, m := range marker {
markerString = append(markerString, m.String())
}
hashString := fmt.Sprintf("%s|%d|%s|%dx%d", center.String(), zoom, strings.Join(markerString, "+"), x, y)
return fmt.Sprintf("%x", sha256.Sum256([]byte(hashString)))
}

42
cache_filesystem.go Normal file
View file

@ -0,0 +1,42 @@
package main
import (
"bytes"
"io"
"io/ioutil"
"os"
"path"
"time"
"github.com/golang/geo/s2"
)
func filesystemCache(center s2.LatLng, zoom int, marker []marker, x, y int) (io.ReadCloser, error) {
cacheKey := cacheKeyHelper(center, zoom, marker, x, y)
cacheFileName := path.Join(cfg.CacheDir, cacheKey[0:2], cacheKey+".png")
if info, err := os.Stat(cacheFileName); err == nil && info.ModTime().Add(cfg.ForceCache).After(time.Now()) {
return os.Open(cacheFileName)
}
// No cache hit, generate a new map
mapReader, err := generateMap(center, zoom, marker, x, y)
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, mapReader); err != nil {
return nil, err
}
if err := os.MkdirAll(path.Dir(cacheFileName), 0755); err != nil {
return nil, err
}
if err := ioutil.WriteFile(cacheFileName, buf.Bytes(), 0644); err != nil {
return nil, err
}
return ioutil.NopCloser(buf), err
}

205
main.go Normal file
View file

@ -0,0 +1,205 @@
package main
import (
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
httpHelper "github.com/Luzifer/go_helpers/http"
"github.com/Luzifer/rconfig"
log "github.com/Sirupsen/logrus"
"github.com/golang/geo/s2"
"github.com/gorilla/mux"
colorful "github.com/lucasb-eyer/go-colorful"
)
var (
cfg struct {
CacheDir string `flag:"cache-dir" default:"cache" env:"CACHE_DIR" description:"Directory to save the cached images to"`
ForceCache time.Duration `flag:"force-cache" default:"24h" description:"Force map to be cached for this duration"`
Listen string `flag:"listen" default:":3000" description:"IP/Port to listen on"`
MaxSize string `flag:"max-size" default:"1024x1024" description:"Maximum map size requestable"`
VersionAndExit bool `flag:"version" default:"false" description:"Print version information and exit"`
}
mapMaxX, mapMaxY int
cacheFunc cacheFunction = filesystemCache // For now this is simply set and might be extended later
version = "dev"
)
func init() {
var err error
if err = rconfig.Parse(&cfg); err != nil {
log.Fatalf("Unable to parse CLI parameters")
}
if mapMaxX, mapMaxY, err = parseSize(cfg.MaxSize, false); err != nil {
log.Fatalf("Unable to parse max-size: %s", err)
}
if cfg.VersionAndExit {
fmt.Printf("staticmap %s\n", version)
}
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/status", func(res http.ResponseWriter, r *http.Request) { http.Error(res, "I'm fine", http.StatusOK) })
r.HandleFunc("/map.png", handleMapRequest)
log.Fatalf("HTTP Server exitted: %s", http.ListenAndServe(cfg.Listen, httpHelper.NewHTTPLogHandler(r)))
}
func handleMapRequest(res http.ResponseWriter, r *http.Request) {
var (
center *s2.LatLng
err error
mapReader io.ReadCloser
markers []marker
x, y int
zoom int
)
if center, err = parseCoordinate(r.URL.Query().Get("center")); err != nil {
http.Error(res, fmt.Sprintf("Unable to parse 'center' parameter: %s", err), http.StatusBadRequest)
return
}
if zoom, err = strconv.Atoi(r.URL.Query().Get("zoom")); err != nil {
http.Error(res, fmt.Sprintf("Unable to parse 'zoom' parameter: %s", err), http.StatusBadRequest)
return
}
if x, y, err = parseSize(r.URL.Query().Get("size"), true); err != nil {
http.Error(res, fmt.Sprintf("Unable to parse 'size' parameter: %s", err), http.StatusBadRequest)
return
}
if markers, err = parseMarkerLocations(r.URL.Query()["markers"]); err != nil {
http.Error(res, fmt.Sprintf("Unable to parse 'markers' parameter: %s", err), http.StatusBadRequest)
return
}
if mapReader, err = cacheFunc(*center, zoom, markers, x, y); err != nil {
log.Error("Map render failed: %s (Request: %s)", err, r.URL.String())
http.Error(res, fmt.Sprintf("I experienced difficulties rendering your map: %s", err), http.StatusInternalServerError)
return
}
defer mapReader.Close()
res.Header().Set("Content-Type", "image/png")
res.Header().Set("Cache-Control", "public")
io.Copy(res, mapReader)
}
func parseCoordinate(coord string) (*s2.LatLng, error) {
if coord == "" {
return nil, errors.New("No coordinate given")
}
parts := strings.Split(coord, ",")
if len(parts) != 2 {
return nil, errors.New("Coordinate not in format lat,lon")
}
var (
lat, lon float64
err error
)
if lat, err = strconv.ParseFloat(parts[0], 64); err != nil {
return nil, errors.New("Latitude not parseable as float")
}
if lon, err = strconv.ParseFloat(parts[1], 64); err != nil {
return nil, errors.New("Longitude not parseable as float")
}
pt := s2.LatLngFromDegrees(lat, lon)
return &pt, nil
}
func parseSize(size string, validate bool) (x, y int, err error) {
if size == "" {
return 0, 0, errors.New("No size given")
}
parts := strings.Split(size, "x")
if len(parts) != 2 {
return 0, 0, errors.New("Size not in format 600x300")
}
if x, err = strconv.Atoi(parts[0]); err != nil {
return
}
if y, err = strconv.Atoi(parts[1]); err != nil {
return
}
if validate {
if x > mapMaxX || y > mapMaxY {
err = fmt.Errorf("Map size exceeds allowed bounds of %dx%d", mapMaxX, mapMaxY)
return
}
}
return
}
func parseMarkerLocations(markers []string) ([]marker, error) {
if markers == nil {
// No markers parameters passed, lets ignore this
return nil, nil
}
result := []marker{}
for _, markerInformation := range markers {
parts := strings.Split(markerInformation, "|")
var (
size = markerSizes["small"]
col = markerColors["red"]
)
for _, p := range parts {
switch {
case strings.HasPrefix(p, "size:"):
if s, ok := markerSizes[strings.TrimPrefix(p, "size:")]; ok {
size = s
} else {
return nil, fmt.Errorf("Bad marker size %q", strings.TrimPrefix(p, "size:"))
}
case strings.HasPrefix(p, "color:0x"):
if c, err := colorful.Hex("#" + strings.TrimPrefix(p, "color:0x")); err == nil {
col = c
} else {
return nil, fmt.Errorf("Unable to parse color %q: %s", strings.TrimPrefix(p, "color:"), err)
}
case strings.HasPrefix(p, "color:"):
if c, ok := markerColors[strings.TrimPrefix(p, "color:")]; ok {
col = c
} else {
return nil, fmt.Errorf("Bad color name %q", strings.TrimPrefix(p, "color:"))
}
default:
pos, err := parseCoordinate(p)
if err != nil {
return nil, fmt.Errorf("Unparsable chunk found in marker: %q", p)
}
result = append(result, marker{
pos: *pos,
color: col,
size: size,
})
}
}
}
return result, nil
}

68
map.go Normal file
View file

@ -0,0 +1,68 @@
package main
import (
"bytes"
"fmt"
"image/color"
"io"
staticMap "github.com/flopp/go-staticmaps"
"github.com/fogleman/gg"
"github.com/golang/geo/s2"
)
var markerColors = map[string]color.Color{
"black": color.RGBA{R: 145, G: 145, B: 145, A: 0xff},
"brown": color.RGBA{R: 178, G: 154, B: 123, A: 0xff},
"green": color.RGBA{R: 168, G: 196, B: 68, A: 0xff},
"purple": color.RGBA{R: 177, G: 150, B: 191, A: 0xff},
"yellow": color.RGBA{R: 237, G: 201, B: 107, A: 0xff},
"blue": color.RGBA{R: 163, G: 196, B: 253, A: 0xff},
"gray": color.RGBA{R: 204, G: 204, B: 204, A: 0xff},
"orange": color.RGBA{R: 229, G: 165, B: 68, A: 0xff},
"red": color.RGBA{R: 246, G: 118, B: 112, A: 0xff},
"white": color.RGBA{R: 245, G: 244, B: 241, A: 0xff},
}
type markerSize float64
var markerSizes = map[string]markerSize{
"tiny": 10,
"mid": 15,
"small": 20,
}
type marker struct {
pos s2.LatLng
color color.Color
size markerSize
}
func (m marker) String() string {
r, g, b, a := m.color.RGBA()
return fmt.Sprintf("%s|%.0f|%d,%d,%d,%d", m.pos.String(), m.size, r, g, b, a)
}
func generateMap(center s2.LatLng, zoom int, marker []marker, x, y int) (io.Reader, error) {
ctx := staticMap.NewContext()
ctx.SetSize(x, y)
ctx.SetCenter(center)
ctx.SetZoom(zoom)
if marker != nil {
for _, m := range marker {
ctx.AddMarker(staticMap.NewMarker(m.pos, m.color, float64(m.size)))
}
}
staticMap.TileFetcherUserAgent = fmt.Sprintf("Mozilla/5.0+(compatible; staticmap/%s; https://github.com/Luzifer/staticmap)", version)
img, err := ctx.Render()
if err != nil {
return nil, err
}
pngCtx := gg.NewContextForImage(img)
pngBuf := new(bytes.Buffer)
return pngBuf, pngCtx.EncodePNG(pngBuf)
}