diff --git a/README.md b/README.md index 64e28e6..aebd0df 100644 --- a/README.md +++ b/README.md @@ -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/) diff --git a/example/postmap.json b/example/postmap.json new file mode 100644 index 0000000..bbb5095 --- /dev/null +++ b/example/postmap.json @@ -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 +} diff --git a/example/postmap.png b/example/postmap.png new file mode 100644 index 0000000..3bf36cb Binary files /dev/null and b/example/postmap.png differ diff --git a/main.go b/main.go index 90ea4fc..770390c 100644 --- a/main.go +++ b/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") diff --git a/postmap.go b/postmap.go new file mode 100644 index 0000000..cee9a81 --- /dev/null +++ b/postmap.go @@ -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 +}