From 6f347a4d3f5d00a63ca3685c159b04d668729cc2 Mon Sep 17 00:00:00 2001 From: Gautam Dey Date: Thu, 11 Oct 2018 10:56:56 -0700 Subject: [PATCH] Got the basics of the snap program working. --- cmd/snap/README.md | 49 ++++ cmd/snap/cmd/generate.go | 101 +++++++ cmd/snap/cmd/generate_bounds.go | 67 +++++ cmd/snap/cmd/generate_center.go | 77 ++++++ cmd/snap/cmd/root.go | 22 ++ cmd/snap/cmd/server.go | 415 ++++++++++++++++++++++++++++ cmd/snap/cmd/version.go | 18 ++ cmd/snap/main.go | 15 + cmd/snapshot_simple/main.go | 115 ++++---- internal/bounds/bounds.go | 17 +- mbgl/simplified/simplified.go | 15 + mbgl/simplified/simplified_linux.go | 1 + mbgl/simplified/snapshot.cpp | 4 +- mbgl/simplified/snapshot.go | 114 +++++++- 14 files changed, 961 insertions(+), 69 deletions(-) create mode 100644 cmd/snap/README.md create mode 100644 cmd/snap/cmd/generate.go create mode 100644 cmd/snap/cmd/generate_bounds.go create mode 100644 cmd/snap/cmd/generate_center.go create mode 100644 cmd/snap/cmd/root.go create mode 100644 cmd/snap/cmd/server.go create mode 100644 cmd/snap/cmd/version.go create mode 100644 cmd/snap/main.go diff --git a/cmd/snap/README.md b/cmd/snap/README.md new file mode 100644 index 0000000..6f26d50 --- /dev/null +++ b/cmd/snap/README.md @@ -0,0 +1,49 @@ +# Raster +Raster is a raster tile server that takes a mapbox style files and generates raster tiles for it. + + +# URLs that are supported by the application. + + + +## Raster Tile Server API + +``` +/styles/${style-name:string}/tiles/[${tilesize:int}/]${z:int}/${x:int}/${y:int}[@2x][.${file-extention:enum(jpg,png)}] +``` + +* style-name [required] : the name of the style. If loaded via the command line the style name will be "default" (currently this +the only thing that is supported.) +* tilesize [optional] : Default is 512, valid valus are positive multiples of 256. +* z [required] : the zoom +* x [required] : the x coordinate (column) in the slippy tile scheme. +* y [required] : the y coordinate (row) in the slippy tile scheme. +* @2x [optional] : to serve hight definition (retina) tiles. Omit to serve standard definition tiles. +* file-extension [optional] : the file type to encode the raster image in. Currently supported formats png, jpg. Default is jpg. + +## Static map server +For generating an image of a map at a given point and zoom use the following url. + +``` +/styles/${style-name:string}/static/${lon:float},${lat:float},${zoom:float},[${bearing:float],[${pitch:float}]]/${width:int}x${height:int}[@2x][.${file-extention:enum(jpg,png)}] +``` + +* style-name [required] : the name of the style. If loaded via the command line the style name will be "default" (currently this +the only thing that is supported.) +* lon [optional] : Default is 512, valid valus are positive multiples of 256. +* lat [required] : the zoom +* zoom [required] : the x coordinate (column) in the slippy tile scheme. +* bearing [required] : the y coordinate (row) in the slippy tile scheme. +* @2x [optional] : to serve hight definition (retina) tiles. Omit to serve standard definition tiles. +* file-extension [optional] : the file type to encode the raster image in. Currently supported formats png, jpg. Default is jpg. + +## Health check + +If the server is up this url will return a 200. + +``` +/health +``` + + + diff --git a/cmd/snap/cmd/generate.go b/cmd/snap/cmd/generate.go new file mode 100644 index 0000000..b0deca6 --- /dev/null +++ b/cmd/snap/cmd/generate.go @@ -0,0 +1,101 @@ +package cmd + +import ( + "errors" + "fmt" + "io" + "os" + "strconv" + "strings" + + "github.com/spf13/cobra" +) + +var cmdGenerateWidth int +var cmdGenerateHeight int +var cmdGeneratePPIRatio float64 +var cmdGeneratePitch float64 +var cmdGenerateBearing float64 +var cmdGenerateFormat string + +var cmdGenerate = &cobra.Command{ + Use: "generate", + Short: "generate an image for given map coordinates.", + Aliases: []string{"gen"}, +} + +func ValidateGenerateParams() error { + if cmdGenerateWidth < 1 { + return errors.New("--width must be greater then or equal to 1") + } + if cmdGenerateHeight < 1 { + return errors.New("--height must be greater then or equal to 1") + } + if cmdGeneratePitch < 0 { + return errors.New("--pitch must be greater then or equal to 0") + } + if cmdGenerateBearing < 0 { + return errors.New("--bearing must be greater then or equal to 0") + } + cmdGenerateFormat = strings.ToLower(cmdGenerateFormat) + if cmdGenerateFormat != "" && (cmdGenerateFormat != "jpg" || cmdGenerateFormat != "png") { + return errors.New("--format must be jpg or png") + } + return nil + +} + +func IsValidLngString(lng string) bool { + + f64, err := strconv.ParseFloat(strings.TrimSpace(lng), 64) + if err != nil { + return false + } + return -180.0 <= f64 && f64 <= 190.0 +} + +func IsValidLatString(lat string) bool { + + f64, err := strconv.ParseFloat(strings.TrimSpace(lat), 64) + if err != nil { + return false + } + return -90.0 <= f64 && f64 <= 90.0 +} + +func genImage(center [2]float64, zoom float64, output string) error { + ext, err := getFormatString(output) + if err != nil { + return err + } + var out io.Writer + + if output == "" { + out = os.Stdout + } else { + file, err := os.Create(output) + if err != nil { + return fmt.Errorf("error creating output file: %v -- %v\n", output, err) + } + defer file.Close() + out = file + } + + if _, err := generateZoomCenterImage(out, RootStyle, cmdGenerateWidth, cmdGenerateHeight, cmdGeneratePPIRatio, cmdGeneratePitch, cmdGenerateBearing, center, zoom, ext); err != nil { + return fmt.Errorf("failed to generate image: %v", err) + } + return nil +} + +func init() { + pf := cmdGenerate.PersistentFlags() + pf.IntVar(&cmdGenerateWidth, "width", 512, "Width of the image to generate.") + pf.IntVar(&cmdGenerateHeight, "height", 512, "Height of the image to generate.") + pf.Float64Var(&cmdGeneratePPIRatio, "ppiratio", 1.0, "The pixel per inch ratio.") + pf.Float64Var(&cmdGeneratePitch, "pitch", 0.0, "The pitch of the map.") + pf.Float64Var(&cmdGenerateBearing, "bearing", 0.0, "The bearing of the map.") + pf.StringVar(&cmdGenerateFormat, "format", "", "Defaults to the ext of the output file, or jpg if not provided.") + + cmdGenerate.AddCommand(cmdGenerateBounds) + cmdGenerate.AddCommand(cmdGenerateCenter) +} diff --git a/cmd/snap/cmd/generate_bounds.go b/cmd/snap/cmd/generate_bounds.go new file mode 100644 index 0000000..6ee0db1 --- /dev/null +++ b/cmd/snap/cmd/generate_bounds.go @@ -0,0 +1,67 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "os" + "strconv" + "strings" + + "github.com/go-spatial/geom/spherical" + "github.com/go-spatial/go-mbgl/internal/bounds" + mbgl "github.com/go-spatial/go-mbgl/mbgl/simplified" + "github.com/spf13/cobra" +) + +var cmdGenerateBounds = &cobra.Command{ + Use: "bounds lng lat lng lat [output_file.jpg]", + Short: "generate image using bounds", + Long: `use a bounds described as (lng lat lng lat) set of coordinates to generate the image.`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 4 { + return errors.New("requres at bounds( lng lat lng lat )") + } + if len(args) > 5 { + return errors.New("extra values provided") + } + if !IsValidLngString(args[0]) { + return fmt.Errorf("Longitude must be between -180.0 and 180.0 : given %v", args[0]) + } + if !IsValidLatString(args[1]) { + return fmt.Errorf("Latitude must be between -90.0 and 90.0 : given %v", args[1]) + } + if !IsValidLngString(args[2]) { + return fmt.Errorf("Longitude must be between -180.0 and 80.0 : given %v", args[2]) + } + if !IsValidLatString(args[3]) { + return fmt.Errorf("Latitude must be between -90.0 and 90.0 : given %v", args[3]) + } + return nil + }, + Run: commandGenerateBounds, +} + +func commandGenerateBounds(cmd *cobra.Command, args []string) { + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + mbgl.StartSnapshotManager(ctx) + + // Already checked in the Args validation function + lng1, _ := strconv.ParseFloat(strings.TrimSpace(args[0]), 64) + lat1, _ := strconv.ParseFloat(strings.TrimSpace(args[1]), 64) + lng2, _ := strconv.ParseFloat(strings.TrimSpace(args[2]), 64) + lat2, _ := strconv.ParseFloat(strings.TrimSpace(args[3]), 64) + hull := spherical.Hull([2]float64{lat1, lng1}, [2]float64{lat2, lng2}) + center, zoom := bounds.CenterZoom(hull, float64(cmdGenerateWidth), float64(cmdGenerateHeight)) + output := "" + if len(args) == 5 { + output = args[4] + } + if err := genImage(center, zoom, output); err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } +} diff --git a/cmd/snap/cmd/generate_center.go b/cmd/snap/cmd/generate_center.go new file mode 100644 index 0000000..4fbf2d6 --- /dev/null +++ b/cmd/snap/cmd/generate_center.go @@ -0,0 +1,77 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + mbgl "github.com/go-spatial/go-mbgl/mbgl/simplified" + "github.com/spf13/cobra" +) + +var cmdGenerateCenter = &cobra.Command{ + Use: "center lng lat zoom [output_file.jpg]", + Short: "generate image using center and zoom", + Long: `use a center point (lng lat) and a zoom to generate the image.`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 3 { + return errors.New("requres a center (lng lat) and a zoom.") + } + if len(args) > 4 { + return errors.New("extra values provided") + } + if !IsValidLngString(args[0]) { + return fmt.Errorf("Longitude must be between -180.0 and 180.0 : given %v", args[0]) + } + if !IsValidLatString(args[1]) { + return fmt.Errorf("Latitude must be between -90.0 and 90.0 : given %v", args[1]) + } + z64, err := strconv.ParseFloat(strings.TrimSpace(args[2]), 64) + if err != nil || z64 < 0 || z64 > 22 { + return fmt.Errorf("zoom (%v) must be a number from 0 - 22", args[2]) + } + + return nil + }, + Run: commandGenerateCenter, +} + +func getFormatString(file string) (ext string, err error) { + if cmdGenerateFormat != "" { + ext = cmdGenerateFormat + } else { + ext = strings.ToLower(strings.TrimPrefix(filepath.Ext(file), ".")) + if ext == "" { + ext = "jpg" + } else if ext != "jpg" && ext != "png" { + return "jpg", fmt.Errorf("output format(%v) must be jpg or png", ext) + } + } + return ext, nil +} + +func commandGenerateCenter(cmd *cobra.Command, args []string) { + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + mbgl.StartSnapshotManager(ctx) + + // Already checked in the Args validation function + lng, _ := strconv.ParseFloat(strings.TrimSpace(args[0]), 64) + lat, _ := strconv.ParseFloat(strings.TrimSpace(args[1]), 64) + zoom, _ := strconv.ParseFloat(strings.TrimSpace(args[2]), 64) + + output := "" + if len(args) == 4 { + output = args[3] + } + if err := genImage([2]float64{lng, lat}, zoom, output); err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } +} diff --git a/cmd/snap/cmd/root.go b/cmd/snap/cmd/root.go new file mode 100644 index 0000000..cf12964 --- /dev/null +++ b/cmd/snap/cmd/root.go @@ -0,0 +1,22 @@ +package cmd + +import "github.com/spf13/cobra" + +var RootCmd = &cobra.Command{ + Use: "snap", + Short: "snap is a raster tile server", + Long: `snap is a raster tile server version: ` + Version, +} + +const DefaultRootStyle = "https://raw.githubusercontent.com/go-spatial/tegola-web-demo/master/styles/hot-osm.json" + +var RootStyle string = DefaultRootStyle + +func init() { + + RootCmd.PersistentFlags().StringVarP(&RootStyle, "style", "s", DefaultRootStyle, "style to use. Style name will be default") + + RootCmd.AddCommand(cmdServer) + RootCmd.AddCommand(cmdVersion) + RootCmd.AddCommand(cmdGenerate) +} diff --git a/cmd/snap/cmd/server.go b/cmd/snap/cmd/server.go new file mode 100644 index 0000000..7178ecd --- /dev/null +++ b/cmd/snap/cmd/server.go @@ -0,0 +1,415 @@ +package cmd + +import ( + "bytes" + "context" + "fmt" + "image/jpeg" + "image/png" + "io" + "log" + "math" + "net/http" + "strconv" + "strings" + + "github.com/dimfeld/httptreemux" + "github.com/disintegration/imaging" + "github.com/go-spatial/geom/slippy" + + "github.com/go-spatial/go-mbgl/internal/bounds" + mbgl "github.com/go-spatial/go-mbgl/mbgl/simplified" + + "github.com/spf13/cobra" +) + +var cmdServer = &cobra.Command{ + Use: "serve", + Short: "Use snap as a tile server", + Aliases: []string{"server"}, + Long: `Use snap as a tile server. Maps tiles will be served at following urls: + /styles/:style-name/tiles/[tilesize]/:z/:x/:y[@2x].[file-extension] + or + /styles/:style-name/static/:lon,:lat,:zoom[,:bearing[,:pitch]]/:widthx:height[@2x][.:file-extension] + where + • style-name [required]: the name of the style. If loaded via the command line + the style name will be "default." If loaded via a config file the name of the + style to reference -- "default" for a config will be the first style in the + config file. + • tilesize [optional]: Default is 512x512 pixels. + • z [required]: the zoom + • x [required]: the x coordinate (column) in the slippy tile scheme. + • y [required]: the y coordinate (row) in the slippy tile scheme. + • @2 [optional]: to server high defination (retina) tiles. Omit to serve standard definition tiles. + • file-extension [optional]: the file type to encode the raster image in. Values: png, jpg. Default jpg. + • lon [required]: Longitude for the center point of the static map. -180 and 180. + • lat [required]: Latitude for the center point of the static map. -90 and 90. + • zoom [required]: Zoom level; a number between 0 and 20. + • bearing [optional]: Bearing rotates the map around it center. An number between 0 and 360; default 0. + • pitch [optional]: Pitch tilts the map, producing a perspective effect. A number between 0 and 60; default 0. + • width [required]: Width of the image; a number between 1 and 1280 pixels. + • height [required]: Height of the image; a number between 1 and 1280 pixels. +`, + Run: commandServer, +} + +const defaultServerAddress = ":8080" + +var cmdServerAddress string = defaultServerAddress + +func init() { + cmdServer.Flags().StringVarP(&cmdServerAddress, "address", "a", defaultServerAddress, "address to bind the tile server to.") +} + +func commandServer(cmd *cobra.Command, args []string) { + fmt.Println("Would start up the server here.", strings.Join(args, " , ")) + // start our server + router := newRouter() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if !strings.Contains(RootStyle, "://") { + RootStyle = "file://" + RootStyle + } + + mbgl.StartSnapshotManager(ctx) + + http.ListenAndServe(cmdServerAddress, router) +} + +func newRouter() *httptreemux.TreeMux { + + r := httptreemux.New() + r.GET("/health", serverHandlerHealth) + + group := r.NewGroup("/styles") + group.GET("/:style-name/tiles/:tilesize/:z/:x/:ypath", serverHandlerTileSize) + group.GET("/:style-name/tiles/:z/:x/:ypath", serverHandlerTile) + group.GET("/:style-name/static/:lonlatzoompath/:widthheightpath", serverHandlerStatic) + + return r +} + +func parsePPIPath(path []byte) (startPos int, val float64, err error) { + + // look for the last @...x + startPos = bytes.LastIndexByte(path, '@') + if startPos == -1 { + return -1, 1, nil + } + + endPos := bytes.IndexByte(path[startPos:], 'x') + if endPos == -1 { + return -1, 1, nil + } + + val, err = strconv.ParseFloat(string(path[startPos+1:startPos+endPos]), 64) + return startPos, val, err +} + +func parseAtAndDot(path []byte) (pre []byte, ppi float64, ext string, err error) { + + ext = "jpg" + + extPos := bytes.LastIndexByte(path, '.') + if extPos != -1 && extPos+1 < len(path) { + ext = string(bytes.ToLower(path[extPos+1:])) + } + + atPos, ppi, err := parsePPIPath(path) + if err != nil { + return pre, 1.0, ext, err + } + + switch { + case atPos != -1: + pre = path[:atPos] + case extPos != -1: + pre = path[:extPos] + default: + pre = path + } + + return pre, ppi, ext, nil +} + +func parseWidthHeightPath(widthHeightPath string) (width, height int, at2x float64, ext string, err error) { + + prePath, at2x, ext, err := parseAtAndDot([]byte(widthHeightPath)) + if err != nil { + return 0, 0, at2x, ext, fmt.Errorf("Error parsing extention or ppi (%v): %v", widthHeightPath, err) + } + + // width x height + xindex := bytes.IndexByte(prePath, 'x') + if xindex == -1 { + return 0, 0, at2x, ext, fmt.Errorf("expected width 'x' height. Did not find seperator. %v", string(prePath)) + } + + w64, err := strconv.ParseInt(string(prePath[:xindex]), 10, 64) + if err != nil { + return 0, 0, at2x, ext, fmt.Errorf("Error parsing width(%v): %v", string(prePath[:xindex]), err) + } + + h64, err := strconv.ParseInt(string(prePath[xindex+1:]), 10, 64) + if err != nil { + return 0, 0, at2x, ext, fmt.Errorf("Error parsing height(%v): %v", string(prePath[xindex+1:]), err) + } + + return int(w64), int(h64), at2x, ext, nil + +} + +func parseYPath(ypath string) (y uint, at2x float64, ext string, err error) { + + prePath, at2x, ext, err := parseAtAndDot([]byte(ypath)) + if err != nil { + return y, at2x, ext, fmt.Errorf("Error parsing extention or ppi (%v): %v", ypath, err) + } + y64, err := strconv.ParseUint(string(prePath), 10, 64) + return uint(y64), at2x, ext, err + +} + +func centerZoom(tilesize int, z, x, y uint) (center [2]float64, zoom float64) { + + var tile = slippy.Tile{ + Z: z, + X: x, + Y: y, + } + center = bounds.Center(tile.Extent4326()) + n := int(math.Log2(float64(tilesize / 256))) + zoom = float64(int(z) - n) + if zoom < 0.0 { + zoom = 0.0 + } + return center, zoom +} + +func serverHandlerHealth(w http.ResponseWriter, r *http.Request, params map[string]string) { + w.WriteHeader(http.StatusOK) +} + +// serverhandlerTileSize will handle the tiles url with a tilesize value in it. +func serverHandlerTileSize(w http.ResponseWriter, r *http.Request, params map[string]string) { + + y, ppi, ext, err := parseYPath(params["ypath"]) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + x64, err := strconv.ParseUint(strings.TrimSpace(params["x"]), 10, 64) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to parse param x: %v", err.Error()), http.StatusBadRequest) + return + } + if x64 < 0 { + http.Error(w, fmt.Sprintf("Failed param x should be greater then 0: %v", x64), http.StatusBadRequest) + return + } + z64, err := strconv.ParseUint(strings.TrimSpace(params["z"]), 10, 64) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to parse param z: %v", err.Error()), http.StatusBadRequest) + return + } + if z64 < 0 { + http.Error(w, fmt.Sprintf("Failed param z should be greater then 0: %v", z64), http.StatusBadRequest) + return + } + tileSize64, err := strconv.ParseUint(strings.TrimSpace(params["tilesize"]), 10, 64) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to parse param tile size: %v", err.Error()), http.StatusBadRequest) + return + } + if tileSize64%256 != 0 { + http.Error(w, fmt.Sprintf("Failed param tile-size should be a multiple of 256: %v", tileSize64), http.StatusBadRequest) + return + } + styleName := strings.ToLower(strings.TrimSpace(params["style-name"])) + if styleName != "default" { + http.Error(w, fmt.Sprintf("Unknown style %v", styleName), http.StatusNotFound) + return + } + + if ext != "jpg" && ext != "png" { + http.Error(w, fmt.Sprintf("only supported extentions are jpg and png, got [%v]\n", ext), http.StatusBadRequest) + return + } + + buffer := new(bytes.Buffer) + imgType, err := generateTileImage(buffer, RootStyle, float64(tileSize64), uint(z64), uint(x64), y, ppi, ext) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to generate %v image: %v", ext, err.Error()), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", imgType) + w.Header().Set("Content-Length", strconv.Itoa(buffer.Len())) + if _, err = io.Copy(w, buffer); err != nil { + log.Println("Got error writing to write out image:", err) + } + +} + +func generateTileImage(w io.Writer, style string, tsize float64, z, x, y uint, ppi float64, ext string) (string, error) { + + center, zoom := centerZoom(int(tsize), z, x, y) + + tilesize := uint32(float64(tsize) * ppi) + // .20 from expermintation + var pxBuffer uint32 + switch { + case zoom >= 2: + pxBuffer = uint32(float64(tsize) * 0.10) + case zoom >= 10: + pxBuffer = uint32(float64(tsize) * 0.20) + } + + snpsht := mbgl.Snapshotter{ + Style: style, + Width: tilesize + pxBuffer, + Height: tilesize + pxBuffer, + PPIRatio: ppi, + Lat: center[0], + Lng: center[1], + Zoom: zoom, + } + + return generateImage(w, snpsht, int(tilesize), int(tilesize), ext) +} + +func generateZoomCenterImage(w io.Writer, style string, width, height int, ppi, pitch, bearing float64, center [2]float64, zoom float64, ext string) (string, error) { + _width := uint32(float64(width) * ppi) + _height := uint32(float64(height) * ppi) + snpsht := mbgl.Snapshotter{ + Style: style, + Width: _width, + Height: _height, + PPIRatio: ppi, + Lat: center[1], + Lng: center[0], + Zoom: zoom, + Pitch: pitch, + Bearing: bearing, + } + + return generateImage(w, snpsht, int(_width), int(_height), ext) +} + +func generateImage(w io.Writer, param mbgl.Snapshotter, width, height int, ext string) (string, error) { + imageType := "unknown" + param.Zoom -= 1 + + img, err := mbgl.Snapshot1(param) + if err != nil { + return imageType, err + } + + cimg := imaging.CropCenter(img, width, height) + + switch ext { + case "png": + imageType = "image/png" + if err := png.Encode(w, cimg); err != nil { + return imageType, err + } + case "jpg": + imageType = "image/jpeg" + if err := jpeg.Encode(w, cimg, nil); err != nil { + return imageType, err + } + } + return imageType, nil +} + +func serverHandlerTile(w http.ResponseWriter, r *http.Request, params map[string]string) { + params["tilesize"] = "256" + serverHandlerTileSize(w, r, params) +} + +func serverHandlerStatic(w http.ResponseWriter, r *http.Request, params map[string]string) { + // /styles/:style-name/static/:lon,:lat,:zoom[,:bearing[,:pitch]]/:widthx:height[@2x][.:file-extension] + + styleName := strings.ToLower(strings.TrimSpace(params["style-name"])) + if styleName != "default" { + http.Error(w, fmt.Sprintf("Unknown style %v", styleName), http.StatusNotFound) + return + } + lnglatzParts := strings.Split(params["lonlatzoompath"], ",") + if len(lnglatzParts) < 3 { + http.Error(w, fmt.Sprintf("not enough params for lat lng and zoom: got %v", params["lonlatzoompath"]), http.StatusBadRequest) + return + } + lng64, err := strconv.ParseFloat(lnglatzParts[0], 64) + if err != nil { + http.Error(w, fmt.Sprintf("could not parse lng %v", err), http.StatusBadRequest) + return + } + lat64, err := strconv.ParseFloat(lnglatzParts[1], 64) + if err != nil { + http.Error(w, fmt.Sprintf("could not parse lat %v", err), http.StatusBadRequest) + return + } + zoom, err := strconv.ParseFloat(lnglatzParts[2], 64) + if err != nil { + http.Error(w, fmt.Sprintf("could not parse zoom %v", err), http.StatusBadRequest) + return + } + var bear64, pitch64 float64 + if len(lnglatzParts) >= 4 { + bear64, err = strconv.ParseFloat(lnglatzParts[3], 64) + if err != nil { + http.Error(w, fmt.Sprintf("could not parse bearing %v", err), http.StatusBadRequest) + return + } + } + if len(lnglatzParts) >= 5 { + pitch64, err = strconv.ParseFloat(lnglatzParts[4], 64) + if err != nil { + http.Error(w, fmt.Sprintf("could not parse pitch %v", err), http.StatusBadRequest) + return + } + } + width, height, ppi, ext, err := parseWidthHeightPath(params["widthheightpath"]) + if err != nil { + http.Error(w, fmt.Sprintf("could not parse width height %v", err), http.StatusBadRequest) + return + } + + if ext != "jpg" && ext != "png" { + http.Error(w, fmt.Sprintf("only supported extentions are jpg and png, got [%v]\n", ext), http.StatusBadRequest) + return + } + + width = int(float64(width) * ppi) + height = int(float64(height) * ppi) + + snpsht := mbgl.Snapshotter{ + Style: RootStyle, + Width: uint32(width), + Height: uint32(height), + PPIRatio: ppi, + Lat: lat64, + Lng: lng64, + Zoom: zoom, + Pitch: pitch64, + Bearing: bear64, + } + + buffer := new(bytes.Buffer) + + imgType, err := generateImage(buffer, snpsht, width, height, ext) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to generate %v image: %v", ext, err.Error()), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", imgType) + w.Header().Set("Content-Length", strconv.Itoa(buffer.Len())) + if _, err = io.Copy(w, buffer); err != nil { + log.Println("Got error writing to write out image:", err) + } + +} diff --git a/cmd/snap/cmd/version.go b/cmd/snap/cmd/version.go new file mode 100644 index 0000000..0f2d62f --- /dev/null +++ b/cmd/snap/cmd/version.go @@ -0,0 +1,18 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var Version = "version not set" + +var cmdVersion = &cobra.Command{ + Use: "version", + Short: "Print the version number.", + Long: `The version of the software. [` + Version + `]`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(Version) + }, +} diff --git a/cmd/snap/main.go b/cmd/snap/main.go new file mode 100644 index 0000000..cee4d9f --- /dev/null +++ b/cmd/snap/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "os" + + "github.com/go-spatial/go-mbgl/cmd/snap/cmd" +) + +func main() { + if err := cmd.RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/cmd/snapshot_simple/main.go b/cmd/snapshot_simple/main.go index 0ee6ba9..e6cf177 100644 --- a/cmd/snapshot_simple/main.go +++ b/cmd/snapshot_simple/main.go @@ -10,8 +10,9 @@ import ( "strconv" "strings" - "github.com/go-spatial/geom" "github.com/go-spatial/geom/slippy" + "github.com/go-spatial/geom/spherical" + "github.com/go-spatial/go-mbgl/internal/bounds" mbgl "github.com/go-spatial/go-mbgl/mbgl/simplified" ) @@ -20,58 +21,46 @@ snapshotter bounds "lat long lat long" -width=100, -height=100 style filename.pn snapshotter tile "z/x/y" style filename.png */ -type cmdType uint8 - -const ( - CmdUnknown = cmdType(iota) - CmdBounds - CmdTile -) - -func (c cmdType) String() string { - switch c { - case CmdBounds: - return "Bounds" - case CmdTile: - return "Tile" - default: - return "Unknown" - } -} - var FWidth uint var FHeight uint var FStyle string var FPixelRatio float64 +var FCenter [2]float64 +var FZoom uint -var FBounds geom.Extent -var FTile slippy.Tile var FOutputFilename string func usage() { fmt.Fprintf(os.Stderr, "%v [options...] \"lat long lat long\" output_filename.png\n", os.Args[0]) fmt.Fprintf(os.Stderr, "%v [options...] bounds \"lat long lat long\" output_filename.png\n", os.Args[0]) fmt.Fprintf(os.Stderr, "%v [options...] tile \"z/x/y\" output_filename.png\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "%v [options...] center \"lat long zoom\" output_filename.png\n", os.Args[0]) flag.PrintDefaults() } func parseBounds(boundString string) { var err error - bounds := strings.Split(boundString, " ") - if len(bounds) != 4 { - fmt.Fprintf(os.Stderr, "Error: invalid bounds provided — %v\n", boundString) + bnds := strings.Split(boundString, " ") + if len(bnds) != 4 { + fmt.Fprintf(os.Stderr, "Error: invalid bounds provided %v\n", boundString) usage() os.Exit(2) } - for i, bound := range bounds { - FBounds[i], err = strconv.ParseFloat(strings.TrimSpace(bound), 64) + var fbounds [4]float64 + for i, bound := range bnds { + fbounds[i], err = strconv.ParseFloat(strings.TrimSpace(bound), 64) if err != nil { - fmt.Fprintf(os.Stderr, "Error: invalid bounds provided — %v\n", boundString) + fmt.Fprintf(os.Stderr, "Error: invalid bounds provided %v\n", boundString) fmt.Fprintf(os.Stderr, "Error: unabled to parse %v(%v) as a float.\n", bound, i) usage() os.Exit(2) } } + // our fbounds is in lng lat lng lat order need to fix. to lat lng lat lng + hull := spherical.Hull([2]float64{fbounds[1], fbounds[0]}, [2]float64{fbounds[3], fbounds[2]}) + var zoom float64 + FCenter, zoom = bounds.CenterZoom(hull, float64(FWidth), float64(FHeight)) + FZoom = uint(zoom) } func parseTile(tileString string) { @@ -79,32 +68,62 @@ func parseTile(tileString string) { var v uint64 parts := strings.Split(tileString, "/") if len(parts) != 3 { - fmt.Fprintf(os.Stderr, "Error: invalid z/x/y coordinates — %v\n", tileString) + fmt.Fprintf(os.Stderr, "Error: invalid z/x/y coordinates %v\n", tileString) usage() os.Exit(2) } var label = [...]string{"Z", "X", "Y"} + var fTile slippy.Tile for i, part := range parts { v, err = strconv.ParseUint(strings.TrimSpace(part), 10, 64) if err != nil { - fmt.Fprintf(os.Stderr, "Error: invalid tile coordinates provided — %v\n", tileString) + fmt.Fprintf(os.Stderr, "Error: invalid tile coordinates provided %v\n", tileString) fmt.Fprintf(os.Stderr, "Error: unabled to parse %v %v as a uint.\n", label[i], part) usage() os.Exit(2) } switch i { case 0: - FTile.Z = uint(v) + fTile.Z = uint(v) case 1: - FTile.X = uint(v) + fTile.X = uint(v) case 2: - FTile.Y = uint(v) + fTile.Y = uint(v) + } + } + FCenter = bounds.Center(fTile.Extent3857()) + FZoom = fTile.Z +} + +func parseCenterZoom(centerZoomString string) { + var err error + errFn := func(label string, item string, expectedType string, err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Error: invalid center zoom provided %v\n", centerZoomString) + fmt.Fprintf(os.Stderr, "Error: unabled to parse %v(%v) as a %v.\n", label, expectedType, item) + usage() + os.Exit(2) } } + centerzoom := strings.Split(centerZoomString, " ") + if len(centerzoom) != 3 { + fmt.Fprintf(os.Stderr, "Error: invalid center zoom provided %v\n", centerZoomString) + usage() + os.Exit(2) + } + + FCenter[0], err = strconv.ParseFloat(strings.TrimSpace(centerzoom[0]), 64) + errFn("lat", centerzoom[0], "float", err) + FCenter[1], err = strconv.ParseFloat(strings.TrimSpace(centerzoom[1]), 64) + errFn("lng", centerzoom[1], "float", err) + zoom, err := strconv.ParseUint(strings.TrimSpace(centerzoom[2]), 10, 64) + errFn("zoom", centerzoom[2], "uint", err) + FZoom = uint(zoom) + } -func ParseFlags() cmdType { +func ParseFlags() { flag.UintVar(&FWidth, "width", 512, "Width of the image to generate.") flag.UintVar(&FWidth, "w", 512, "Width of the image to generate.") @@ -122,57 +141,47 @@ func ParseFlags() cmdType { os.Exit(2) } - var cmd cmdType var fileIdx = 2 switch strings.TrimSpace(strings.ToLower(flag.Arg(0))) { case "bounds": // The next variable should be the bounds seperated by spaces. parseBounds(flag.Arg(1)) - cmd = CmdBounds case "tile": // The next variable should be the coordinates sepearted by forward slash. parseTile(flag.Arg(1)) - - cmd = CmdTile + case "center": + parseCenterZoom(flag.Arg(1)) default: // assume bounds as the default subcommand parseBounds(flag.Arg(0)) fileIdx = 1 - cmd = CmdBounds } // Next should be the output filename. FOutputFilename = strings.TrimSpace(flag.Arg(fileIdx)) - return cmd - } func main() { var file *os.File var err error var img image.Image - _ = ParseFlags() + ParseFlags() + mbgl.NewRunLoop() + defer mbgl.DestroyRunLoop() snpsht := mbgl.Snapshotter{ Style: FStyle, Width: uint32(FWidth), Height: uint32(FHeight), - PPIRatio: int(FPixelRatio), + PPIRatio: FPixelRatio, + Lat: FCenter[0], + Lng: FCenter[1], + Zoom: float64(FZoom), } img, err = mbgl.Snapshot(snpsht) if err != nil { fmt.Fprintf(os.Stderr, "got an error creating the snapshot: %v\n", err) os.Exit(3) } - /* - switch cmd { - case CmdBounds: - img = ss.Snapshot(&FBounds, size) - - case CmdTile: - img = mbgl.SnapshotTile(ss, FTile, size) - - } - */ file, err = os.Create(FOutputFilename) if err != nil { fmt.Fprintf(os.Stderr, "error creating output file: %v -- %v\n", FOutputFilename, err) diff --git a/internal/bounds/bounds.go b/internal/bounds/bounds.go index 3964cb0..4f5ea22 100644 --- a/internal/bounds/bounds.go +++ b/internal/bounds/bounds.go @@ -141,7 +141,7 @@ func Zoom(bounds *geom.Extent, width, height float64) float64 { return math.Floor(math.Log(scale) / math.Ln2) } -func CenterZoom(bounds *geom.Extent, width, height float64) ([2]float64, float64) { +func Center(bounds *geom.Extent) [2]float64 { // assume ESPG3857 for now. prj := ESPG3857 if bounds == nil { @@ -149,9 +149,6 @@ func CenterZoom(bounds *geom.Extent, width, height float64) ([2]float64, float64 bounds = prj.Bounds() } - // calculate our zoom - zoom := Zoom(bounds, width, height) - // for lat lng geom.Extent should be laid out as follows: // {west, south, east, north} sw := [2]float64{bounds[1], bounds[0]} @@ -166,7 +163,15 @@ func CenterZoom(bounds *geom.Extent, width, height float64) ([2]float64, float64 centerPtY := (swPt[1] + nePt[1]) / 2 // 256 is the tile size. - center := prj.Unproject(prj.Untransform([2]float64{centerPtX, centerPtY}, 256)) + return prj.Unproject(prj.Untransform([2]float64{centerPtX, centerPtY}, 256)) +} - return center, zoom +func CenterZoom(bounds *geom.Extent, width, height float64) ([2]float64, float64) { + // assume ESPG3857 for now. + prj := ESPG3857 + if bounds == nil { + // we want the whole world. + bounds = prj.Bounds() + } + return Center(bounds), Zoom(bounds, width, height) } diff --git a/mbgl/simplified/simplified.go b/mbgl/simplified/simplified.go index ae17459..5735433 100644 --- a/mbgl/simplified/simplified.go +++ b/mbgl/simplified/simplified.go @@ -7,4 +7,19 @@ package simplified #cgo CXXFLAGS: -g */ import "C" +import "github.com/go-spatial/go-mbgl/mbgl" +var RunLoop *mbgl.RunLoop + +func NewRunLoop() { + if RunLoop == nil { + RunLoop = mbgl.NewRunLoop() + } +} + +func DestroyRunLoop() { + if RunLoop != nil { + RunLoop.Destruct() + RunLoop = nil + } +} diff --git a/mbgl/simplified/simplified_linux.go b/mbgl/simplified/simplified_linux.go index 4ed18bc..36b477a 100644 --- a/mbgl/simplified/simplified_linux.go +++ b/mbgl/simplified/simplified_linux.go @@ -20,6 +20,7 @@ package simplified #cgo LDFLAGS: ${SRCDIR}/../c/lib/linux/libuv.a #cgo LDFLAGS: -lrt -lpthread #cgo LDFLAGS: -lnsl -ldl +#cgo LDFLAGS: -static-libstdc++ */ import "C" diff --git a/mbgl/simplified/snapshot.cpp b/mbgl/simplified/snapshot.cpp index fd4115f..01407ac 100644 --- a/mbgl/simplified/snapshot.cpp +++ b/mbgl/simplified/snapshot.cpp @@ -13,7 +13,6 @@ snapshot_Result Snapshot(snapshot_Params params) { - mbgl::util::RunLoop loop; snapshot_Result result; result.DidError = 0; @@ -23,11 +22,10 @@ snapshot_Result Snapshot(snapshot_Params params) { mbgl::ThreadPool threadPool(4); mbgl::DefaultFileSource fileSource(params.cache_file, params.asset_root); - mbgl::HeadlessFrontend frontend({ params.height, params.width }, float(params.ppi_ratio), fileSource, threadPool); + mbgl::HeadlessFrontend frontend({ params.width, params.height }, float(params.ppi_ratio), fileSource, threadPool); mbgl::Map map(frontend, mbgl::MapObserver::nullObserver(), frontend.getSize(), params.ppi_ratio, fileSource, threadPool, mbgl::MapMode::Static); - //map.getStyle().loadURL("file://style.json"); map.getStyle().loadURL(params.style); map.setLatLngZoom({ params.lat, params.lng }, params.zoom); map.setBearing(params.bearing); diff --git a/mbgl/simplified/snapshot.go b/mbgl/simplified/snapshot.go index d92017f..cdc16d6 100644 --- a/mbgl/simplified/snapshot.go +++ b/mbgl/simplified/snapshot.go @@ -1,10 +1,13 @@ package simplified import ( + "context" "errors" "log" "runtime" "strings" + "sync" + "time" "unsafe" ) @@ -13,7 +16,7 @@ import ( #include "snapshot.h" -snapshot_Params NewSnapshotParams( char* style, char* cacheFile, char* assetRoot, uint32_t width, uint32_t height, int ppi_ratio, double lat, double lng, double zoom, double pitch, double bearing) { +snapshot_Params NewSnapshotParams( char* style, char* cacheFile, char* assetRoot, uint32_t width, uint32_t height, double ppi_ratio, double lat, double lng, double zoom, double pitch, double bearing) { snapshot_Params params; params.style = style; @@ -32,6 +35,8 @@ snapshot_Params NewSnapshotParams( char* style, char* cacheFile, char* assetRoot */ import "C" +var ErrManagerExiting = errors.New("Manager shutting down.") + type Snapshotter struct { Style string CacheFile string @@ -71,7 +76,7 @@ func (snap Snapshotter) AsParams() (p C.snapshot_Params, err error) { if zoom <= 0 { zoom = 0 } - ppiratio = snap.PPIRatio + ppiratio := snap.PPIRatio if ppiratio == 0.0 { ppiratio = 1.0 } @@ -91,10 +96,7 @@ func (snap Snapshotter) AsParams() (p C.snapshot_Params, err error) { return img, nil } -func Snapshot(snap Snapshotter) (img Image, err error) { - - runtime.LockOSThread() - defer runtime.UnlockOSThread() +func snapshot(snap Snapshotter) (img Image, err error) { _params, err := snap.AsParams() if err != nil { @@ -109,8 +111,106 @@ func Snapshot(snap Snapshotter) (img Image, err error) { img.Width, img.Height = int(result.Image.Width), int(result.Image.Height) bytes := img.Width * img.Height * 4 - log.Printf("Width %v : Height %v : Bytes %v", img.Width, img.Height, bytes) img.Data = C.GoBytes(unsafe.Pointer(result.Image.Data), C.int(bytes)) return img, nil } + +type snapJobReply struct { + image Image + err error +} + +type snapJob struct { + Params Snapshotter + Reply chan<- snapJobReply +} + +func snapshotWorker(job snapJob) { + img, err := snapshot(job.Params) + job.Reply <- snapJobReply{ + image: img, + err: err, + } +} + +var rwManagerRunning sync.RWMutex +var isManagerRunning bool +var workQueue chan snapJob + +func IsManagerRunning() (b bool) { + rwManagerRunning.RLock() + defer rwManagerRunning.RUnlock() + return isManagerRunning +} + +// StartSnapshotManager will block till the Manager has started. +func StartSnapshotManager(ctx context.Context) { + go SnapshotManager(ctx) + for { + <-time.After(10 * time.Millisecond) + if IsManagerRunning() { + return + } + } +} + +func SnapshotManager(ctx context.Context) { + if IsManagerRunning() { + return + } + + log.Println("Starting up manager.") + runtime.LockOSThread() + defer runtime.UnlockOSThread() + NewRunLoop() + defer DestroyRunLoop() + workQueue = make(chan snapJob) + + rwManagerRunning.Lock() + isManagerRunning = true + rwManagerRunning.Unlock() + defer func() { + rwManagerRunning.Lock() + isManagerRunning = false + rwManagerRunning.Unlock() + }() + + for { + select { + case job := <-workQueue: + snapshotWorker(job) + case <-ctx.Done(): + // We are exiting... + rwManagerRunning.Lock() + isManagerRunning = false + rwManagerRunning.Unlock() + break + } + } + for j := range workQueue { + // We are shutdowning, let's empty the queue. + j.Reply <- snapJobReply{ + err: ErrManagerExiting, + } + } +} + +func Snapshot1(snap Snapshotter) (img Image, err error) { + if !IsManagerRunning() { + if workQueue != nil { + close(workQueue) + } + return img, ErrManagerExiting + } + var reply = make(chan snapJobReply) + + workQueue <- snapJob{ + Params: snap, + Reply: reply, + } + + r := <-reply + close(reply) + return r.image, r.err +}