mirror of
https://github.com/Luzifer/staticmap.git
synced 2024-12-30 01:31:18 +00:00
Add support for overlay map generation with post request
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
a37dafe18d
commit
491effdd99
5 changed files with 182 additions and 1 deletions
|
@ -33,6 +33,14 @@ The map center is set to a coordinate within Hamburg, Germany with a zoom level
|
||||||
|
|
||||||
![](example/map.png)
|
![](example/map.png)
|
||||||
|
|
||||||
|
### Overlay support
|
||||||
|
|
||||||
|
Starting with version `v0.4.0` the generator supports map overlays for tiles based on transparent background (like [OpenFireMap](http://openfiremap.org/), [OpenSeaMap](http://www.openseamap.org/), ...). As those requests would be too complex for `GET` requests and are also not a common usecase they are implemented as `POST` requests containing a JSON object.
|
||||||
|
|
||||||
|
This example is generated with [OpenFireMap](http://openfiremap.org/) overlay tiles using the [`example/postmap.json`](example/postmap.json) file:
|
||||||
|
|
||||||
|
![](example/postmap.png)
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
- There is a Docker container available: [`luzifer/staticmap`](https://hub.docker.com/r/luzifer/staticmap/)
|
- There is a Docker container available: [`luzifer/staticmap`](https://hub.docker.com/r/luzifer/staticmap/)
|
||||||
|
|
28
example/postmap.json
Normal file
28
example/postmap.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"center": {
|
||||||
|
"lat": 53.5438,
|
||||||
|
"lon": 9.9768
|
||||||
|
},
|
||||||
|
"height": 500,
|
||||||
|
"markers": [
|
||||||
|
{
|
||||||
|
"color": "blue",
|
||||||
|
"coord": {
|
||||||
|
"lat": 53.54129165,
|
||||||
|
"lon": 9.98420576699353
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "yellow",
|
||||||
|
"coord": {
|
||||||
|
"lat": 53.54565525,
|
||||||
|
"lon": 9.9680555636958
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"overlays": [
|
||||||
|
"http://openfiremap.org/hytiles/{0}/{1}/{2}.png"
|
||||||
|
],
|
||||||
|
"width": 800,
|
||||||
|
"zoom": 15
|
||||||
|
}
|
BIN
example/postmap.png
Normal file
BIN
example/postmap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 416 KiB |
33
main.go
33
main.go
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -59,7 +60,8 @@ func main() {
|
||||||
|
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
r.HandleFunc("/status", func(res http.ResponseWriter, r *http.Request) { http.Error(res, "I'm fine", http.StatusOK) })
|
r.HandleFunc("/status", func(res http.ResponseWriter, r *http.Request) { http.Error(res, "I'm fine", http.StatusOK) })
|
||||||
r.Handle("/map.png", tollbooth.LimitFuncHandler(rateLimit, handleMapRequest))
|
r.Handle("/map.png", tollbooth.LimitFuncHandler(rateLimit, handleMapRequest)).Methods("GET")
|
||||||
|
r.Handle("/map.png", tollbooth.LimitFuncHandler(rateLimit, handlePostMapRequest)).Methods("POST")
|
||||||
log.Fatalf("HTTP Server exitted: %s", http.ListenAndServe(cfg.Listen, httpHelper.NewHTTPLogHandler(r)))
|
log.Fatalf("HTTP Server exitted: %s", http.ListenAndServe(cfg.Listen, httpHelper.NewHTTPLogHandler(r)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,6 +106,35 @@ func handleMapRequest(res http.ResponseWriter, r *http.Request) {
|
||||||
io.Copy(res, mapReader)
|
io.Copy(res, mapReader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handlePostMapRequest(res http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
body = postMapEnvelope{}
|
||||||
|
mapReader io.ReadCloser
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
http.Error(res, fmt.Sprintf("Unable to parse input: %s", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := body.toGenerateMapConfig()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(res, fmt.Sprintf("Unable to process input: %s", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mapReader, err = cacheFunc(opts); err != nil {
|
||||||
|
log.Errorf("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) {
|
func parseCoordinate(coord string) (s2.LatLng, error) {
|
||||||
if coord == "" {
|
if coord == "" {
|
||||||
return s2.LatLng{}, errors.New("No coordinate given")
|
return s2.LatLng{}, errors.New("No coordinate given")
|
||||||
|
|
114
postmap.go
Normal file
114
postmap.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
staticMap "github.com/Luzifer/go-staticmaps"
|
||||||
|
"github.com/golang/geo/s2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type postMapEnvelope struct {
|
||||||
|
Center postMapPoint `json:"center"`
|
||||||
|
Zoom int `json:"zoom"`
|
||||||
|
Markers postMapMarkers `json:"markers"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
DisableAttribution bool `json:"disable_attribution"`
|
||||||
|
Overlays postMapOverlay `json:"overlays"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p postMapEnvelope) toGenerateMapConfig() (generateMapConfig, error) {
|
||||||
|
result := generateMapConfig{
|
||||||
|
Center: p.Center.getPoint(),
|
||||||
|
Zoom: p.Zoom,
|
||||||
|
Width: p.Width,
|
||||||
|
Height: p.Height,
|
||||||
|
DisableAttribution: p.DisableAttribution,
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Width > mapMaxX || p.Height > mapMaxY {
|
||||||
|
return generateMapConfig{}, fmt.Errorf("Map size exceeds allowed bounds of %dx%d", mapMaxX, mapMaxY)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if result.Markers, err = p.Markers.toMarkers(); err != nil {
|
||||||
|
return generateMapConfig{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Overlays, err = p.Overlays.toOverlays(); err != nil {
|
||||||
|
return generateMapConfig{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type postMapMarker struct {
|
||||||
|
Size string `json:"size"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
Coord postMapPoint `json:"coord"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p postMapMarker) String() string {
|
||||||
|
parts := []string{}
|
||||||
|
|
||||||
|
if p.Size != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("size:%s", p.Size))
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Color != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("color:%s", p.Color))
|
||||||
|
}
|
||||||
|
|
||||||
|
parts = append(parts, p.Coord.String())
|
||||||
|
return strings.Join(parts, "|")
|
||||||
|
}
|
||||||
|
|
||||||
|
type postMapMarkers []postMapMarker
|
||||||
|
|
||||||
|
func (p postMapMarkers) toMarkers() ([]marker, error) {
|
||||||
|
raw := []string{}
|
||||||
|
for _, pm := range p {
|
||||||
|
raw = append(raw, pm.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseMarkerLocations(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
type postMapPoint struct {
|
||||||
|
Lat float64 `json:"lat"`
|
||||||
|
Lon float64 `json:"lon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p postMapPoint) String() string {
|
||||||
|
return fmt.Sprintf("%f,%f", p.Lat, p.Lon)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p postMapPoint) getPoint() s2.LatLng {
|
||||||
|
return s2.LatLngFromDegrees(p.Lat, p.Lon)
|
||||||
|
}
|
||||||
|
|
||||||
|
type postMapOverlay []string
|
||||||
|
|
||||||
|
func (p postMapOverlay) toOverlays() ([]*staticMap.TileProvider, error) {
|
||||||
|
result := []*staticMap.TileProvider{}
|
||||||
|
for _, pat := range p {
|
||||||
|
|
||||||
|
for _, v := range []string{`{0}`, `{1}`, `{2}`} {
|
||||||
|
if !strings.Contains(pat, v) {
|
||||||
|
return nil, fmt.Errorf("Placeholder %q not found in pattern %q", v, pat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pat = strings.NewReplacer(`{0}`, `%[2]d`, `{1}`, `%[3]d`, `{2}`, `%[4]d`).Replace(pat)
|
||||||
|
|
||||||
|
result = append(result, &staticMap.TileProvider{
|
||||||
|
Name: fmt.Sprintf("%x", sha256.Sum256([]byte(pat))),
|
||||||
|
TileSize: 256,
|
||||||
|
URLPattern: pat,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
Loading…
Reference in a new issue