diff --git a/main.go b/main.go index d15c2ed..a922aad 100644 --- a/main.go +++ b/main.go @@ -32,9 +32,11 @@ var cli struct { } `cmd:"" help:"Convert an MBTiles or older spec version to PMTiles."` Show struct { - Path string `arg:""` - Bucket string `help:"Remote bucket"` - Metadata bool `help:"Print only the JSON metadata."` + Path string `arg:""` + Bucket string `help:"Remote bucket"` + Metadata bool `help:"Print only the JSON metadata."` + Tilejson bool `help:"Print the TileJSON."` + PublicUrl string `help:"Public URL of tile endpoint to use in the Tilejson output e.g. https://example.com/tiles/pmtiles/{z}/{x}/{y}"` } `cmd:"" help:"Inspect a local or remote archive."` Tile struct { @@ -113,12 +115,12 @@ func main() { switch ctx.Command() { case "show ": - err := pmtiles.Show(logger, cli.Show.Bucket, cli.Show.Path, cli.Show.Metadata, false, 0, 0, 0) + err := pmtiles.Show(logger, cli.Show.Bucket, cli.Show.Path, cli.Show.Metadata, cli.Show.Tilejson, cli.Show.PublicUrl, false, 0, 0, 0) if err != nil { logger.Fatalf("Failed to show archive, %v", err) } case "tile ": - err := pmtiles.Show(logger, cli.Tile.Bucket, cli.Tile.Path, false, true, cli.Tile.Z, cli.Tile.X, cli.Tile.Y) + err := pmtiles.Show(logger, cli.Tile.Bucket, cli.Tile.Path, false, false, "", true, cli.Tile.Z, cli.Tile.X, cli.Tile.Y) if err != nil { logger.Fatalf("Failed to show tile, %v", err) } diff --git a/pmtiles/server.go b/pmtiles/server.go index 5e01867..52f049e 100644 --- a/pmtiles/server.go +++ b/pmtiles/server.go @@ -234,29 +234,16 @@ func (server *Server) get_tilejson(ctx context.Context, http_headers map[string] var metadata_map map[string]interface{} json.Unmarshal(metadata_bytes, &metadata_map) - tilejson := make(map[string]interface{}) - if server.publicHostname == "" { return 501, http_headers, []byte("PUBLIC_HOSTNAME must be set for TileJSON") } + tilejson_bytes, err := CreateTilejson(header, metadata_bytes, server.publicHostname+"/"+name+"/{z}/{x}/{y}"+headerExt(header)) + if err != nil { + return 500, http_headers, []byte("Error generating tilejson") + } + http_headers["Content-Type"] = "application/json" - tilejson["tilejson"] = "3.0.0" - tilejson["scheme"] = "xyz" - tilejson["tiles"] = []string{server.publicHostname + "/" + name + "/{z}/{x}/{y}" + headerExt(header)} - tilejson["vector_layers"] = metadata_map["vector_layers"] - tilejson["attribution"] = metadata_map["attribution"] - tilejson["description"] = metadata_map["description"] - tilejson["name"] = metadata_map["name"] - tilejson["version"] = metadata_map["version"] - - E7 := 10000000.0 - tilejson["bounds"] = []float64{float64(header.MinLonE7) / E7, float64(header.MinLatE7) / E7, float64(header.MaxLonE7) / E7, float64(header.MaxLatE7) / E7} - tilejson["center"] = []interface{}{float64(header.CenterLonE7) / E7, float64(header.CenterLatE7) / E7, header.CenterZoom} - tilejson["minzoom"] = header.MinZoom - tilejson["maxzoom"] = header.MaxZoom - - tilejson_bytes, err := json.Marshal(tilejson) return 200, http_headers, tilejson_bytes } diff --git a/pmtiles/show.go b/pmtiles/show.go index 2617ca8..8338c0f 100644 --- a/pmtiles/show.go +++ b/pmtiles/show.go @@ -6,13 +6,14 @@ import ( "context" "encoding/json" "fmt" + // "github.com/dustin/go-humanize" "io" "log" "os" ) -func Show(logger *log.Logger, bucketURL string, key string, show_metadata_only bool, show_tile bool, z int, x int, y int) error { +func Show(logger *log.Logger, bucketURL string, key string, show_metadata_only bool, show_tilejson bool, public_url string, show_tile bool, z int, x int, y int) error { ctx := context.Background() bucketURL, key, err := NormalizeBucketKey(bucketURL, "", key) @@ -87,8 +88,25 @@ func Show(logger *log.Logger, bucketURL string, key string, show_metadata_only b } metadata_reader.Close() + if show_metadata_only && show_tilejson { + return fmt.Errorf("Cannot use --metadata and --tilejson together.") + } + if show_metadata_only { fmt.Print(string(metadata_bytes)) + } else if show_tilejson { + if public_url == "" { + // Using Fprintf instead of logger here, as this message should be written to Stderr in case + // Stdout is being redirected. + fmt.Fprintln(os.Stderr, "Warning: No --public-url specified; using placeholder tiles URL.") + public_url = "https://example.com/{z}/{x}/{y}.mvt" + + } + tilejson_bytes, err := CreateTilejson(header, metadata_bytes, public_url) + if err != nil { + return fmt.Errorf("Failed to create tilejson for %s, %w", key, err) + } + fmt.Print(string(tilejson_bytes)) } else { fmt.Printf("pmtiles spec version: %d\n", header.SpecVersion) // fmt.Printf("total size: %s\n", humanize.Bytes(uint64(r.Size()))) diff --git a/pmtiles/tilejson.go b/pmtiles/tilejson.go new file mode 100644 index 0000000..45aa8b8 --- /dev/null +++ b/pmtiles/tilejson.go @@ -0,0 +1,29 @@ +package pmtiles + +import ( + "encoding/json" +) + +func CreateTilejson(header HeaderV3, metadata_bytes []byte, tileUrl string) ([]byte, error) { + var metadata_map map[string]interface{} + json.Unmarshal(metadata_bytes, &metadata_map) + + tilejson := make(map[string]interface{}) + + tilejson["tilejson"] = "3.0.0" + tilejson["scheme"] = "xyz" + tilejson["tiles"] = []string{tileUrl} + tilejson["vector_layers"] = metadata_map["vector_layers"] + tilejson["attribution"] = metadata_map["attribution"] + tilejson["description"] = metadata_map["description"] + tilejson["name"] = metadata_map["name"] + tilejson["version"] = metadata_map["version"] + + E7 := 10000000.0 + tilejson["bounds"] = []float64{float64(header.MinLonE7) / E7, float64(header.MinLatE7) / E7, float64(header.MaxLonE7) / E7, float64(header.MaxLatE7) / E7} + tilejson["center"] = []interface{}{float64(header.CenterLonE7) / E7, float64(header.CenterLatE7) / E7, header.CenterZoom} + tilejson["minzoom"] = header.MinZoom + tilejson["maxzoom"] = header.MaxZoom + + return json.MarshalIndent(tilejson, "", "\t") +} diff --git a/pmtiles/tilejson_test.go b/pmtiles/tilejson_test.go new file mode 100644 index 0000000..2af533e --- /dev/null +++ b/pmtiles/tilejson_test.go @@ -0,0 +1,60 @@ +package pmtiles + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCreateTilejson(t *testing.T) { + // Define test inputs + header := HeaderV3{ + MinZoom: 0.0, + MaxZoom: 14.0, + MinLonE7: -1144000000, + MinLatE7: 479000000, + MaxLonE7: -1139000000, + MaxLatE7: 483000000, + CenterLonE7: -1141500000, + CenterLatE7: 481000000, + } + metadataBytes := []byte(` + { + "vector_layers": [{"id": "layer1"}], + "attribution": "Attribution", + "description": "Description", + "name": "Name", + "version": "1.0" + }`) + tileURL := "https://example.com/tiles.pmtiles/{z}/{x}/{y}" + + // Call the function + tilejsonBytes, err := CreateTilejson(header, metadataBytes, tileURL) + + // Check for errors + if err != nil { + t.Errorf("CreateTilejson returned an error: %v", err) + } + + // Parse the tilejsonBytes to check the output + var tilejson map[string]interface{} + err = json.Unmarshal(tilejsonBytes, &tilejson) + if err != nil { + t.Errorf("Failed to parse the generated TileJSON: %v", err) + } + + assert.Equal(t, "3.0.0", tilejson["tilejson"]) + assert.Equal(t, "xyz", tilejson["scheme"]) + assert.Equal(t, []interface{}{"https://example.com/tiles.pmtiles/{z}/{x}/{y}"}, tilejson["tiles"]) + assert.Equal(t, []interface{}{map[string]interface{}{"id": "layer1"}}, tilejson["vector_layers"]) + assert.Equal(t, "Attribution", tilejson["attribution"]) + assert.Equal(t, "Description", tilejson["description"]) + assert.Equal(t, "Name", tilejson["name"]) + assert.Equal(t, "1.0", tilejson["version"]) + + assert.Equal(t, []interface{}{-114.400000, 47.900000, -113.900000, 48.300000}, tilejson["bounds"]) + assert.Equal(t, []interface{}{-114.150000, 48.100000, 0.0}, tilejson["center"]) + assert.Equal(t, 0.0, tilejson["minzoom"]) + assert.Equal(t, 14.0, tilejson["maxzoom"]) +}