diff --git a/main.go b/main.go index bff9e06..e1ee0bb 100644 --- a/main.go +++ b/main.go @@ -36,11 +36,12 @@ 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."` - Tilejson bool `help:"Print the TileJSON."` - PublicURL string `help:"Public base URL of tile endpoint for TileJSON e.g. https://example.com/tiles"` + Path string `arg:""` + Bucket string `help:"Remote bucket"` + Metadata bool `help:"Print only the JSON metadata."` + HeaderJson bool `help:Print a JSON representation of the header information.` + Tilejson bool `help:"Print the TileJSON."` + PublicURL string `help:"Public base URL of tile endpoint for TileJSON e.g. https://example.com/tiles"` } `cmd:"" help:"Inspect a local or remote archive."` Tile struct { @@ -51,6 +52,17 @@ var cli struct { Bucket string `help:"Remote bucket"` } `cmd:"" help:"Fetch one tile from a local or remote archive and output on stdout."` + WriteHeader struct { + Input string `arg:"" help:"Input archive file." type:"existingfile"` + HeaderJsonFile string `arg:"" help:"Input header JSON (written by show --header-json)." type:"existingfile"` + } `cmd:"" help:"Write header data to an existing archive in-place."` + + WriteMetadata struct { + Input string `arg:"" help:"Input archive file." type:"existingfile"` + MetadataFile string `arg:"" help:"Input metadata JSON." type:"existingfile"` + Tmpdir string `help:"An optional path to a folder for tmp data." type:"existingdir"` + } `cmd:"" help:"Write JSON metadata to an existing archive in-place."` + Extract struct { Input string `arg:"" help:"Input local or remote archive."` Output string `arg:"" help:"Output archive." type:"path"` @@ -122,12 +134,12 @@ func main() { switch ctx.Command() { case "show ": - err := pmtiles.Show(logger, cli.Show.Bucket, cli.Show.Path, cli.Show.Metadata, cli.Show.Tilejson, cli.Show.PublicURL, false, 0, 0, 0) + err := pmtiles.Show(logger, cli.Show.Bucket, cli.Show.Path, cli.Show.HeaderJson, 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, false, "", true, cli.Tile.Z, cli.Tile.X, cli.Tile.Y) + err := pmtiles.Show(logger, cli.Tile.Bucket, cli.Tile.Path, false, 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/directory.go b/pmtiles/directory.go index 5820810..00bc6ef 100644 --- a/pmtiles/directory.go +++ b/pmtiles/directory.go @@ -5,6 +5,7 @@ import ( "bytes" "compress/gzip" "encoding/binary" + "encoding/json" "fmt" ) @@ -63,6 +64,23 @@ type HeaderV3 struct { CenterLatE7 int32 } +// HeaderJson is a human-readable representation of parts of the binary header +// that may need to be manually edited. +// Omitted parts are the responsibility of the generated program and not editable. +type HeaderJson struct { + TileCompression string + TileType string + MinZoom int + MaxZoom int + MinLon float64 + MinLat float64 + MaxLon float64 + MaxLat float64 + CenterZoom int + CenterLon float64 + CenterLat float64 +} + func headerContentType(header HeaderV3) (string, bool) { switch header.TileType { case Mvt: @@ -80,23 +98,31 @@ func headerContentType(header HeaderV3) (string, bool) { } } -func headerExt(header HeaderV3) string { - switch header.TileType { +func stringifiedTileType(t TileType) string { + switch t { case Mvt: - return ".mvt" + return "mvt" case Png: - return ".png" + return "png" case Jpeg: - return ".jpg" + return "jpg" case Webp: - return ".webp" + return "webp" case Avif: - return ".avif" + return "avif" default: return "" } } +func headerExt(header HeaderV3) string { + base := stringifiedTileType(header.TileType) + if base == "" { + return "" + } + return "." + base +} + func headerContentEncoding(compression Compression) (string, bool) { switch compression { case Gzip: @@ -108,6 +134,28 @@ func headerContentEncoding(compression Compression) (string, bool) { } } +func headerToJson(header HeaderV3) HeaderJson { + compressionString, _ := headerContentEncoding(header.TileCompression) + return HeaderJson{ + TileCompression: compressionString, + TileType: stringifiedTileType(header.TileType), + MinZoom: int(header.MinZoom), + MaxZoom: int(header.MaxZoom), + MinLon: float64(header.MinLonE7) / 10000000, + MinLat: float64(header.MinLatE7) / 10000000, + MaxLon: float64(header.MaxLonE7) / 10000000, + MaxLat: float64(header.MaxLatE7) / 10000000, + CenterZoom: int(header.CenterZoom), + CenterLon: float64(header.CenterLonE7) / 10000000, + CenterLat: float64(header.CenterLatE7) / 10000000, + } +} + +func headerToStringifiedJson(header HeaderV3) string { + s, _ := json.Marshal(headerToJson(header)) + return string(s) +} + // EntryV3 is an entry in a PMTiles spec version 3 directory. type EntryV3 struct { TileID uint64 diff --git a/pmtiles/directory_test.go b/pmtiles/directory_test.go index a5a8399..01f9f59 100644 --- a/pmtiles/directory_test.go +++ b/pmtiles/directory_test.go @@ -84,6 +84,33 @@ func TestHeaderRoundtrip(t *testing.T) { assert.Equal(t, int32(32000000), result.CenterLatE7) } +func TestHeaderJsonRoundtrip(t *testing.T) { + header := HeaderV3{} + header.TileCompression = Brotli + header.TileType = Mvt + header.MinZoom = 1 + header.MaxZoom = 3 + header.MinLonE7 = 1.1 * 10000000 + header.MinLatE7 = 2.1 * 10000000 + header.MaxLonE7 = 1.2 * 10000000 + header.MaxLatE7 = 2.2 * 10000000 + header.CenterZoom = 2 + header.CenterLonE7 = 3.1 * 10000000 + header.CenterLatE7 = 3.2 * 10000000 + j := headerToJson(header) + assert.Equal(t, "br", j.TileCompression) + assert.Equal(t, "mvt", j.TileType) + assert.Equal(t, 1, j.MinZoom) + assert.Equal(t, 3, j.MaxZoom) + assert.Equal(t, 2, j.CenterZoom) + assert.Equal(t, 1.1, j.MinLon) + assert.Equal(t, 2.1, j.MinLat) + assert.Equal(t, 1.2, j.MaxLon) + assert.Equal(t, 2.2, j.MaxLat) + assert.Equal(t, 3.1, j.CenterLon) + assert.Equal(t, 3.2, j.CenterLat) +} + func TestOptimizeDirectories(t *testing.T) { rand.Seed(3857) entries := make([]EntryV3, 0) @@ -171,3 +198,16 @@ func TestBuildRootsLeaves(t *testing.T) { _, _, numLeaves := buildRootsLeaves(entries, 1) assert.Equal(t, 1, numLeaves) } + +func TestStringifiedCompression(t *testing.T) { + +} + +func TestStringifiedExtension(t *testing.T) { + assert.Equal(t, "", headerExt(HeaderV3{})) + assert.Equal(t, ".mvt", headerExt(HeaderV3{TileType: Mvt})) + assert.Equal(t, ".png", headerExt(HeaderV3{TileType: Png})) + assert.Equal(t, ".jpg", headerExt(HeaderV3{TileType: Jpeg})) + assert.Equal(t, ".webp", headerExt(HeaderV3{TileType: Webp})) + assert.Equal(t, ".avif", headerExt(HeaderV3{TileType: Avif})) +} diff --git a/pmtiles/show.go b/pmtiles/show.go index dc6e3a9..e93f583 100644 --- a/pmtiles/show.go +++ b/pmtiles/show.go @@ -14,7 +14,7 @@ import ( ) // Show prints detailed information about an archive. -func Show(_ *log.Logger, bucketURL string, key string, showMetadataOnly bool, showTilejson bool, publicURL string, showTile bool, z int, x int, y int) error { +func Show(_ *log.Logger, bucketURL string, key string, showHeaderJsonOnly bool, showMetadataOnly bool, showTilejson bool, publicURL string, showTile bool, z int, x int, y int) error { ctx := context.Background() bucketURL, key, err := NormalizeBucketKey(bucketURL, "", key) @@ -90,11 +90,13 @@ func Show(_ *log.Logger, bucketURL string, key string, showMetadataOnly bool, sh metadataReader.Close() if showMetadataOnly && showTilejson { - return fmt.Errorf("cannot use --metadata and --tilejson together") + return fmt.Errorf("cannot use more than one of --header-json, --metadata, and --tilejson together") } - if showMetadataOnly { - fmt.Print(string(metadataBytes)) + if showHeaderJsonOnly { + fmt.Println(headerToStringifiedJson(header)) + } else if showMetadataOnly { + fmt.Println(string(metadataBytes)) } else if showTilejson { if publicURL == "" { // Using Fprintf instead of logger here, as this message should be written to Stderr in case