mirror of
https://github.com/Luzifer/staticmap.git
synced 2024-12-05 22:24:02 +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)
|
||||
|
||||
### 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
|
||||
|
||||
- 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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -59,7 +60,8 @@ func main() {
|
|||
|
||||
r := mux.NewRouter()
|
||||
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)))
|
||||
}
|
||||
|
||||
|
@ -104,6 +106,35 @@ func handleMapRequest(res http.ResponseWriter, r *http.Request) {
|
|||
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) {
|
||||
if coord == "" {
|
||||
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