diff --git a/main.go b/main.go index 0710f52..5338b4a 100644 --- a/main.go +++ b/main.go @@ -32,7 +32,7 @@ var cli struct { Output string `arg:"" help:"Output PMTiles archive." type:"path"` Force bool `help:"Force removal."` NoDeduplication bool `help:"Don't attempt to deduplicate tiles."` - Tmpdir string `help:"An optional path to a folder for tmp data." type:"existingdir"` + Tmpdir string `help:"An optional path to a folder for temporary files." type:"existingdir"` } `cmd:"" help:"Convert an MBTiles or older spec version to PMTiles."` Show struct { @@ -52,11 +52,11 @@ var cli struct { Bucket string `help:"Remote bucket"` } `cmd:"" help:"Fetch one tile from a local or remote archive and output on stdout."` - Write struct { + Edit struct { Input string `arg:"" help:"Input archive file." type:"existingfile"` HeaderJson string `help:"Input header JSON file (written by show --header-json)." type:"existingfile"` Metadata string `help:"Input metadata JSON (written by show --metadata)." type:"existingfile"` - } `cmd:"" help:"Write header data or metadata to an existing archive." hidden:""` + } `cmd:"" help:"Edit JSON metadata or parts of the header in-place." hidden:""` Extract struct { Input string `arg:"" help:"Input local or remote archive."` @@ -211,7 +211,7 @@ func main() { if err != nil { logger.Fatalf("Failed to convert %s, %v", path, err) } - case "upload ": + case "upload ": err := pmtiles.Upload(logger, cli.Upload.InputPmtiles, cli.Upload.Bucket, cli.Upload.RemotePmtiles, cli.Upload.MaxConcurrency) if err != nil { @@ -222,6 +222,11 @@ func main() { if err != nil { logger.Fatalf("Failed to verify archive, %v", err) } + case "edit ": + err := pmtiles.Edit(logger, cli.Edit.Input, cli.Edit.HeaderJson, cli.Edit.Metadata) + if err != nil { + logger.Fatalf("Failed to edit archive, %v", err) + } case "makesync ": err := pmtiles.Makesync(logger, version, cli.Makesync.Input, cli.Makesync.BlockSizeKb, cli.Makesync.Checksum) if err != nil { diff --git a/pmtiles/directory.go b/pmtiles/directory.go index 26cf286..9daf734 100644 --- a/pmtiles/directory.go +++ b/pmtiles/directory.go @@ -67,18 +67,14 @@ type HeaderV3 struct { // 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 generator program and not editable. +// The formatting is aligned with the TileJSON / MBTiles specification. 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 + TileCompression string `json:"tile_compression"` + TileType string `json:"tile_type"` + MinZoom int `json:"minzoom"` + MaxZoom int `json:"maxzoom"` + Bounds []float64 `json:"bounds"` + Center []float64 `json:"center"` } func headerContentType(header HeaderV3) (string, bool) { @@ -98,7 +94,7 @@ func headerContentType(header HeaderV3) (string, bool) { } } -func stringifiedTileType(t TileType) string { +func tileTypeToString(t TileType) string { switch t { case Mvt: return "mvt" @@ -115,39 +111,70 @@ func stringifiedTileType(t TileType) string { } } +func stringToTileType(t string) TileType { + switch t { + case "mvt": + return Mvt + case "png": + return Png + case "jpg": + return Jpeg + case "webp": + return Webp + case "avif": + return Avif + default: + return UnknownTileType + } +} + func headerExt(header HeaderV3) string { - base := stringifiedTileType(header.TileType) + base := tileTypeToString(header.TileType) if base == "" { return "" } return "." + base } -func headerContentEncoding(compression Compression) (string, bool) { +func compressionToString(compression Compression) (string, bool) { switch compression { + case NoCompression: + return "none", false case Gzip: return "gzip", true case Brotli: return "br", true + case Zstd: + return "zstd", true default: - return "", false + return "unknown", false + } +} + +func stringToCompression(s string) Compression { + switch s { + case "none": + return NoCompression + case "gzip": + return Gzip + case "br": + return Brotli + case "zstd": + return Zstd + default: + return UnknownCompression } } func headerToJson(header HeaderV3) HeaderJson { - compressionString, _ := headerContentEncoding(header.TileCompression) + compressionString, _ := compressionToString(header.TileCompression) return HeaderJson{ TileCompression: compressionString, - TileType: stringifiedTileType(header.TileType), + TileType: tileTypeToString(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, + Bounds: []float64{float64(header.MinLonE7) / 10000000, float64(header.MinLatE7) / 10000000, float64(header.MaxLonE7) / 10000000, float64(header.MaxLatE7) / 10000000}, + Center: []float64{float64(header.CenterLonE7) / 10000000, float64(header.CenterLatE7) / 10000000, float64(header.CenterZoom)}, } } diff --git a/pmtiles/directory_test.go b/pmtiles/directory_test.go index 21b06ee..d42598a 100644 --- a/pmtiles/directory_test.go +++ b/pmtiles/directory_test.go @@ -102,13 +102,8 @@ func TestHeaderJsonRoundtrip(t *testing.T) { 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) + assert.Equal(t, []float64{1.1, 2.1, 1.2, 2.2}, j.Bounds) + assert.Equal(t, []float64{3.1, 3.2, 2}, j.Center) } func TestOptimizeDirectories(t *testing.T) { @@ -207,3 +202,30 @@ func TestStringifiedExtension(t *testing.T) { assert.Equal(t, ".webp", headerExt(HeaderV3{TileType: Webp})) assert.Equal(t, ".avif", headerExt(HeaderV3{TileType: Avif})) } + +func TestStringToTileType(t *testing.T) { + assert.Equal(t, "mvt", tileTypeToString(stringToTileType("mvt"))) + assert.Equal(t, "png", tileTypeToString(stringToTileType("png"))) + assert.Equal(t, "jpg", tileTypeToString(stringToTileType("jpg"))) + assert.Equal(t, "webp", tileTypeToString(stringToTileType("webp"))) + assert.Equal(t, "avif", tileTypeToString(stringToTileType("avif"))) + assert.Equal(t, "", tileTypeToString(stringToTileType(""))) +} + +func TestStringToCompression(t *testing.T) { + s, has := compressionToString(stringToCompression("gzip")) + assert.True(t, has) + assert.Equal(t, "gzip", s) + s, has = compressionToString(stringToCompression("br")) + assert.True(t, has) + assert.Equal(t, "br", s) + s, has = compressionToString(stringToCompression("zstd")) + assert.True(t, has) + assert.Equal(t, "zstd", s) + s, has = compressionToString(stringToCompression("none")) + assert.False(t, has) + assert.Equal(t, "none", s) + s, has = compressionToString(stringToCompression("unknown")) + assert.False(t, has) + assert.Equal(t, "unknown", s) +} diff --git a/pmtiles/edit.go b/pmtiles/edit.go new file mode 100644 index 0000000..d4c5b82 --- /dev/null +++ b/pmtiles/edit.go @@ -0,0 +1,152 @@ +package pmtiles + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "github.com/schollz/progressbar/v3" + "io" + "io/ioutil" + "log" + "os" +) + +// Edit parts of the header or metadata. +// works in-place if only the header is modified. +func Edit(_ *log.Logger, inputArchive string, newHeaderJSONFile string, newMetadataFile string) error { + if newHeaderJSONFile == "" && newMetadataFile == "" { + return fmt.Errorf("must supply --header-json and/or --metadata to edit") + } + + file, err := os.OpenFile(inputArchive, os.O_RDWR, 0666) + defer file.Close() + if err != nil { + return err + } + + buf := make([]byte, 127) + _, err = file.Read(buf) + if err != nil { + return err + } + oldHeader, err := deserializeHeader(buf) + if err != nil { + return err + } + + newHeader := oldHeader + + if newHeaderJSONFile != "" { + newHeaderData := HeaderJson{} + data, err := ioutil.ReadFile(newHeaderJSONFile) + if err != nil { + return err + } + err = json.Unmarshal(data, &newHeaderData) + if err != nil { + return err + } + + if len(newHeaderData.Bounds) != 4 { + return fmt.Errorf("header len(bounds) must == 4") + } + if len(newHeaderData.Center) != 3 { + return fmt.Errorf("header len(center) must == 3") + } + + newHeader.TileType = stringToTileType(newHeaderData.TileType) + newHeader.TileCompression = stringToCompression(newHeaderData.TileCompression) + newHeader.MinZoom = uint8(newHeaderData.MinZoom) + newHeader.MaxZoom = uint8(newHeaderData.MaxZoom) + newHeader.MinLonE7 = int32(newHeaderData.Bounds[0] * 10000000) + newHeader.MinLatE7 = int32(newHeaderData.Bounds[1] * 10000000) + newHeader.MaxLonE7 = int32(newHeaderData.Bounds[2] * 10000000) + newHeader.MaxLatE7 = int32(newHeaderData.Bounds[3] * 10000000) + newHeader.CenterLonE7 = int32(newHeaderData.Center[0] * 10000000) + newHeader.CenterLatE7 = int32(newHeaderData.Center[1] * 10000000) + newHeader.CenterZoom = uint8(newHeaderData.Center[2]) + } + + if newMetadataFile == "" { + buf = serializeHeader(newHeader) + _, err = file.WriteAt(buf, 0) + if err != nil { + return err + } + file.Close() + return nil + } + + newMetadataUncompressed, err := ioutil.ReadFile(newMetadataFile) + + var parsedMetadata map[string]interface{} + if err := json.Unmarshal(newMetadataUncompressed, &parsedMetadata); err != nil { + return err + } + + var metadataBytes bytes.Buffer + if oldHeader.InternalCompression != Gzip { + return fmt.Errorf("only gzip internal compression is currently supported") + } + + w, _ := gzip.NewWriterLevel(&metadataBytes, gzip.BestCompression) + w.Write(newMetadataUncompressed) + w.Close() + + if err != nil { + return err + } + + tempFilePath := inputArchive + ".tmp" + + if _, err = os.Stat(tempFilePath); err == nil { + return fmt.Errorf("A file with the same name already exists") + } + + outfile, err := os.Create(tempFilePath) + if err != nil { + return err + } + defer outfile.Close() + + newHeader.MetadataOffset = newHeader.RootOffset + newHeader.RootLength + newHeader.MetadataLength = uint64(len(metadataBytes.Bytes())) + newHeader.LeafDirectoryOffset = newHeader.MetadataOffset + newHeader.MetadataLength + newHeader.TileDataOffset = newHeader.LeafDirectoryOffset + newHeader.LeafDirectoryLength + + bar := progressbar.DefaultBytes( + int64(HeaderV3LenBytes+newHeader.RootLength+uint64(len(metadataBytes.Bytes()))+newHeader.LeafDirectoryLength+newHeader.TileDataLength), + "writing file", + ) + + buf = serializeHeader(newHeader) + io.Copy(io.MultiWriter(outfile, bar), bytes.NewReader(buf)) + + rootSection := io.NewSectionReader(file, int64(oldHeader.RootOffset), int64(oldHeader.RootLength)) + if _, err := io.Copy(io.MultiWriter(outfile, bar), rootSection); err != nil { + return err + } + + if _, err := io.Copy(io.MultiWriter(outfile, bar), bytes.NewReader(metadataBytes.Bytes())); err != nil { + return err + } + + leafSection := io.NewSectionReader(file, int64(oldHeader.LeafDirectoryOffset), int64(oldHeader.LeafDirectoryLength)) + if _, err := io.Copy(io.MultiWriter(outfile, bar), leafSection); err != nil { + return err + } + + tileSection := io.NewSectionReader(file, int64(oldHeader.TileDataOffset), int64(oldHeader.TileDataLength)) + if _, err := io.Copy(io.MultiWriter(outfile, bar), tileSection); err != nil { + return err + } + + // explicitly close in order to rename + file.Close() + outfile.Close() + if err := os.Rename(tempFilePath, inputArchive); err != nil { + return err + } + return nil +} diff --git a/pmtiles/edit_test.go b/pmtiles/edit_test.go new file mode 100644 index 0000000..be43970 --- /dev/null +++ b/pmtiles/edit_test.go @@ -0,0 +1,150 @@ +package pmtiles + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/stretchr/testify/assert" + "io" + "log" + "os" + "path/filepath" + "testing" +) + +var logger = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) + +func makeFixtureCopy(t *testing.T, name string) string { + src, _ := os.OpenFile("fixtures/test_fixture_1.pmtiles", os.O_RDONLY, 0666) + defer src.Close() + fname := filepath.Join(t.TempDir(), name+".pmtiles") + dest, _ := os.Create(fname) + _, _ = io.Copy(dest, src) + dest.Close() + return fname +} + +func TestEditHeader(t *testing.T) { + fileToEdit := makeFixtureCopy(t, "edit_header") + + headerPath := filepath.Join(t.TempDir(), "header.json") + headerFile, _ := os.Create(headerPath) + fmt.Fprint(headerFile, `{"tile_type":"png","tile_compression":"br","bounds":[-1,1,-1,1],"center":[0,0,0]}`) + headerFile.Close() + + err := Edit(logger, fileToEdit, headerPath, "") + assert.Nil(t, err) + + var b bytes.Buffer + err = Show(logger, &b, "", fileToEdit, true, false, false, "", false, 0, 0, 0) + assert.Nil(t, err) + + var input map[string]interface{} + json.Unmarshal(b.Bytes(), &input) + assert.Equal(t, "png", input["tile_type"]) + assert.Equal(t, "br", input["tile_compression"]) +} + +func TestEditMetadata(t *testing.T) { + fileToEdit := makeFixtureCopy(t, "edit_metadata") + + metadataPath := filepath.Join(t.TempDir(), "metadata.json") + metadataFile, _ := os.Create(metadataPath) + fmt.Fprint(metadataFile, `{"foo":"bar"}`) + metadataFile.Close() + + err := Edit(logger, fileToEdit, "", metadataPath) + assert.Nil(t, err) + + var b bytes.Buffer + err = Show(logger, &b, "", fileToEdit, false, true, false, "", false, 0, 0, 0) + assert.Nil(t, err) + + var input map[string]interface{} + json.Unmarshal(b.Bytes(), &input) + assert.Equal(t, "bar", input["foo"]) +} + +func TestMalformedHeader(t *testing.T) { + fileToEdit := makeFixtureCopy(t, "edit_header_malformed") + + headerPath := filepath.Join(t.TempDir(), "header_malformed.json") + headerFile, _ := os.Create(headerPath) + fmt.Fprint(headerFile, `{`) + headerFile.Close() + + err := Edit(logger, fileToEdit, headerPath, "") + assert.Error(t, err) +} + +func TestMalformedHeaderBounds(t *testing.T) { + fileToEdit := makeFixtureCopy(t, "edit_header_bad_bounds") + + headerPath := filepath.Join(t.TempDir(), "header_bad_bounds.json") + headerFile, _ := os.Create(headerPath) + fmt.Fprint(headerFile, `{"tile_type":"png","tile_compression":"br","bounds":[],"center":[0,0,0]}`) + headerFile.Close() + + err := Edit(logger, fileToEdit, headerPath, "") + assert.Error(t, err) +} + +func TestHeaderUnknownEnum(t *testing.T) { + fileToEdit := makeFixtureCopy(t, "edit_header_unknown") + + headerPath := filepath.Join(t.TempDir(), "header_unknown.json") + headerFile, _ := os.Create(headerPath) + fmt.Fprint(headerFile, `{"tile_type":"foo","tile_compression":"foo","bounds":[-1,1,-1,1],"center":[0,0,0]}`) + headerFile.Close() + + err := Edit(logger, fileToEdit, headerPath, "") + assert.Nil(t, err) + + var b bytes.Buffer + err = Show(logger, &b, "", fileToEdit, true, false, false, "", false, 0, 0, 0) + assert.Nil(t, err) + + var input map[string]interface{} + json.Unmarshal(b.Bytes(), &input) + assert.Equal(t, "", input["tile_type"]) + assert.Equal(t, "unknown", input["tile_compression"]) +} + +func TestMalformedHeaderCenter(t *testing.T) { + fileToEdit := makeFixtureCopy(t, "edit_header_bad_center") + + headerPath := filepath.Join(t.TempDir(), "header_bad_center.json") + headerFile, _ := os.Create(headerPath) + fmt.Fprint(headerFile, `{"tile_type":"png","tile_compression":"br","bounds":[-1,1,-1,1],"center":[0,0]}`) + headerFile.Close() + + err := Edit(logger, fileToEdit, headerPath, "") + assert.Error(t, err) +} + +func TestMalformedMetadata(t *testing.T) { + fileToEdit := makeFixtureCopy(t, "edit_metadata_malformed") + + metadataPath := filepath.Join(t.TempDir(), "metadata_malformed.json") + metadataFile, _ := os.Create(metadataPath) + fmt.Fprint(metadataFile, `{`) + metadataFile.Close() + + err := Edit(logger, fileToEdit, "", metadataPath) + assert.Error(t, err) +} + +func TestTempfileExists(t *testing.T) { + fileToEdit := makeFixtureCopy(t, "edit_existing_tempfile") + + tmp, _ := os.Create(fileToEdit + ".tmp") + defer tmp.Close() + + metadataPath := filepath.Join(t.TempDir(), "metadata.json") + metadataFile, _ := os.Create(metadataPath) + fmt.Fprint(metadataFile, `{"foo":"bar"}`) + metadataFile.Close() + + err := Edit(logger, fileToEdit, "", metadataPath) + assert.Error(t, err) +} diff --git a/pmtiles/server.go b/pmtiles/server.go index 0b2c5eb..0405404 100644 --- a/pmtiles/server.go +++ b/pmtiles/server.go @@ -418,7 +418,7 @@ func (server *Server) getTileAttempt(ctx context.Context, httpHeaders map[string if headerVal, ok := headerContentType(header); ok { httpHeaders["Content-Type"] = headerVal } - if headerVal, ok := headerContentEncoding(header.TileCompression); ok { + if headerVal, ok := compressionToString(header.TileCompression); ok { httpHeaders["Content-Encoding"] = headerVal } return 200, httpHeaders, b, "" diff --git a/pmtiles/show_test.go b/pmtiles/show_test.go index 3ea59c0..dc9262d 100644 --- a/pmtiles/show_test.go +++ b/pmtiles/show_test.go @@ -17,8 +17,8 @@ func TestShowHeader(t *testing.T) { var input map[string]interface{} json.Unmarshal(b.Bytes(), &input) - assert.Equal(t, "mvt", input["TileType"]) - assert.Equal(t, "gzip", input["TileCompression"]) + assert.Equal(t, "mvt", input["tile_type"]) + assert.Equal(t, "gzip", input["tile_compression"]) } func TestShowMetadata(t *testing.T) { diff --git a/pmtiles/upload.go b/pmtiles/upload.go index f8954e8..b1a3fe6 100644 --- a/pmtiles/upload.go +++ b/pmtiles/upload.go @@ -19,7 +19,7 @@ func partSizeBytes(totalSize int64) int { } // Upload a pmtiles archive to a bucket. -func Upload(logger *log.Logger, InputPMTiles string, bucket string, RemotePMTiles string, maxConcurrency int) error { +func Upload(_ *log.Logger, InputPMTiles string, bucket string, RemotePMTiles string, maxConcurrency int) error { ctx := context.Background() b, err := blob.OpenBucket(ctx, bucket) diff --git a/pmtiles/verify.go b/pmtiles/verify.go index 8124bba..7b90643 100644 --- a/pmtiles/verify.go +++ b/pmtiles/verify.go @@ -27,25 +27,25 @@ func Verify(_ *log.Logger, file string) error { bucket, err := OpenBucket(ctx, bucketURL, "") if err != nil { - return fmt.Errorf("Failed to open bucket for %s, %w", bucketURL, err) + return fmt.Errorf("failed to open bucket for %s, %w", bucketURL, err) } defer bucket.Close() r, err := bucket.NewRangeReader(ctx, key, 0, 16384) if err != nil { - return fmt.Errorf("Failed to create range reader for %s, %w", key, err) + return fmt.Errorf("failed to create range reader for %s, %w", key, err) } b, err := io.ReadAll(r) if err != nil { - return fmt.Errorf("Failed to read %s, %w", key, err) + return fmt.Errorf("failed to read %s, %w", key, err) } r.Close() header, err := deserializeHeader(b[0:HeaderV3LenBytes]) if err != nil { - return fmt.Errorf("Failed to read %s, %w", key, err) + return fmt.Errorf("failed to read %s, %w", key, err) } fileInfo, _ := os.Stat(file) @@ -54,7 +54,7 @@ func Verify(_ *log.Logger, file string) error { lengthFromHeaderWithPadding := int64(16384 + header.MetadataLength + header.LeafDirectoryLength + header.TileDataLength) if fileInfo.Size() != lengthFromHeader && fileInfo.Size() != lengthFromHeaderWithPadding { - return fmt.Errorf("Total length of archive %v does not match header %v or %v (padded)", fileInfo.Size(), lengthFromHeader, lengthFromHeaderWithPadding) + return fmt.Errorf("total length of archive %v does not match header %v or %v (padded)", fileInfo.Size(), lengthFromHeader, lengthFromHeaderWithPadding) } var CollectEntries func(uint64, uint64, func(EntryV3)) @@ -108,7 +108,7 @@ func Verify(_ *log.Logger, file string) error { if header.Clustered { if !offsets.Contains(e.Offset) { if e.Offset != currentOffset { - fmt.Printf("Invalid: out-of-order entry %v in clustered archive.", e) + fmt.Printf("Invalid: out-of-order entry %v in clustered archive", e) } currentOffset += uint64(e.Length) } @@ -116,31 +116,31 @@ func Verify(_ *log.Logger, file string) error { }) if uint64(addressedTiles) != header.AddressedTilesCount { - return fmt.Errorf("Invalid: header AddressedTilesCount=%v but %v tiles addressed.", header.AddressedTilesCount, addressedTiles) + return fmt.Errorf("invalid: header AddressedTilesCount=%v but %v tiles addressed", header.AddressedTilesCount, addressedTiles) } if uint64(tileEntries) != header.TileEntriesCount { - return fmt.Errorf("Invalid: header TileEntriesCount=%v but %v tile entries.", header.TileEntriesCount, tileEntries) + return fmt.Errorf("invalid: header TileEntriesCount=%v but %v tile entries", header.TileEntriesCount, tileEntries) } if offsets.GetCardinality() != header.TileContentsCount { - return fmt.Errorf("Invalid: header TileContentsCount=%v but %v tile contents.", header.TileContentsCount, offsets.GetCardinality()) + return fmt.Errorf("invalid: header TileContentsCount=%v but %v tile contents", header.TileContentsCount, offsets.GetCardinality()) } if z, _, _ := IDToZxy(minTileID); z != header.MinZoom { - return fmt.Errorf("Invalid: header MinZoom=%v does not match min tile z %v", header.MinZoom, z) + return fmt.Errorf("invalid: header MinZoom=%v does not match min tile z %v", header.MinZoom, z) } if z, _, _ := IDToZxy(maxTileID); z != header.MaxZoom { - return fmt.Errorf("Invalid: header MaxZoom=%v does not match max tile z %v", header.MaxZoom, z) + return fmt.Errorf("invalid: header MaxZoom=%v does not match max tile z %v", header.MaxZoom, z) } if !(header.CenterZoom >= header.MinZoom && header.CenterZoom <= header.MaxZoom) { - return fmt.Errorf("Invalid: header CenterZoom=%v not within MinZoom/MaxZoom.", header.CenterZoom) + return fmt.Errorf("invalid: header CenterZoom=%v not within MinZoom/MaxZoom", header.CenterZoom) } if header.MinLonE7 >= header.MaxLonE7 || header.MinLatE7 >= header.MaxLatE7 { - return fmt.Errorf("Invalid: bounds has area <= 0: clients may not display tiles correctly.") + return fmt.Errorf("Invalid: bounds has area <= 0: clients may not display tiles correctly") } fmt.Printf("Completed verify in %v.\n", time.Since(start)) diff --git a/pmtiles/write.go b/pmtiles/write.go deleted file mode 100644 index a4e0f1e..0000000 --- a/pmtiles/write.go +++ /dev/null @@ -1,42 +0,0 @@ -package pmtiles - -import ( - "fmt" - "log" - "os" -) - -func Write(logger *log.Logger, inputArchive string, newHeaderJsonFile string, newMetadataFile string) error { - if newMetadataFile == "" { - if newHeaderJsonFile == "" { - return fmt.Errorf("No data to write.") - } - // we can write the header in-place without writing the whole file. - return nil - } - - // write metadata: - // always writes in this order: - // copy the header - // copy the root directory - // write the new the metadata - // copy the leaf directories - // copy the tile data - file, err := os.OpenFile(inputArchive, os.O_RDWR, 0666) - - buf := make([]byte, 127) - _, err = file.Read(buf) - if err != nil { - return err - } - originalHeader, _ := deserializeHeader(buf) - - // modify the header - - buf = serializeHeader(originalHeader) - _, err = file.WriteAt(buf, 0) - if err != nil { - return err - } - return nil -} diff --git a/pmtiles/write_test.go b/pmtiles/write_test.go deleted file mode 100644 index aa227c6..0000000 --- a/pmtiles/write_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package pmtiles - -import ( - "io" - "io/ioutil" - "os" - "path/filepath" - "testing" -) - -func TestWriteHeader(t *testing.T) { - tempDir, _ := ioutil.TempDir("", "testing") - defer os.RemoveAll(tempDir) - src, _ := os.Open("fixtures/test_fixture_1.pmtiles") - defer src.Close() - dest, _ := os.Create(filepath.Join(tempDir, "test.pmtiles")) - defer dest.Close() - _, _ = io.Copy(dest, src) -}