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:
commit
4414239854
5 changed files with 339 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
staticmap
|
||||
cache
|
22
cache.go
Normal file
22
cache.go
Normal 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
42
cache_filesystem.go
Normal 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
205
main.go
Normal 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
68
map.go
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue