From 2746979b1052bc409479f770a7b2bba84e6eaac9 Mon Sep 17 00:00:00 2001 From: sfomuseumbot Date: Wed, 6 Sep 2023 12:34:27 -0700 Subject: [PATCH 01/10] update go.mod --- go.mod | 4 +++- go.sum | 1 - vendor/github.com/jmespath/go-jmespath/go.mod | 5 ----- vendor/github.com/jmespath/go-jmespath/go.sum | 11 ----------- vendor/github.com/mattn/go-sqlite3/go.mod | 3 --- vendor/github.com/mattn/go-sqlite3/go.sum | 0 vendor/modules.txt | 4 ++++ 7 files changed, 7 insertions(+), 21 deletions(-) delete mode 100644 vendor/github.com/jmespath/go-jmespath/go.mod delete mode 100644 vendor/github.com/jmespath/go-jmespath/go.sum delete mode 100644 vendor/github.com/mattn/go-sqlite3/go.mod delete mode 100644 vendor/github.com/mattn/go-sqlite3/go.sum diff --git a/go.mod b/go.mod index 3c24611..c85ef66 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,11 @@ module github.com/tilezen/go-tilepacks -go 1.12 +go 1.18 require ( github.com/aaronland/go-string v1.0.0 github.com/aws/aws-sdk-go v1.44.65 github.com/mattn/go-sqlite3 v1.14.14 ) + +require github.com/jmespath/go-jmespath v0.4.0 // indirect diff --git a/go.sum b/go.sum index 8a9d806..42e4bee 100644 --- a/go.sum +++ b/go.sum @@ -22,7 +22,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/github.com/jmespath/go-jmespath/go.mod b/vendor/github.com/jmespath/go-jmespath/go.mod deleted file mode 100644 index 4d448e8..0000000 --- a/vendor/github.com/jmespath/go-jmespath/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/jmespath/go-jmespath - -go 1.14 - -require github.com/jmespath/go-jmespath/internal/testify v1.5.1 diff --git a/vendor/github.com/jmespath/go-jmespath/go.sum b/vendor/github.com/jmespath/go-jmespath/go.sum deleted file mode 100644 index d2db411..0000000 --- a/vendor/github.com/jmespath/go-jmespath/go.sum +++ /dev/null @@ -1,11 +0,0 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/github.com/mattn/go-sqlite3/go.mod b/vendor/github.com/mattn/go-sqlite3/go.mod deleted file mode 100644 index 3d0854a..0000000 --- a/vendor/github.com/mattn/go-sqlite3/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/mattn/go-sqlite3 - -go 1.12 diff --git a/vendor/github.com/mattn/go-sqlite3/go.sum b/vendor/github.com/mattn/go-sqlite3/go.sum deleted file mode 100644 index e69de29..0000000 diff --git a/vendor/modules.txt b/vendor/modules.txt index 13547b5..7e48e59 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,6 +1,8 @@ # github.com/aaronland/go-string v1.0.0 +## explicit; go 1.16 github.com/aaronland/go-string/dsn # github.com/aws/aws-sdk-go v1.44.65 +## explicit; go 1.11 github.com/aws/aws-sdk-go/aws github.com/aws/aws-sdk-go/aws/arn github.com/aws/aws-sdk-go/aws/awserr @@ -53,6 +55,8 @@ github.com/aws/aws-sdk-go/service/sso/ssoiface github.com/aws/aws-sdk-go/service/sts github.com/aws/aws-sdk-go/service/sts/stsiface # github.com/jmespath/go-jmespath v0.4.0 +## explicit; go 1.14 github.com/jmespath/go-jmespath # github.com/mattn/go-sqlite3 v1.14.14 +## explicit; go 1.12 github.com/mattn/go-sqlite3 From c6c541198f57a422ebb7ee09746005682be8b3c9 Mon Sep 17 00:00:00 2001 From: sfomuseumbot Date: Fri, 6 Sep 2024 17:22:04 -0700 Subject: [PATCH 02/10] snapshot: start blocking out metadata stuff --- cmd/build/main.go | 2 +- cmd/merge/main.go | 58 ++++++++++++++++++++++++++++++----- tilepack/mbtiles_outputter.go | 46 +++++++++++++++++++++++++-- tilepack/mbtiles_reader.go | 58 +++++++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 10 deletions(-) diff --git a/cmd/build/main.go b/cmd/build/main.go index 4b5e76e..704e183 100644 --- a/cmd/build/main.go +++ b/cmd/build/main.go @@ -236,7 +236,7 @@ func main() { case "disk": outputter, outputterErr = tilepack.NewDiskOutputter(*outputDSN) case "mbtiles": - outputter, outputterErr = tilepack.NewMbtilesOutputter(*outputDSN, *mbtilesBatchSize) + outputter, outputterErr = tilepack.NewMbtilesOutputter(*outputDSN, *mbtilesBatchSize, bounds, zooms[0], zooms[len(zooms)-1]) default: log.Fatalf("Unknown outputter: %s", *outputMode) } diff --git a/cmd/merge/main.go b/cmd/merge/main.go index 12d4396..761ab25 100644 --- a/cmd/merge/main.go +++ b/cmd/merge/main.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/paulmach/orb/maptile" - "github.com/tilezen/go-tilepacks/tilepack" ) @@ -39,8 +38,57 @@ func main() { log.Fatalf("Output path %s already exists and cannot be overwritten", *outputFilename) } + var outputBounds orb.Bound + var outputMinZoom uint + var outputMaxZoom uint + + inputReaders := make([]tilepack.MbtilesReader, len(inputFilenames)) + + for i, inputFilename := range inputFilenames { + + mbtilesReader, err := tilepack.NewMbtilesReader(inputFilename) + if err != nil { + log.Fatalf("Couldn't read input mbtiles %s: %+v", inputFilename, err) + } + + metadata, err := mbtilesReader.Metadata() + + if err != nil { + log.Fatalf("Unable to read metadata for %s, %v", inputFilename, err) + } + + bounds, err := metadata.Bounds() + + if err != nil { + log.Fatalf("Unable to derive bounds for %s, %v", inputFilename, err) + } + + if outputBounds == nil { + outputBounds = bounds + } else { + outputBounds = outputBounds.Union(bounds) + } + + minZoom, err := metadata.MinZoom() + + if err != nil { + log.Fatalf("Unable to min zoom for %s, %v", inputFilename, err) + } + + maxZoom, err := metadata.MaxZoom() + + if err != nil { + log.Fatalf("Unable to max zoom for %s, %v", inputFilename, err) + } + + outputMinZoom = min(outputMinZoom, minzoom) + outputMaxZoom = min(outputMaxZoom, maxzoom) + + inputReaders[i] = mbtilesReader + } + // Create the output mbtiles - outputMbtiles, err := tilepack.NewMbtilesOutputter(*outputFilename, 1000) + outputMbtiles, err := tilepack.NewMbtilesOutputter(*outputFilename, 1000, outputBounds, maptile.Zoom(outputMinZoom), maptile.Zoom(outputMaxZoom)) if err != nil { log.Fatalf("Couldn't create output mbtiles: %+v", err) } @@ -50,11 +98,7 @@ func main() { log.Fatalf("Couldn't create output mbtiles: %+v", err) } - for _, inputFilename := range inputFilenames { - mbtilesReader, err := tilepack.NewMbtilesReader(inputFilename) - if err != nil { - log.Fatalf("Couldn't read input mbtiles %s: %+v", inputFilename, err) - } + for _, mbtilesReader := range inputReaders { err = mbtilesReader.VisitAllTiles(func(t maptile.Tile, data []byte) { outputMbtiles.Save(t, data) diff --git a/tilepack/mbtiles_outputter.go b/tilepack/mbtiles_outputter.go index 6bddd53..b66da00 100644 --- a/tilepack/mbtiles_outputter.go +++ b/tilepack/mbtiles_outputter.go @@ -4,19 +4,22 @@ import ( "crypto/md5" "database/sql" "encoding/hex" + "fmt" "math" + "strconv" _ "github.com/mattn/go-sqlite3" // Register sqlite3 database driver + "github.com/paulmach/orb" "github.com/paulmach/orb/maptile" ) -func NewMbtilesOutputter(dsn string, batchSize int) (*mbtilesOutputter, error) { +func NewMbtilesOutputter(dsn string, batchSize int, bounds orb.Bound, minZoom maptile.Zoom, maxZoom maptile.Zoom) (*mbtilesOutputter, error) { db, err := sql.Open("sqlite3", dsn) if err != nil { return nil, err } - return &mbtilesOutputter{db: db, batchSize: batchSize}, nil + return &mbtilesOutputter{db: db, batchSize: batchSize, bounds: bounds, minZoom: minZoom, maxZoom: maxZoom}, nil } type mbtilesOutputter struct { @@ -26,6 +29,9 @@ type mbtilesOutputter struct { hasTiles bool batchCount int batchSize int + bounds orb.Bound + minZoom maptile.Zoom + maxZoom maptile.Zoom } func (o *mbtilesOutputter) Close() error { @@ -84,11 +90,47 @@ func (o *mbtilesOutputter) CreateTiles() error { return nil } +func (o *mbtilesOutputter) AssignMetadata(bounds orb.Bound, minZoom maptile.Zoom, maxZoom maptile.Zoom) error { + + // https://github.com/mapbox/mbtiles-spec/blob/master/1.3/spec.md + + center := bounds.Center() + + str_bounds := fmt.Sprintf("%f,%f,%f,%f", bounds.Min[0], bounds.Min[1], bounds.Max[0], bounds.Max[1]) + str_center := fmt.Sprintf("%f,%f", center[0], center[1]) + + str_minzoom := strconv.Itoa(int(minZoom)) + str_maxzoom := strconv.Itoa(int(maxZoom)) + + metadata := map[string]string{ + "bounds": str_bounds, + "center": str_center, + "minzoom": str_minzoom, + "maxzoom": str_maxzoom, + } + + for name, value := range metadata { + + q := "INSERT INTO metadata (name, value) VALUES(?, ?)" + _, err := o.db.Exec(q, name, value) + + if err != nil { + return fmt.Errorf("Failed to add %s metadata key, %w", name, err) + } + } + + return nil +} + func (o *mbtilesOutputter) Save(tile maptile.Tile, data []byte) error { if err := o.CreateTiles(); err != nil { return err } + if err := o.AssignMetadata(o.bounds, o.minZoom, o.maxZoom); err != nil { + return err + } + if o.txn == nil { tx, err := o.db.Begin() if err != nil { diff --git a/tilepack/mbtiles_reader.go b/tilepack/mbtiles_reader.go index dae312e..a0ee403 100644 --- a/tilepack/mbtiles_reader.go +++ b/tilepack/mbtiles_reader.go @@ -2,9 +2,13 @@ package tilepack import ( "database/sql" + "fmt" "log" + "strconv" + "strings" _ "github.com/mattn/go-sqlite3" // Register sqlite3 database driver + "github.com/paulmach/orb" "github.com/paulmach/orb/maptile" ) @@ -17,6 +21,60 @@ type MbtilesReader interface { Close() error GetTile(tile maptile.Tile) (*TileData, error) VisitAllTiles(visitor func(maptile.Tile, []byte)) error + Metadata() (*MbtilesMetadata, error) +} + +type MbtilesMetadata map[string]string + +func (m *MbtilesMetadata) Bounds() (orb.Bound, error) { + + str_bounds, exists := m["bounds"] + + if !exists { + return nil, fmt.Errorf("Metadata is missing bounds") + } + + parts := strings.Split(str_bounds, ",") + + if len(parts) != 4 { + return nil, fmt.Errorf("Invalid bounds metadata") + } + + return nil, nil +} + +func (m *MbtilesMetadata) MinZoom() (uint, error) { + + str_minzoom, exists := *m["minzoom"] + + if !exists { + return 0, fmt.Errorf("Metadata is missing minzoom") + } + + i, err := strconv.Atoi(str_minzoom) + + if err != nil { + return 0, fmt.Errorf("Failed to parse minzoom value, %w", err) + } + + return uint(i), nil +} + +func (m *MbtilesMetadata) MaxZoom() (uint, error) { + + str_maxzoom, exists := *m["maxzoom"] + + if !exists { + return 0, fmt.Errorf("Metadata is missing maxzoom") + } + + i, err := strconv.Atoi(str_maxzoom) + + if err != nil { + return 0, fmt.Errorf("Failed to parse maxzoom value, %w", err) + } + + return uint(i), nil } type tileDataFromDatabase struct { From 08e05aaaf085bb2bca8c00ad94d2c1b67d6be30d Mon Sep 17 00:00:00 2001 From: sfomuseumbot Date: Fri, 6 Sep 2024 17:38:02 -0700 Subject: [PATCH 03/10] snapshot: metadata stuff compiles, untested --- cmd/merge/main.go | 11 +++--- tilepack/mbtiles_reader.go | 75 ++++++++++++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/cmd/merge/main.go b/cmd/merge/main.go index 761ab25..4ae8e06 100644 --- a/cmd/merge/main.go +++ b/cmd/merge/main.go @@ -6,6 +6,7 @@ import ( "os" "strings" + "github.com/paulmach/orb" "github.com/paulmach/orb/maptile" "github.com/tilezen/go-tilepacks/tilepack" ) @@ -63,7 +64,7 @@ func main() { log.Fatalf("Unable to derive bounds for %s, %v", inputFilename, err) } - if outputBounds == nil { + if i == 0 { outputBounds = bounds } else { outputBounds = outputBounds.Union(bounds) @@ -81,8 +82,8 @@ func main() { log.Fatalf("Unable to max zoom for %s, %v", inputFilename, err) } - outputMinZoom = min(outputMinZoom, minzoom) - outputMaxZoom = min(outputMaxZoom, maxzoom) + outputMinZoom = min(outputMinZoom, minZoom) + outputMaxZoom = min(outputMaxZoom, maxZoom) inputReaders[i] = mbtilesReader } @@ -98,13 +99,13 @@ func main() { log.Fatalf("Couldn't create output mbtiles: %+v", err) } - for _, mbtilesReader := range inputReaders { + for i, mbtilesReader := range inputReaders { err = mbtilesReader.VisitAllTiles(func(t maptile.Tile, data []byte) { outputMbtiles.Save(t, data) }) if err != nil { - log.Fatalf("Couldn't read tiles from %s: %+v", inputFilename, err) + log.Fatalf("Couldn't read tiles from %s: %+v", inputFilenames[i], err) } mbtilesReader.Close() } diff --git a/tilepack/mbtiles_reader.go b/tilepack/mbtiles_reader.go index a0ee403..fd5ce97 100644 --- a/tilepack/mbtiles_reader.go +++ b/tilepack/mbtiles_reader.go @@ -24,28 +24,89 @@ type MbtilesReader interface { Metadata() (*MbtilesMetadata, error) } -type MbtilesMetadata map[string]string +type MbtilesMetadata struct { + metadata map[string]string +} + +func NewMbtilesMetadata(metadata map[string]string) *MbtilesMetadata { + + m := &MbtilesMetadata{ + metadata: metadata, + } + + return m +} + +func (m *MbtilesMetadata) Get(k string) (string, bool) { + v, exists := m.metadata[k] + return v, exists +} + +func (m *MbtilesMetadata) Keys() []string { + + keys := make([]string, 0) + + for k, _ := range m.metadata { + keys = append(keys, k) + } + + return keys +} func (m *MbtilesMetadata) Bounds() (orb.Bound, error) { - str_bounds, exists := m["bounds"] + var bounds orb.Bound + + str_bounds, exists := m.Get("bounds") if !exists { - return nil, fmt.Errorf("Metadata is missing bounds") + return bounds, fmt.Errorf("Metadata is missing bounds") } parts := strings.Split(str_bounds, ",") if len(parts) != 4 { - return nil, fmt.Errorf("Invalid bounds metadata") + return bounds, fmt.Errorf("Invalid bounds metadata") + } + + minx, err := strconv.ParseFloat(parts[0], 64) + + if err != nil { + return bounds, fmt.Errorf("Failed to parse minx, %w", err) + } + + miny, err := strconv.ParseFloat(parts[1], 64) + + if err != nil { + return bounds, fmt.Errorf("Failed to parse miny, %w", err) + } + + maxx, err := strconv.ParseFloat(parts[2], 64) + + if err != nil { + return bounds, fmt.Errorf("Failed to parse maxx, %w", err) + } + + maxy, err := strconv.ParseFloat(parts[3], 64) + + if err != nil { + return bounds, fmt.Errorf("Failed to parse maxy, %w", err) + } + + min := orb.Point([2]float64{minx, miny}) + max := orb.Point([2]float64{maxx, maxy}) + + bounds = orb.Bound{ + Min: min, + Max: max, } - return nil, nil + return bounds, nil } func (m *MbtilesMetadata) MinZoom() (uint, error) { - str_minzoom, exists := *m["minzoom"] + str_minzoom, exists := m.Get("minzoom") if !exists { return 0, fmt.Errorf("Metadata is missing minzoom") @@ -62,7 +123,7 @@ func (m *MbtilesMetadata) MinZoom() (uint, error) { func (m *MbtilesMetadata) MaxZoom() (uint, error) { - str_maxzoom, exists := *m["maxzoom"] + str_maxzoom, exists := m.Get("maxzoom") if !exists { return 0, fmt.Errorf("Metadata is missing maxzoom") From e2b037255766d5fcdeaf4c4576db244a5e19154c Mon Sep 17 00:00:00 2001 From: sfomuseumbot Date: Fri, 6 Sep 2024 18:39:38 -0700 Subject: [PATCH 04/10] move AssignSpatialMetadata in the outputter interface --- cmd/build/main.go | 11 ++- cmd/ensure-metadata/main.go | 75 ++++++++++++++++ cmd/merge/main.go | 8 +- tilepack/disk_outputter.go | 5 ++ tilepack/mbtiles_metadata.go | 123 +++++++++++++++++++++++++++ tilepack/mbtiles_outputter.go | 13 +-- tilepack/mbtiles_reader.go | 155 ++++++++-------------------------- tilepack/outputter.go | 2 + 8 files changed, 262 insertions(+), 130 deletions(-) create mode 100644 cmd/ensure-metadata/main.go create mode 100644 tilepack/mbtiles_metadata.go diff --git a/cmd/build/main.go b/cmd/build/main.go index 704e183..a43cb31 100644 --- a/cmd/build/main.go +++ b/cmd/build/main.go @@ -236,7 +236,7 @@ func main() { case "disk": outputter, outputterErr = tilepack.NewDiskOutputter(*outputDSN) case "mbtiles": - outputter, outputterErr = tilepack.NewMbtilesOutputter(*outputDSN, *mbtilesBatchSize, bounds, zooms[0], zooms[len(zooms)-1]) + outputter, outputterErr = tilepack.NewMbtilesOutputter(*outputDSN, *mbtilesBatchSize) default: log.Fatalf("Unknown outputter: %s", *outputMode) } @@ -294,6 +294,15 @@ func main() { // Wait for the results to be written out resultWG.Wait() log.Print("Finished processing tiles") + + switch *outputMode { + case "mbtiles": + err = outputter.AssignSpatialMetadata(bounds, zooms[0], zooms[len(zooms)-1]) + + if err != nil { + log.Printf("Wrote tiles but failed to assign spatial metadata, %v", err) + } + } } func calculateExpectedTiles(bounds orb.Bound, zooms []maptile.Zoom) uint32 { diff --git a/cmd/ensure-metadata/main.go b/cmd/ensure-metadata/main.go new file mode 100644 index 0000000..1cd0a72 --- /dev/null +++ b/cmd/ensure-metadata/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "flag" + "log" + + "github.com/paulmach/orb" + "github.com/paulmach/orb/maptile" + "github.com/tilezen/go-tilepacks/tilepack" +) + +func main() { + + flag.Parse() + + for _, path := range flag.Args() { + + mbtilesReader, err := tilepack.NewMbtilesReader(path) + + if err != nil { + log.Fatalf("Couldn't read input mbtiles %s: %+v", path, err) + } + + /* + metadata, err := mbtilesReader.Metadata() + + if err != nil { + log.Fatalf("Unable to read metadata for %s, %v", path, err) + } + + log.Println(metadata) + */ + + var bounds *orb.Bound + minZoom := uint(20) + maxZoom := uint(0) + + err = mbtilesReader.VisitAllTiles(func(t maptile.Tile, data []byte) { + + tb := t.Bound() + + if bounds == nil { + bounds = &tb + } else { + tb = bounds.Union(tb) + bounds = &tb + } + + minZoom = min(minZoom, uint(t.Z)) + maxZoom = max(maxZoom, uint(t.Z)) + }) + + if err != nil { + log.Fatalf("Couldn't read tiles from %s: %+v", path, err) + } + + mbtilesReader.Close() + + log.Println(bounds, minZoom, maxZoom) + + mbtilesWriter, err := tilepack.NewMbtilesOutputter(path, 0) + + if err != nil { + log.Fatalf("Couldn't read input mbtiles %s: %+v", path, err) + } + + err = mbtilesWriter.AssignSpatialMetadata(*bounds, maptile.Zoom(minZoom), maptile.Zoom(maxZoom)) + + if err != nil { + log.Fatalf("Failed to assign spatial metadata to %s: %+v", path, err) + } + + mbtilesWriter.Close() + } +} diff --git a/cmd/merge/main.go b/cmd/merge/main.go index 4ae8e06..b08fddd 100644 --- a/cmd/merge/main.go +++ b/cmd/merge/main.go @@ -89,7 +89,7 @@ func main() { } // Create the output mbtiles - outputMbtiles, err := tilepack.NewMbtilesOutputter(*outputFilename, 1000, outputBounds, maptile.Zoom(outputMinZoom), maptile.Zoom(outputMaxZoom)) + outputMbtiles, err := tilepack.NewMbtilesOutputter(*outputFilename, 1000) if err != nil { log.Fatalf("Couldn't create output mbtiles: %+v", err) } @@ -110,5 +110,11 @@ func main() { mbtilesReader.Close() } + err = outputMbtiles.AssignSpatialMetadata(outputBounds, maptile.Zoom(outputMinZoom), maptile.Zoom(outputMaxZoom)) + + if err != nil { + log.Printf("Wrote tiles but failed to assign spatial metadata, %v", err) + } + outputMbtiles.Close() } diff --git a/tilepack/disk_outputter.go b/tilepack/disk_outputter.go index 82b9df6..dbe10d9 100644 --- a/tilepack/disk_outputter.go +++ b/tilepack/disk_outputter.go @@ -7,6 +7,7 @@ import ( "path/filepath" "github.com/aaronland/go-string/dsn" + "github.com/paulmach/orb" "github.com/paulmach/orb/maptile" ) @@ -39,6 +40,10 @@ func NewDiskOutputter(dsnStr string) (*diskOutputter, error) { return &o, nil } +func (o *diskOutputter) AssignSpatialMetadata(bounds orb.Bound, minZoom maptile.Zoom, maxZoom maptile.Zoom) error { + return nil +} + func (o *diskOutputter) Close() error { return nil } diff --git a/tilepack/mbtiles_metadata.go b/tilepack/mbtiles_metadata.go new file mode 100644 index 0000000..27730cf --- /dev/null +++ b/tilepack/mbtiles_metadata.go @@ -0,0 +1,123 @@ +package tilepack + +import ( + "fmt" + "strconv" + "strings" + + "github.com/paulmach/orb" +) + +type MbtilesMetadata struct { + metadata map[string]string +} + +func NewMbtilesMetadata(metadata map[string]string) *MbtilesMetadata { + + m := &MbtilesMetadata{ + metadata: metadata, + } + + return m +} + +func (m *MbtilesMetadata) Get(k string) (string, bool) { + v, exists := m.metadata[k] + return v, exists +} + +func (m *MbtilesMetadata) Keys() []string { + + keys := make([]string, 0) + + for k, _ := range m.metadata { + keys = append(keys, k) + } + + return keys +} + +func (m *MbtilesMetadata) Bounds() (orb.Bound, error) { + + var bounds orb.Bound + + str_bounds, exists := m.Get("bounds") + + if !exists { + return bounds, fmt.Errorf("Metadata is missing bounds") + } + + parts := strings.Split(str_bounds, ",") + + if len(parts) != 4 { + return bounds, fmt.Errorf("Invalid bounds metadata") + } + + minx, err := strconv.ParseFloat(parts[0], 64) + + if err != nil { + return bounds, fmt.Errorf("Failed to parse minx, %w", err) + } + + miny, err := strconv.ParseFloat(parts[1], 64) + + if err != nil { + return bounds, fmt.Errorf("Failed to parse miny, %w", err) + } + + maxx, err := strconv.ParseFloat(parts[2], 64) + + if err != nil { + return bounds, fmt.Errorf("Failed to parse maxx, %w", err) + } + + maxy, err := strconv.ParseFloat(parts[3], 64) + + if err != nil { + return bounds, fmt.Errorf("Failed to parse maxy, %w", err) + } + + min := orb.Point([2]float64{minx, miny}) + max := orb.Point([2]float64{maxx, maxy}) + + bounds = orb.Bound{ + Min: min, + Max: max, + } + + return bounds, nil +} + +func (m *MbtilesMetadata) MinZoom() (uint, error) { + + str_minzoom, exists := m.Get("minzoom") + + if !exists { + return 0, fmt.Errorf("Metadata is missing minzoom") + } + + i, err := strconv.Atoi(str_minzoom) + + if err != nil { + return 0, fmt.Errorf("Failed to parse minzoom value, %w", err) + } + + return uint(i), nil +} + +func (m *MbtilesMetadata) MaxZoom() (uint, error) { + + str_maxzoom, exists := m.Get("maxzoom") + + if !exists { + return 0, fmt.Errorf("Metadata is missing maxzoom") + } + + i, err := strconv.Atoi(str_maxzoom) + + if err != nil { + return 0, fmt.Errorf("Failed to parse maxzoom value, %w", err) + } + + return uint(i), nil +} diff --git a/tilepack/mbtiles_outputter.go b/tilepack/mbtiles_outputter.go index b66da00..cfa3a77 100644 --- a/tilepack/mbtiles_outputter.go +++ b/tilepack/mbtiles_outputter.go @@ -13,13 +13,13 @@ import ( "github.com/paulmach/orb/maptile" ) -func NewMbtilesOutputter(dsn string, batchSize int, bounds orb.Bound, minZoom maptile.Zoom, maxZoom maptile.Zoom) (*mbtilesOutputter, error) { +func NewMbtilesOutputter(dsn string, batchSize int) (*mbtilesOutputter, error) { db, err := sql.Open("sqlite3", dsn) if err != nil { return nil, err } - return &mbtilesOutputter{db: db, batchSize: batchSize, bounds: bounds, minZoom: minZoom, maxZoom: maxZoom}, nil + return &mbtilesOutputter{db: db, batchSize: batchSize}, nil } type mbtilesOutputter struct { @@ -29,9 +29,6 @@ type mbtilesOutputter struct { hasTiles bool batchCount int batchSize int - bounds orb.Bound - minZoom maptile.Zoom - maxZoom maptile.Zoom } func (o *mbtilesOutputter) Close() error { @@ -90,7 +87,7 @@ func (o *mbtilesOutputter) CreateTiles() error { return nil } -func (o *mbtilesOutputter) AssignMetadata(bounds orb.Bound, minZoom maptile.Zoom, maxZoom maptile.Zoom) error { +func (o *mbtilesOutputter) AssignSpatialMetadata(bounds orb.Bound, minZoom maptile.Zoom, maxZoom maptile.Zoom) error { // https://github.com/mapbox/mbtiles-spec/blob/master/1.3/spec.md @@ -127,10 +124,6 @@ func (o *mbtilesOutputter) Save(tile maptile.Tile, data []byte) error { return err } - if err := o.AssignMetadata(o.bounds, o.minZoom, o.maxZoom); err != nil { - return err - } - if o.txn == nil { tx, err := o.db.Begin() if err != nil { diff --git a/tilepack/mbtiles_reader.go b/tilepack/mbtiles_reader.go index fd5ce97..7c74e75 100644 --- a/tilepack/mbtiles_reader.go +++ b/tilepack/mbtiles_reader.go @@ -2,13 +2,9 @@ package tilepack import ( "database/sql" - "fmt" "log" - "strconv" - "strings" _ "github.com/mattn/go-sqlite3" // Register sqlite3 database driver - "github.com/paulmach/orb" "github.com/paulmach/orb/maptile" ) @@ -24,120 +20,6 @@ type MbtilesReader interface { Metadata() (*MbtilesMetadata, error) } -type MbtilesMetadata struct { - metadata map[string]string -} - -func NewMbtilesMetadata(metadata map[string]string) *MbtilesMetadata { - - m := &MbtilesMetadata{ - metadata: metadata, - } - - return m -} - -func (m *MbtilesMetadata) Get(k string) (string, bool) { - v, exists := m.metadata[k] - return v, exists -} - -func (m *MbtilesMetadata) Keys() []string { - - keys := make([]string, 0) - - for k, _ := range m.metadata { - keys = append(keys, k) - } - - return keys -} - -func (m *MbtilesMetadata) Bounds() (orb.Bound, error) { - - var bounds orb.Bound - - str_bounds, exists := m.Get("bounds") - - if !exists { - return bounds, fmt.Errorf("Metadata is missing bounds") - } - - parts := strings.Split(str_bounds, ",") - - if len(parts) != 4 { - return bounds, fmt.Errorf("Invalid bounds metadata") - } - - minx, err := strconv.ParseFloat(parts[0], 64) - - if err != nil { - return bounds, fmt.Errorf("Failed to parse minx, %w", err) - } - - miny, err := strconv.ParseFloat(parts[1], 64) - - if err != nil { - return bounds, fmt.Errorf("Failed to parse miny, %w", err) - } - - maxx, err := strconv.ParseFloat(parts[2], 64) - - if err != nil { - return bounds, fmt.Errorf("Failed to parse maxx, %w", err) - } - - maxy, err := strconv.ParseFloat(parts[3], 64) - - if err != nil { - return bounds, fmt.Errorf("Failed to parse maxy, %w", err) - } - - min := orb.Point([2]float64{minx, miny}) - max := orb.Point([2]float64{maxx, maxy}) - - bounds = orb.Bound{ - Min: min, - Max: max, - } - - return bounds, nil -} - -func (m *MbtilesMetadata) MinZoom() (uint, error) { - - str_minzoom, exists := m.Get("minzoom") - - if !exists { - return 0, fmt.Errorf("Metadata is missing minzoom") - } - - i, err := strconv.Atoi(str_minzoom) - - if err != nil { - return 0, fmt.Errorf("Failed to parse minzoom value, %w", err) - } - - return uint(i), nil -} - -func (m *MbtilesMetadata) MaxZoom() (uint, error) { - - str_maxzoom, exists := m.Get("maxzoom") - - if !exists { - return 0, fmt.Errorf("Metadata is missing maxzoom") - } - - i, err := strconv.Atoi(str_maxzoom) - - if err != nil { - return 0, fmt.Errorf("Failed to parse maxzoom value, %w", err) - } - - return uint(i), nil -} - type tileDataFromDatabase struct { Data *[]byte } @@ -216,3 +98,40 @@ func (o *mbtilesReader) VisitAllTiles(visitor func(maptile.Tile, []byte)) error } return nil } + +func (o *mbtilesReader) Metadata() (*MbtilesMetadata, error) { + + metadata := make(map[string]string) + + q := "SELECT name, value FROM metadata" + rows, err := o.db.Query(q) + + if err != nil { + return nil, err + } + + for rows.Next() { + var name string + var value string + + err := rows.Scan(&name, &value) + + if err != nil { + return nil, err + } + + metadata[name] = value + } + + rerr := rows.Close() + + if rerr != nil { + return nil, err + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return NewMbtilesMetadata(metadata), nil +} diff --git a/tilepack/outputter.go b/tilepack/outputter.go index 73e3e5e..5f819d4 100644 --- a/tilepack/outputter.go +++ b/tilepack/outputter.go @@ -1,11 +1,13 @@ package tilepack import ( + "github.com/paulmach/orb" "github.com/paulmach/orb/maptile" ) type TileOutputter interface { CreateTiles() error Save(tile maptile.Tile, data []byte) error + AssignSpatialMetadata(orb.Bound, maptile.Zoom, maptile.Zoom) error Close() error } From 3850c501cf158bc02cfa613bedcbf119a653cb2f Mon Sep 17 00:00:00 2001 From: sfomuseumbot Date: Fri, 6 Sep 2024 18:49:26 -0700 Subject: [PATCH 05/10] fix SQL for assigning metadata --- Makefile | 1 + .../main.go | 76 ++++++++++++++----- tilepack/mbtiles_outputter.go | 2 +- 3 files changed, 59 insertions(+), 20 deletions(-) rename cmd/{ensure-metadata => mbtiles-assign-metadata}/main.go (54%) diff --git a/Makefile b/Makefile index 2d49eff..c3d72af 100644 --- a/Makefile +++ b/Makefile @@ -2,3 +2,4 @@ tools: go build -mod vendor -o bin/build cmd/build/main.go go build -mod vendor -o bin/merge cmd/merge/main.go go build -mod vendor -o bin/serve cmd/serve/main.go + go build -mod vendor -o bin/mbtiles-assign-metadata cmd/mbtiles-assign-metadata/main.go diff --git a/cmd/ensure-metadata/main.go b/cmd/mbtiles-assign-metadata/main.go similarity index 54% rename from cmd/ensure-metadata/main.go rename to cmd/mbtiles-assign-metadata/main.go index 1cd0a72..8da6b37 100644 --- a/cmd/ensure-metadata/main.go +++ b/cmd/mbtiles-assign-metadata/main.go @@ -1,65 +1,67 @@ package main import ( - "flag" "log" - + "flag" + "github.com/paulmach/orb" "github.com/paulmach/orb/maptile" - "github.com/tilezen/go-tilepacks/tilepack" + "github.com/tilezen/go-tilepacks/tilepack" ) func main() { + var verify bool + + flag.BoolVar(&verify, "verify", false, "Verify that spatial metadata was written to each database") + flag.Parse() for _, path := range flag.Args() { mbtilesReader, err := tilepack.NewMbtilesReader(path) - + if err != nil { log.Fatalf("Couldn't read input mbtiles %s: %+v", path, err) } /* - metadata, err := mbtilesReader.Metadata() + metadata, err := mbtilesReader.Metadata() - if err != nil { - log.Fatalf("Unable to read metadata for %s, %v", path, err) - } + if err != nil { + log.Fatalf("Unable to read metadata for %s, %v", path, err) + } - log.Println(metadata) + log.Println(metadata) */ - + var bounds *orb.Bound minZoom := uint(20) maxZoom := uint(0) - + err = mbtilesReader.VisitAllTiles(func(t maptile.Tile, data []byte) { tb := t.Bound() - + if bounds == nil { bounds = &tb } else { tb = bounds.Union(tb) bounds = &tb } - + minZoom = min(minZoom, uint(t.Z)) - maxZoom = max(maxZoom, uint(t.Z)) + maxZoom = max(maxZoom, uint(t.Z)) }) - + if err != nil { log.Fatalf("Couldn't read tiles from %s: %+v", path, err) } mbtilesReader.Close() - - log.Println(bounds, minZoom, maxZoom) - + mbtilesWriter, err := tilepack.NewMbtilesOutputter(path, 0) - + if err != nil { log.Fatalf("Couldn't read input mbtiles %s: %+v", path, err) } @@ -71,5 +73,41 @@ func main() { } mbtilesWriter.Close() + + if verify { + + mbtilesReader, err := tilepack.NewMbtilesReader(path) + + if err != nil { + log.Fatalf("Couldn't read input mbtiles %s: %+v", path, err) + } + + metadata, err := mbtilesReader.Metadata() + + if err != nil { + log.Fatalf("Unable to read metadata for %s, %v", path, err) + } + + bounds, err := metadata.Bounds() + + if err != nil { + log.Fatalf("Failed to derive bounds metadata after update") + } + + minZoom, err := metadata.MinZoom() + + if err != nil { + log.Fatalf("Failed to derive min zoom metadata after update") + } + + maxZoom, err := metadata.MaxZoom() + + if err != nil { + log.Fatalf("Failed to derive max zoom metadata after update") + } + + log.Printf("[%s] bounds: %v zoom: %d-%d\n", path, bounds, minZoom, maxZoom) + } } } + diff --git a/tilepack/mbtiles_outputter.go b/tilepack/mbtiles_outputter.go index cfa3a77..3ca1d5c 100644 --- a/tilepack/mbtiles_outputter.go +++ b/tilepack/mbtiles_outputter.go @@ -108,7 +108,7 @@ func (o *mbtilesOutputter) AssignSpatialMetadata(bounds orb.Bound, minZoom mapti for name, value := range metadata { - q := "INSERT INTO metadata (name, value) VALUES(?, ?)" + q := "INSERT OR REPLACE INTO metadata (name, value) VALUES(?, ?)" _, err := o.db.Exec(q, name, value) if err != nil { From affe02fa54d1e871b0c0cf5cd917a65a88506f7c Mon Sep 17 00:00:00 2001 From: sfomuseumbot Date: Fri, 6 Sep 2024 19:00:07 -0700 Subject: [PATCH 06/10] don't trap final AssignSpatialMetadata call in a switch statement in cmd/build --- cmd/build/main.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/cmd/build/main.go b/cmd/build/main.go index a43cb31..f228ded 100644 --- a/cmd/build/main.go +++ b/cmd/build/main.go @@ -295,13 +295,10 @@ func main() { resultWG.Wait() log.Print("Finished processing tiles") - switch *outputMode { - case "mbtiles": - err = outputter.AssignSpatialMetadata(bounds, zooms[0], zooms[len(zooms)-1]) - - if err != nil { - log.Printf("Wrote tiles but failed to assign spatial metadata, %v", err) - } + err = outputter.AssignSpatialMetadata(bounds, zooms[0], zooms[len(zooms)-1]) + + if err != nil { + log.Printf("Wrote tiles but failed to assign spatial metadata, %v", err) } } From 0beefab90bd2b66b8e6794169e8ec01468780fb3 Mon Sep 17 00:00:00 2001 From: sfomuseumbot Date: Fri, 6 Sep 2024 19:01:25 -0700 Subject: [PATCH 07/10] remove unused code --- cmd/mbtiles-assign-metadata/main.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/cmd/mbtiles-assign-metadata/main.go b/cmd/mbtiles-assign-metadata/main.go index 8da6b37..ea33cd6 100644 --- a/cmd/mbtiles-assign-metadata/main.go +++ b/cmd/mbtiles-assign-metadata/main.go @@ -24,16 +24,6 @@ func main() { if err != nil { log.Fatalf("Couldn't read input mbtiles %s: %+v", path, err) } - - /* - metadata, err := mbtilesReader.Metadata() - - if err != nil { - log.Fatalf("Unable to read metadata for %s, %v", path, err) - } - - log.Println(metadata) - */ var bounds *orb.Bound minZoom := uint(20) From 0301c3ee4ee50ae5be45386b99c9a3f7183d7d1f Mon Sep 17 00:00:00 2001 From: sfomuseumbot Date: Fri, 6 Sep 2024 19:05:33 -0700 Subject: [PATCH 08/10] add a Center method to MbtilesMetadata --- cmd/build/main.go | 2 +- cmd/mbtiles-assign-metadata/main.go | 47 ++++++++++++++++------------- tilepack/mbtiles_metadata.go | 32 ++++++++++++++++++++ 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/cmd/build/main.go b/cmd/build/main.go index f228ded..a449d31 100644 --- a/cmd/build/main.go +++ b/cmd/build/main.go @@ -296,7 +296,7 @@ func main() { log.Print("Finished processing tiles") err = outputter.AssignSpatialMetadata(bounds, zooms[0], zooms[len(zooms)-1]) - + if err != nil { log.Printf("Wrote tiles but failed to assign spatial metadata, %v", err) } diff --git a/cmd/mbtiles-assign-metadata/main.go b/cmd/mbtiles-assign-metadata/main.go index ea33cd6..4e0aa76 100644 --- a/cmd/mbtiles-assign-metadata/main.go +++ b/cmd/mbtiles-assign-metadata/main.go @@ -1,12 +1,12 @@ package main import ( - "log" "flag" - + "log" + "github.com/paulmach/orb" "github.com/paulmach/orb/maptile" - "github.com/tilezen/go-tilepacks/tilepack" + "github.com/tilezen/go-tilepacks/tilepack" ) func main() { @@ -14,44 +14,44 @@ func main() { var verify bool flag.BoolVar(&verify, "verify", false, "Verify that spatial metadata was written to each database") - + flag.Parse() for _, path := range flag.Args() { mbtilesReader, err := tilepack.NewMbtilesReader(path) - + if err != nil { log.Fatalf("Couldn't read input mbtiles %s: %+v", path, err) } - + var bounds *orb.Bound minZoom := uint(20) maxZoom := uint(0) - + err = mbtilesReader.VisitAllTiles(func(t maptile.Tile, data []byte) { tb := t.Bound() - + if bounds == nil { bounds = &tb } else { tb = bounds.Union(tb) bounds = &tb } - + minZoom = min(minZoom, uint(t.Z)) - maxZoom = max(maxZoom, uint(t.Z)) + maxZoom = max(maxZoom, uint(t.Z)) }) - + if err != nil { log.Fatalf("Couldn't read tiles from %s: %+v", path, err) } mbtilesReader.Close() - + mbtilesWriter, err := tilepack.NewMbtilesOutputter(path, 0) - + if err != nil { log.Fatalf("Couldn't read input mbtiles %s: %+v", path, err) } @@ -67,37 +67,42 @@ func main() { if verify { mbtilesReader, err := tilepack.NewMbtilesReader(path) - + if err != nil { log.Fatalf("Couldn't read input mbtiles %s: %+v", path, err) } - + metadata, err := mbtilesReader.Metadata() - + if err != nil { log.Fatalf("Unable to read metadata for %s, %v", path, err) } - + bounds, err := metadata.Bounds() if err != nil { log.Fatalf("Failed to derive bounds metadata after update") } + center, err := metadata.Center() + + if err != nil { + log.Fatalf("Failed to derive bounds metadata after update") + } + minZoom, err := metadata.MinZoom() if err != nil { log.Fatalf("Failed to derive min zoom metadata after update") } - + maxZoom, err := metadata.MaxZoom() - + if err != nil { log.Fatalf("Failed to derive max zoom metadata after update") } - log.Printf("[%s] bounds: %v zoom: %d-%d\n", path, bounds, minZoom, maxZoom) + log.Printf("[%s] bounds: %v center: %v zoom: %d-%d\n", path, bounds, center, minZoom, maxZoom) } } } - diff --git a/tilepack/mbtiles_metadata.go b/tilepack/mbtiles_metadata.go index 27730cf..0f1fb5b 100644 --- a/tilepack/mbtiles_metadata.go +++ b/tilepack/mbtiles_metadata.go @@ -88,6 +88,38 @@ func (m *MbtilesMetadata) Bounds() (orb.Bound, error) { return bounds, nil } +func (m *MbtilesMetadata) Center() (orb.Point, error) { + + var pt orb.Point + + str_center, exists := m.Get("center") + + if !exists { + return pt, fmt.Errorf("Metadata is missing center") + } + + parts := strings.Split(str_center, ",") + + if len(parts) != 2 { + return pt, fmt.Errorf("Invalid center metadata") + } + + x, err := strconv.ParseFloat(parts[0], 64) + + if err != nil { + return pt, fmt.Errorf("Failed to parse x, %w", err) + } + + y, err := strconv.ParseFloat(parts[1], 64) + + if err != nil { + return pt, fmt.Errorf("Failed to parse y, %w", err) + } + + pt = orb.Point([2]float64{x, y}) + return pt, nil +} + func (m *MbtilesMetadata) MinZoom() (uint, error) { str_minzoom, exists := m.Get("minzoom") From 22420cd5d7d5121724d54478f93114e2c75d665d Mon Sep 17 00:00:00 2001 From: Aaron Straup Cope Date: Sun, 22 Sep 2024 11:47:14 -0700 Subject: [PATCH 09/10] Update cmd/merge/main.go Co-authored-by: Ian Dees --- cmd/merge/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/merge/main.go b/cmd/merge/main.go index b08fddd..25ab06b 100644 --- a/cmd/merge/main.go +++ b/cmd/merge/main.go @@ -73,7 +73,7 @@ func main() { minZoom, err := metadata.MinZoom() if err != nil { - log.Fatalf("Unable to min zoom for %s, %v", inputFilename, err) + log.Fatalf("Unable to derive min zoom for %s, %v", inputFilename, err) } maxZoom, err := metadata.MaxZoom() From 9c017778d96180f2675f5a6dd80da18502067c04 Mon Sep 17 00:00:00 2001 From: Aaron Straup Cope Date: Sun, 22 Sep 2024 11:47:19 -0700 Subject: [PATCH 10/10] Update cmd/merge/main.go Co-authored-by: Ian Dees --- cmd/merge/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/merge/main.go b/cmd/merge/main.go index 25ab06b..9009e8b 100644 --- a/cmd/merge/main.go +++ b/cmd/merge/main.go @@ -79,7 +79,7 @@ func main() { maxZoom, err := metadata.MaxZoom() if err != nil { - log.Fatalf("Unable to max zoom for %s, %v", inputFilename, err) + log.Fatalf("Unable to derive max zoom for %s, %v", inputFilename, err) } outputMinZoom = min(outputMinZoom, minZoom)