From 16f161e819d5a071666a839575a0a9277d593031 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Tue, 11 Aug 2026 14:41:39 +0200 Subject: [PATCH 1/8] feat: z coord in encodedtable and zoomlevel --- main.go | 2 +- processing/gpkg/gpkgmvt.go | 20 ++++++++++---------- processing/gpkg/gpkgsnap.go | 7 ++++--- processing/interface.go | 6 +++--- processing/processingmvt.go | 4 ++-- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/main.go b/main.go index d619b2f..c96f086 100644 --- a/main.go +++ b/main.go @@ -237,7 +237,7 @@ func main() { &cli.StringFlag{ Name: MVTOUTDIR, Aliases: []string{"o"}, - Usage: "Directory to write the generated /.mvt files to", + Usage: "Directory to write the generated //.mvt files to", Required: true, EnvVars: []string{strcase.ToScreamingSnake(MVTOUTDIR)}, }, diff --git a/processing/gpkg/gpkgmvt.go b/processing/gpkg/gpkgmvt.go index c6ef7c5..ffcc839 100644 --- a/processing/gpkg/gpkgmvt.go +++ b/processing/gpkg/gpkgmvt.go @@ -100,21 +100,21 @@ func (source MVTSourceGeopackage) AttributeColumnNames() []string { // selectEncodedSQL builds the SELECT for fetching the encoded features of a single tile func (t Table) selectEncodedSQL() string { - return `SELECT feature_id, geometry_type, data FROM "` + t.EncodedName() + `" WHERE tile_x = ? AND tile_y = ?` + return `SELECT feature_id, geometry_type, data FROM "` + t.EncodedName() + `" WHERE tile_x = ? AND tile_y = ? AND tile_z = ?` } // selectDistinctTilesSQL builds the SELECT for enumerating every tile that has at // least one encoded feature. func (t Table) selectDistinctTilesSQL() string { - return `SELECT DISTINCT tile_x, tile_y FROM "` + t.EncodedName() + `" ORDER BY tile_x, tile_y` + return `SELECT DISTINCT tile_x, tile_y, tile_z FROM "` + t.EncodedName() + `" ORDER BY tile_x, tile_y` } // GetFeaturesForTile returns every encoded feature stored for the given tile, // i.e. the equivalent of the tile-features table's rows for (tileX, tileY). -func (source MVTSourceGeopackage) GetFeaturesForTile(tileX, tileY uint) ([]tile.EncodedFeatureRow, error) { - rows, err := source.handle.Query(source.Table.selectEncodedSQL(), tileX, tileY) +func (source MVTSourceGeopackage) GetFeaturesForTile(tileX, tileY, tileZ uint) ([]tile.EncodedFeatureRow, error) { + rows, err := source.handle.Query(source.Table.selectEncodedSQL(), tileX, tileY, tileZ) if err != nil { - return nil, fmt.Errorf("querying encoded features for tile (%d,%d): %w", tileX, tileY, err) + return nil, fmt.Errorf("querying encoded features for tile (%d,%d,%d): %w", tileX, tileY, tileZ, err) } defer rows.Close() @@ -162,11 +162,11 @@ func (source MVTSourceGeopackage) ListTiles() ([]processing.TileCoord, error) { var tiles []processing.TileCoord for rows.Next() { - var x, y int64 - if err := rows.Scan(&x, &y); err != nil { + var x, y, z int64 + if err := rows.Scan(&x, &y, &z); err != nil { return nil, fmt.Errorf("scanning tile row: %w", err) } - tiles = append(tiles, processing.TileCoord{X: uint(x), Y: uint(y)}) //nolint:gosec // G115 Tile coords fit within uint + tiles = append(tiles, processing.TileCoord{X: uint(x), Y: uint(y), Z: uint(z)}) //nolint:gosec // G115 Tile coords fit within uint } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterating tile rows: %w", err) @@ -245,8 +245,8 @@ type MVTFileTarget struct { // WriteTile writes one tile's serialized bytes to //.mvt, // creating directories as needed. -func (t *MVTFileTarget) WriteTile(x, y uint, data []byte) error { - dir := filepath.Join(t.OutDir, strconv.FormatUint(uint64(x), 10)) +func (t *MVTFileTarget) WriteTile(x, y, z uint, data []byte) error { + dir := filepath.Join(t.OutDir, strconv.FormatUint(uint64(z), 10), strconv.FormatUint(uint64(x), 10)) if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("creating tile directory %s: %w", dir, err) } diff --git a/processing/gpkg/gpkgsnap.go b/processing/gpkg/gpkgsnap.go index 58cd563..ba1be5b 100644 --- a/processing/gpkg/gpkgsnap.go +++ b/processing/gpkg/gpkgsnap.go @@ -145,8 +145,8 @@ func (t Table) insertSQL() string { // insertSQLEncoded used for writing the encoded geometries when -enc is on func (t Table) insertSQLEncoded() string { return `INSERT INTO "` + t.EncodedName() + `"` + - ` (tile_x, tile_y, feature_id, geometry_type, data)` + - ` VALUES (?, ?, ?, ?, ?)` + ` (tile_x, tile_y, tile_z, feature_id, geometry_type, data)` + + ` VALUES (?, ?, ?, ?, ?, ?)` } func serializeToBytes(intslice []uint32) []byte { @@ -280,7 +280,7 @@ func (target *TargetGeopackage) writeEncodedFeatures(encFeature []processing.Fea fid := cols[0] for _, eg := range ef.EncodedGeoms() { bytes := serializeToBytes(eg.Encoding) - _, err := stmt.Exec(eg.XTile, eg.YTile, fid, eg.GeometryType, bytes) + _, err := stmt.Exec(eg.XTile, eg.YTile, uint(ef.TileMatrixID()), fid, eg.GeometryType, bytes) //nolint:gosec // G115 integers < 40 if err != nil { log.Fatalf("Could not get a result summary from the prepared statement for fid %s: %s", fid, err) } @@ -349,6 +349,7 @@ func buildEncodedTable(h *gpkg.Handle, t Table) error { id INTEGER PRIMARY KEY, tile_x INTEGER NOT NULL, tile_y INTEGER NOT NULL, + tile_z INTEGER NOT NULL, feature_id INTEGER NOT NULL, geometry_type INTEGER NOT NULL, data BLOB diff --git a/processing/interface.go b/processing/interface.go index 4345b29..15f94bb 100644 --- a/processing/interface.go +++ b/processing/interface.go @@ -24,7 +24,7 @@ type SnapResult struct { } type TileCoord struct { - X, Y uint + X, Y, Z uint } // Source and target for snapping @@ -39,11 +39,11 @@ type Target interface { // Source and target for tile generation type MVTSource interface { ListTiles() ([]TileCoord, error) - GetFeaturesForTile(tileX, tileY uint) ([]tile.EncodedFeatureRow, error) + GetFeaturesForTile(tileX, tileY, tileZ uint) ([]tile.EncodedFeatureRow, error) GetAttributesForFeatures(featureIDs []int64) (tile.InternalAttributeTable, error) AttributeColumnNames() []string } type MVTTarget interface { - WriteTile(x, y uint, data []byte) error + WriteTile(x, y, z uint, data []byte) error } diff --git a/processing/processingmvt.go b/processing/processingmvt.go index f90cb8f..68d1754 100644 --- a/processing/processingmvt.go +++ b/processing/processingmvt.go @@ -19,7 +19,7 @@ func BuildAndWriteMVTTiles(source MVTSource, target MVTTarget, tableName string) } for _, tc := range tiles { - encFeatRows, err := source.GetFeaturesForTile(tc.X, tc.Y) + encFeatRows, err := source.GetFeaturesForTile(tc.X, tc.Y, tc.Z) if err != nil { return fmt.Errorf("getting features for tile (%d,%d): %w", tc.X, tc.Y, err) } @@ -38,7 +38,7 @@ func BuildAndWriteMVTTiles(source MVTSource, target MVTTarget, tableName string) return fmt.Errorf("building tile (%d,%d): %w", tc.X, tc.Y, err) } - if err := target.WriteTile(tc.X, tc.Y, data); err != nil { + if err := target.WriteTile(tc.X, tc.Y, tc.Z, data); err != nil { return fmt.Errorf("writing tile (%d,%d): %w", tc.X, tc.Y, err) } } From 5eb4cafa4136642bc25973f8a876664902522ff5 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Wed, 12 Aug 2026 09:42:33 +0200 Subject: [PATCH 2/8] feat: read toml config and process it --- config/mvtconfig.go | 52 +++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 ++ main.go | 72 +++++++++++++++++++++++++++++++++++-- processing/processingmvt.go | 19 ++++++++++ 5 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 config/mvtconfig.go diff --git a/config/mvtconfig.go b/config/mvtconfig.go new file mode 100644 index 0000000..5d651bd --- /dev/null +++ b/config/mvtconfig.go @@ -0,0 +1,52 @@ +package config + +import ( + "fmt" + + "github.com/BurntSushi/toml" +) + +type DataSource struct { + Name string `toml:"name"` + Path string `toml:"path"` +} + +type LayerConfig struct { + Name string `toml:"name"` + MinZoom uint `toml:"minzoom"` + MaxZoom uint `toml:"maxzoom"` + DataSource string `toml:"datasource"` + TableName string `toml:"table_name"` +} + +// Tileset mirrors a `[[Tileset]]` array-of-tables entry, including its +// nested `[[Tileset.layer]]` entries. +type Tileset struct { + Name string `toml:"name"` + MinZoom uint `toml:"minzoom"` + MaxZoom uint `toml:"maxzoom"` + Layer []LayerConfig `toml:"layer"` +} + +// TomlConfig mirrors the top-level structure of an mvt config toml file, +// e.g. example/NetherlandsRDNewQuad.toml. +type TomlConfig struct { + DataSource []DataSource `toml:"datasource"` + Tileset []Tileset `toml:"tileset"` +} + +// ParseMVTConfig reads an mvt config toml file and returns all the +// DataSource and LayerConfig instances it contains. +func ParseMVTConfig(path string) ([]DataSource, []LayerConfig, error) { + var cfg TomlConfig + if _, err := toml.DecodeFile(path, &cfg); err != nil { + return nil, nil, fmt.Errorf("decoding mvt config %q: %w", path, err) + } + + var layers []LayerConfig + for _, ts := range cfg.Tileset { + layers = append(layers, ts.Layer...) + } + + return cfg.DataSource, layers, nil +} diff --git a/go.mod b/go.mod index 24ab543..84723d7 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( ) require ( + github.com/BurntSushi/toml v1.5.0 // indirect github.com/arolek/p v0.0.0-20191103215535-df3c295ed582 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect diff --git a/go.sum b/go.sum index 1c9ce50..d474429 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/arolek/p v0.0.0-20191103215535-df3c295ed582 h1:DugKk4B3PpqfZ1QunDSPqNZDAErQmdCgwurloi1Axaw= github.com/arolek/p v0.0.0-20191103215535-df3c295ed582/go.mod h1:JPNItmi3yb44Q5QWM+Kh5n9oeRhfcJzPNS90mbLo25U= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= diff --git a/main.go b/main.go index c96f086..f3b37d3 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "slices" "syscall" + "github.com/pdok/texel/config" "github.com/pdok/texel/pointindex" "github.com/go-spatial/geom" @@ -39,8 +40,9 @@ const ( TILEBUFFER string = `tilebuffer` USELINETRACE string = `uselinetrace` - MVTSOURCE string = `mvtSourceGpkg` - MVTOUTDIR string = `mvtOutDir` + MVTSOURCE string = `mvtSourceGpkg` + MVTOUTDIR string = `mvtOutDir` + TILEMATRIX string = `tilematrix` ) //nolint:funlen @@ -241,6 +243,13 @@ func main() { Required: true, EnvVars: []string{strcase.ToScreamingSnake(MVTOUTDIR)}, }, + &cli.UintFlag{ + Name: TILEMATRIX, + Aliases: []string{"z"}, + Usage: "Target zoomlevel", + Required: true, + EnvVars: []string{strcase.ToScreamingSnake(TILEMATRIX)}, + }, }, Action: func(c *cli.Context) error { return runBuildMVTTiles(c.String(MVTSOURCE), c.String(MVTOUTDIR)) @@ -296,6 +305,65 @@ func processBySnapping(source processing.Source, targets map[tms20.TMID]processi }, snapConfig.EncodeTiles, snapConfig.Buffer) } +// Construct Layer info from config; init gpkg sources +// Make sure to reuse gpkg handles +// Also return helper that closes all sources +func buildLayers(z uint, rawConfig config.TomlConfig) ([]processing.Layer, func()) { + type initedSource struct { + source gpkg.MVTSourceGeopackage + tables map[string]gpkg.Table + } + dataSourceDictionary := processing.DatasourceToDictionary(rawConfig.DataSource) + layers := make([]processing.Layer, 0) + sources := make(map[string]initedSource) + for _, tileset := range rawConfig.Tileset { + if z < tileset.MinZoom || z > tileset.MaxZoom { + continue + } + for _, rawLayer := range tileset.Layer { + if z < rawLayer.MinZoom || z > rawLayer.MaxZoom { + continue + } + // Only init gpkg sources once + if _, present := sources[rawLayer.DataSource]; !present { + path := dataSourceDictionary[rawLayer.DataSource] + source := gpkg.MVTSourceGeopackage{} + source.Init(path) + tables := source.GetTableInfo() + tableMap := make(map[string]gpkg.Table, len(tables)) + for _, table := range tables { + tableMap[table.Name] = table + } + sources[rawLayer.DataSource] = initedSource{source, tableMap} + } + initedSource, present := sources[rawLayer.DataSource] + if !present { + err := fmt.Errorf("layer %s requires datasource %s; not found", rawLayer.Name, rawLayer.DataSource) + panic(err) + } + table, present := initedSource.tables[rawLayer.TableName] + if !present { + err := fmt.Errorf("layer %s requires table %s in datasource %s; not found", rawLayer.Name, rawLayer.TableName, rawLayer.DataSource) + panic(err) + } + // Create source with this table + source := initedSource.source + source.Table = table + layer := processing.Layer{ + Name: rawLayer.Name, + Source: source, + } + layers = append(layers, layer) + } + } + closeSources := func() { + for _, s := range sources { + s.source.Close() + } + } + return layers, closeSources +} + // Initialize resources for creating vecotrtiles and delegate to processing. func runBuildMVTTiles(sourcePath, outDir string) error { if _, err := os.Stat(sourcePath); os.IsNotExist(err) { diff --git a/processing/processingmvt.go b/processing/processingmvt.go index 68d1754..9437e49 100644 --- a/processing/processingmvt.go +++ b/processing/processingmvt.go @@ -5,6 +5,7 @@ package processing import ( "fmt" + "github.com/pdok/texel/config" "github.com/pdok/texel/tile" ) @@ -44,3 +45,21 @@ func BuildAndWriteMVTTiles(source MVTSource, target MVTTarget, tableName string) } return nil } + +type Layer struct { + Source MVTSource + Name string +} + +type MvtConfig struct { + Layers []Layer + Zoomlevel uint +} + +func DatasourceToDictionary(sources []config.DataSource) map[string]string { + dataSourceDictionary := make(map[string]string, 0) + for _, dataSource := range sources { + dataSourceDictionary[dataSource.Name] = dataSource.Path + } + return dataSourceDictionary +} From 4d43ab5d07354d5cb05e9f4fcf6a8d7332b924fa Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Thu, 13 Aug 2026 10:40:52 +0200 Subject: [PATCH 3/8] feat: list tiles --- main.go | 14 ++++++------- processing/gpkg/gpkgmvt.go | 21 ++++++++++--------- processing/interface.go | 2 +- processing/processingmvt.go | 40 ++++++++++++++++++++++++++++++++----- 4 files changed, 52 insertions(+), 25 deletions(-) diff --git a/main.go b/main.go index f3b37d3..8a4a6c5 100644 --- a/main.go +++ b/main.go @@ -349,10 +349,7 @@ func buildLayers(z uint, rawConfig config.TomlConfig) ([]processing.Layer, func( // Create source with this table source := initedSource.source source.Table = table - layer := processing.Layer{ - Name: rawLayer.Name, - Source: source, - } + layer := processing.BuildLayer(rawLayer.Name, source) layers = append(layers, layer) } } @@ -374,15 +371,16 @@ func runBuildMVTTiles(sourcePath, outDir string) error { source.Init(sourcePath) defer source.Close() - mvtTarget := gpkg.MVTFileTarget{OutDir: outDir} +// mvtTarget := gpkg.MVTFileTarget{OutDir: outDir} tables := source.GetTableInfo() for _, table := range tables { source.Table = table log.Printf(" building MVT tiles for %s", table.Name) - if err := processing.BuildAndWriteMVTTiles(&source, &mvtTarget, table.Name); err != nil { - return fmt.Errorf("building MVT tiles for %s: %w", table.Name, err) - } + // To be replaced + //if err := processing.BuildAndWriteMVTTiles(&source, &mvtTarget, table.Name); err != nil { + // return fmt.Errorf("building MVT tiles for %s: %w", table.Name, err) + //} } return nil } diff --git a/processing/gpkg/gpkgmvt.go b/processing/gpkg/gpkgmvt.go index ffcc839..8ee22cb 100644 --- a/processing/gpkg/gpkgmvt.go +++ b/processing/gpkg/gpkgmvt.go @@ -106,7 +106,7 @@ func (t Table) selectEncodedSQL() string { // selectDistinctTilesSQL builds the SELECT for enumerating every tile that has at // least one encoded feature. func (t Table) selectDistinctTilesSQL() string { - return `SELECT DISTINCT tile_x, tile_y, tile_z FROM "` + t.EncodedName() + `" ORDER BY tile_x, tile_y` + return `SELECT DISTINCT tile_x, tile_y FROM "` + t.EncodedName() + `" WHERE tile_z = ? ORDER BY tile_x, tile_y` } // GetFeaturesForTile returns every encoded feature stored for the given tile, @@ -153,25 +153,24 @@ func deserializeFromBytes(data []byte) []uint32 { } // ListTiles returns every distinct tile present in the "_encoded" table. -func (source MVTSourceGeopackage) ListTiles() ([]processing.TileCoord, error) { - rows, err := source.handle.Query(source.Table.selectDistinctTilesSQL()) +func (source MVTSourceGeopackage) ListTiles(z uint, tileSet map[processing.TileCoord]bool) error { + rows, err := source.handle.Query(source.Table.selectDistinctTilesSQL(), z) if err != nil { - return nil, fmt.Errorf("listing tiles: %w", err) + return fmt.Errorf("listing tiles: %w", err) } defer rows.Close() - var tiles []processing.TileCoord for rows.Next() { - var x, y, z int64 - if err := rows.Scan(&x, &y, &z); err != nil { - return nil, fmt.Errorf("scanning tile row: %w", err) + var x, y int64 + if err := rows.Scan(&x, &y); err != nil { + return fmt.Errorf("scanning tile row: %w", err) } - tiles = append(tiles, processing.TileCoord{X: uint(x), Y: uint(y), Z: uint(z)}) //nolint:gosec // G115 Tile coords fit within uint + tileSet[processing.TileCoord{X: uint(x), Y: uint(y), Z: z}] = true //nolint:gosec // G115 Tile coord fit within uint } if err := rows.Err(); err != nil { - return nil, fmt.Errorf("iterating tile rows: %w", err) + return fmt.Errorf("iterating tile rows: %w", err) } - return tiles, nil + return nil } // Read from source.Table, store rows as attributes[fid][columnName] = value for all fids diff --git a/processing/interface.go b/processing/interface.go index 15f94bb..0d422be 100644 --- a/processing/interface.go +++ b/processing/interface.go @@ -38,7 +38,7 @@ type Target interface { // Source and target for tile generation type MVTSource interface { - ListTiles() ([]TileCoord, error) + ListTiles(zoomlevel uint, tileSet map[TileCoord]bool) error GetFeaturesForTile(tileX, tileY, tileZ uint) ([]tile.EncodedFeatureRow, error) GetAttributesForFeatures(featureIDs []int64) (tile.InternalAttributeTable, error) AttributeColumnNames() []string diff --git a/processing/processingmvt.go b/processing/processingmvt.go index 9437e49..66057d5 100644 --- a/processing/processingmvt.go +++ b/processing/processingmvt.go @@ -10,15 +10,19 @@ import ( ) // Orchestrator for the tile creation pipeline -func BuildAndWriteMVTTiles(source MVTSource, target MVTTarget, tableName string) error { +func BuildAndWriteMVTTiles(layers []Layer, zoomlevel uint, target MVTTarget) error { // Build key index (columns) just once. - keyIndex := tile.BuildKeyDictionary(source.AttributeColumnNames()) - tiles, err := source.ListTiles() + tiles, err := listTiles(zoomlevel, layers) if err != nil { return err } + // temporary code + source := layers[0].Source + tableName := layers[0].Name // This is incorrect + keyIndex := layers[0].keyIndex + for _, tc := range tiles { encFeatRows, err := source.GetFeaturesForTile(tc.X, tc.Y, tc.Z) if err != nil { @@ -47,8 +51,18 @@ func BuildAndWriteMVTTiles(source MVTSource, target MVTTarget, tableName string) } type Layer struct { - Source MVTSource - Name string + Source MVTSource + Name string + keyIndex map[string]uint32 +} + +func BuildLayer(name string, source MVTSource) Layer { + keydict := tile.BuildKeyDictionary(source.AttributeColumnNames()) + return Layer{ + Name: name, + Source: source, + keyIndex: keydict, + } } type MvtConfig struct { @@ -63,3 +77,19 @@ func DatasourceToDictionary(sources []config.DataSource) map[string]string { } return dataSourceDictionary } + +func listTiles(zoomlevel uint, layers []Layer) ([]TileCoord, error) { + tileSet := make(map[TileCoord]bool, 0) + for _, layer := range layers { + err := layer.Source.ListTiles(zoomlevel, tileSet) + if err != nil { + return nil, err + } + } + + tileList := make([]TileCoord, 0, len(tileSet)) + for tilecoord := range tileSet { + tileList = append(tileList, tilecoord) + } + return tileList, nil +} From 9154cfbb16de09f9bbefa594bd6ad588396bbb3e Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Thu, 13 Aug 2026 13:48:40 +0200 Subject: [PATCH 4/8] feat: processing logic for multiple layers --- main.go | 6 +- processing/processingmvt.go | 62 ++++++++----- tile/assemble.go | 59 +++++------- tile/assemble_test.go | 180 +++++++++++++++++++++--------------- 4 files changed, 166 insertions(+), 141 deletions(-) diff --git a/main.go b/main.go index 8a4a6c5..785856a 100644 --- a/main.go +++ b/main.go @@ -371,16 +371,12 @@ func runBuildMVTTiles(sourcePath, outDir string) error { source.Init(sourcePath) defer source.Close() -// mvtTarget := gpkg.MVTFileTarget{OutDir: outDir} + // mvtTarget := gpkg.MVTFileTarget{OutDir: outDir} tables := source.GetTableInfo() for _, table := range tables { source.Table = table log.Printf(" building MVT tiles for %s", table.Name) - // To be replaced - //if err := processing.BuildAndWriteMVTTiles(&source, &mvtTarget, table.Name); err != nil { - // return fmt.Errorf("building MVT tiles for %s: %w", table.Name, err) - //} } return nil } diff --git a/processing/processingmvt.go b/processing/processingmvt.go index 66057d5..3202f2c 100644 --- a/processing/processingmvt.go +++ b/processing/processingmvt.go @@ -5,6 +5,7 @@ package processing import ( "fmt" + vectorTile "github.com/go-spatial/geom/encoding/mvt/vector_tile" "github.com/pdok/texel/config" "github.com/pdok/texel/tile" ) @@ -18,33 +19,13 @@ func BuildAndWriteMVTTiles(layers []Layer, zoomlevel uint, target MVTTarget) err return err } - // temporary code - source := layers[0].Source - tableName := layers[0].Name // This is incorrect - keyIndex := layers[0].keyIndex - - for _, tc := range tiles { - encFeatRows, err := source.GetFeaturesForTile(tc.X, tc.Y, tc.Z) + for _, coord := range tiles { + data, err := processTile(coord, layers) if err != nil { - return fmt.Errorf("getting features for tile (%d,%d): %w", tc.X, tc.Y, err) - } - featIDs := make([]int64, len(encFeatRows)) - for i, encFeatRow := range encFeatRows { - featIDs[i] = encFeatRow.FeatureID + return fmt.Errorf("building tile (%d, %d): %w", coord.X, coord.Y, err) } - - attributes, err := source.GetAttributesForFeatures(featIDs) - if err != nil { - return fmt.Errorf("getting attributes for tile (%d,%d): %w", tc.X, tc.Y, err) - } - - data, err := tile.BuildMVTTile(tableName, keyIndex, encFeatRows, attributes) - if err != nil { - return fmt.Errorf("building tile (%d,%d): %w", tc.X, tc.Y, err) - } - - if err := target.WriteTile(tc.X, tc.Y, tc.Z, data); err != nil { - return fmt.Errorf("writing tile (%d,%d): %w", tc.X, tc.Y, err) + if err := target.WriteTile(coord.X, coord.Y, coord.Z, data); err != nil { + return fmt.Errorf("writing tile (%d, %d): %w", coord.X, coord.Y, err) } } return nil @@ -93,3 +74,34 @@ func listTiles(zoomlevel uint, layers []Layer) ([]TileCoord, error) { } return tileList, nil } + +func processLayer(tileCoord TileCoord, layer Layer) (*vectorTile.Tile_Layer, error) { + encFeatRows, err := layer.Source.GetFeaturesForTile(tileCoord.X, tileCoord.Y, tileCoord.Z) + if err != nil { + return nil, fmt.Errorf("getting features for tile (%d,%d): %w", tileCoord.X, tileCoord.Y, err) + } + featIDs := make([]int64, len(encFeatRows)) + for i, encFeatRow := range encFeatRows { + featIDs[i] = encFeatRow.FeatureID + } + + attributes, err := layer.Source.GetAttributesForFeatures(featIDs) + if err != nil { + return nil, fmt.Errorf("getting attributes for tile (%d,%d): %w", tileCoord.X, tileCoord.Y, err) + } + + return tile.BuildLayer(layer.Name, layer.keyIndex, encFeatRows, attributes) +} + +func processTile(coord TileCoord, layers []Layer) ([]byte, error) { + encLayers := make([]*vectorTile.Tile_Layer, 0, len(layers)) + for _, layer := range layers { + encLayer, err := processLayer(coord, layer) + if err != nil { + err = fmt.Errorf("during layer encoding %s: %w", layer.Name, err) + return nil, err + } + encLayers = append(encLayers, encLayer) + } + return tile.BuildEncodeTile(encLayers) +} diff --git a/tile/assemble.go b/tile/assemble.go index 78106a7..7217a6b 100644 --- a/tile/assemble.go +++ b/tile/assemble.go @@ -180,8 +180,29 @@ func BuildFeature(featureID int64, geom EncodedGeometry, attributes map[string]a }, nil } -// Construct layer according to protobuf format -func BuildLayer(name string, features []*vectorTile.Tile_Feature, keyIndex map[string]uint32, valueIndex map[any]uint32) *vectorTile.Tile_Layer { +func BuildEncodeTile(layers []*vectorTile.Tile_Layer) ([]byte, error) { + tile := &vectorTile.Tile{Layers: layers} + data, err := oldproto.Marshal(tile) + if err != nil { + return nil, fmt.Errorf("marshaling tile: %w", err) + } + return data, nil +} + +func BuildLayer(name string, keyIndex map[string]uint32, encFeatrows []EncodedFeatureRow, attributes InternalAttributeTable) (*vectorTile.Tile_Layer, error) { + valueIndex := BuildValueDictionary(attributes) + + features := make([]*vectorTile.Tile_Feature, 0, len(encFeatrows)) + for _, encodedFeature := range encFeatrows { + featureID := encodedFeature.FeatureID + attrs := attributesForFeature(attributes, featureID) + feature, err := BuildFeature(featureID, encodedFeature.Geom, attrs, keyIndex, valueIndex) + if err != nil { + return nil, err + } + features = append(features, feature) + } + keys := mapslicehelp.OrderedByIndex(keyIndex) rawValues := mapslicehelp.OrderedByIndex(valueIndex) @@ -194,7 +215,7 @@ func BuildLayer(name string, features []*vectorTile.Tile_Feature, keyIndex map[s extent := uint32(precision) layerName := name - return &vectorTile.Tile_Layer{ + tile := &vectorTile.Tile_Layer{ Version: &version, Name: &layerName, Features: features, @@ -202,37 +223,7 @@ func BuildLayer(name string, features []*vectorTile.Tile_Feature, keyIndex map[s Values: values, Extent: &extent, } -} - -// Construct tile according to protobuf format -func BuildTile(layers ...*vectorTile.Tile_Layer) *vectorTile.Tile { - return &vectorTile.Tile{Layers: layers} -} - -// Main exported function that encodes a tile of a single layer. Assumes -// geometry and keyIndex have already been encoded. Returns marshalled byte -// string. -func BuildMVTTile(layerName string, keyIndex map[string]uint32, encFeatRows []EncodedFeatureRow, attributes InternalAttributeTable) ([]byte, error) { - valueIndex := BuildValueDictionary(attributes) - - features := make([]*vectorTile.Tile_Feature, 0, len(encFeatRows)) - for _, encodedFeature := range encFeatRows { - featureID := encodedFeature.FeatureID - attrs := attributesForFeature(attributes, featureID) - feature, err := BuildFeature(featureID, encodedFeature.Geom, attrs, keyIndex, valueIndex) - if err != nil { - return nil, err - } - features = append(features, feature) - } - - layer := BuildLayer(layerName, features, keyIndex, valueIndex) - - data, err := MarshalTile(BuildTile(layer)) - if err != nil { - return nil, fmt.Errorf("marshaling tile: %w", err) - } - return data, nil + return tile, nil } // attributesForFeature extracts a single feature's attributes from the map construct. diff --git a/tile/assemble_test.go b/tile/assemble_test.go index 79aecfb..d9536e9 100644 --- a/tile/assemble_test.go +++ b/tile/assemble_test.go @@ -5,7 +5,6 @@ import ( "testing" vectorTile "github.com/go-spatial/geom/encoding/mvt/vector_tile" - oldproto "github.com/golang/protobuf/proto" //nolint:staticcheck // matches MarshalTile's use of the reflection-based marshaler "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -155,82 +154,109 @@ func TestBuildTags(t *testing.T) { } func TestBuildLayer(t *testing.T) { - t.Run("happy path building layer", func(t *testing.T) { - keyIndex := map[string]uint32{"b": 1, "a": 0} - valueIndex := map[any]uint32{"second": 1, "first": 0} - features := []*vectorTile.Tile_Feature{{}, {}} - - layer := BuildLayer("mylayer", features, keyIndex, valueIndex) - - assert.Equal(t, "mylayer", layer.GetName()) - assert.Equal(t, uint32(mvtVersion), layer.GetVersion()) - assert.Equal(t, uint32(precision), layer.GetExtent()) - assert.Same(t, features[0], layer.GetFeatures()[0]) - assert.Same(t, features[1], layer.GetFeatures()[1]) - - // Keys/values must be ordered according to their assigned index, - // regardless of the (arbitrary) map iteration order used to build them. - assert.Equal(t, []string{"a", "b"}, layer.GetKeys()) - require.Len(t, layer.GetValues(), 2) - assert.Equal(t, "first", layer.GetValues()[0].GetStringValue()) - assert.Equal(t, "second", layer.GetValues()[1].GetStringValue()) - }) - t.Run("empty layer", func(t *testing.T) { - layer := BuildLayer("empty", nil, map[string]uint32{}, map[any]uint32{}) - - assert.Equal(t, "empty", layer.GetName()) - assert.Empty(t, layer.GetKeys()) - assert.Empty(t, layer.GetValues()) - assert.Empty(t, layer.GetFeatures()) - }) -} - -func TestBuildMVTTile(t *testing.T) { - t.Run("happy path roundtrips features, tags and attribute values", func(t *testing.T) { - keyIndex := BuildKeyDictionary([]string{"name"}) - attributes := InternalAttributeTable{ - 1: {"name": "foo"}, - 2: {"name": "bar"}, - } - encFeatRows := []EncodedFeatureRow{ - {FeatureID: 1, Geom: EncodedGeometry{GeometryType: int32(vectorTile.Tile_POINT)}}, - {FeatureID: 2, Geom: EncodedGeometry{GeometryType: int32(vectorTile.Tile_POINT)}}, - } - - data, err := BuildMVTTile("mylayer", keyIndex, encFeatRows, attributes) - require.NoError(t, err) - require.NotEmpty(t, data) - - got := &vectorTile.Tile{} - require.NoError(t, oldproto.Unmarshal(data, got)) - require.Len(t, got.GetLayers(), 1) - - layer := got.GetLayers()[0] - assert.Equal(t, "mylayer", layer.GetName()) - require.Len(t, layer.GetFeatures(), 2) - - // Collect the string values assigned to each feature via its tags, - // independent of feature/value ordering. - gotNames := make([]string, 0, 2) - for _, feature := range layer.GetFeatures() { - require.Len(t, feature.GetTags(), 2) - valueIdx := feature.GetTags()[1] - gotNames = append(gotNames, layer.GetValues()[valueIdx].GetStringValue()) - } - sort.Strings(gotNames) - assert.Equal(t, []string{"bar", "foo"}, gotNames) - }) + tests := []struct { + name string + layerName string + keyIndex map[string]uint32 + encFeatRows []EncodedFeatureRow + attributes InternalAttributeTable + wantErr bool + validate func(t *testing.T, layer *vectorTile.Tile_Layer) + }{ + { + name: "empty layer", + layerName: "empty", + keyIndex: map[string]uint32{}, + encFeatRows: nil, + attributes: InternalAttributeTable{}, + validate: func(t *testing.T, layer *vectorTile.Tile_Layer) { + t.Helper() + assert.Equal(t, "empty", layer.GetName()) + assert.Equal(t, uint32(mvtVersion), layer.GetVersion()) + assert.Equal(t, uint32(precision), layer.GetExtent()) + assert.Empty(t, layer.GetKeys()) + assert.Empty(t, layer.GetValues()) + assert.Empty(t, layer.GetFeatures()) + }, + }, + { + name: "keys are ordered according to key index", + layerName: "mylayer", + keyIndex: map[string]uint32{"b": 1, "a": 0}, + encFeatRows: []EncodedFeatureRow{ + {FeatureID: 1, Geom: EncodedGeometry{GeometryType: int32(vectorTile.Tile_POINT)}}, + }, + attributes: InternalAttributeTable{ + 1: {"a": "first", "b": "second"}, + }, + validate: func(t *testing.T, layer *vectorTile.Tile_Layer) { + t.Helper() + assert.Equal(t, "mylayer", layer.GetName()) + assert.Equal(t, uint32(mvtVersion), layer.GetVersion()) + assert.Equal(t, uint32(precision), layer.GetExtent()) - t.Run("error building a feature's tags propagates", func(t *testing.T) { - keyIndex := map[string]uint32{"name": 0} - attributes := InternalAttributeTable{ - 1: {"unknown-key": "foo"}, - } - encFeatRows := []EncodedFeatureRow{ - {FeatureID: 1, Geom: EncodedGeometry{}}, - } + // Keys must be ordered according to their assigned index, + // regardless of the (arbitrary) map iteration order. + assert.Equal(t, []string{"a", "b"}, layer.GetKeys()) + require.Len(t, layer.GetValues(), 2) + gotValues := make([]string, len(layer.GetValues())) + for i, v := range layer.GetValues() { + gotValues[i] = v.GetStringValue() + } + assert.ElementsMatch(t, []string{"first", "second"}, gotValues) + }, + }, + { + name: "happy path roundtrips features, tags and attribute values", + layerName: "mylayer", + keyIndex: BuildKeyDictionary([]string{"name"}), + encFeatRows: []EncodedFeatureRow{ + {FeatureID: 1, Geom: EncodedGeometry{GeometryType: int32(vectorTile.Tile_POINT)}}, + {FeatureID: 2, Geom: EncodedGeometry{GeometryType: int32(vectorTile.Tile_POINT)}}, + }, + attributes: InternalAttributeTable{ + 1: {"name": "foo"}, + 2: {"name": "bar"}, + }, + validate: func(t *testing.T, layer *vectorTile.Tile_Layer) { + t.Helper() + assert.Equal(t, "mylayer", layer.GetName()) + require.Len(t, layer.GetFeatures(), 2) - _, err := BuildMVTTile("mylayer", keyIndex, encFeatRows, attributes) - require.Error(t, err) - }) + // Collect the string values assigned to each feature via its + // tags, independent of feature/value ordering. + gotNames := make([]string, 0, 2) + for _, feature := range layer.GetFeatures() { + require.Len(t, feature.GetTags(), 2) + valueIdx := feature.GetTags()[1] + gotNames = append(gotNames, layer.GetValues()[valueIdx].GetStringValue()) + } + sort.Strings(gotNames) + assert.Equal(t, []string{"bar", "foo"}, gotNames) + }, + }, + { + name: "error building a feature's tags propagates", + layerName: "mylayer", + keyIndex: map[string]uint32{"name": 0}, + encFeatRows: []EncodedFeatureRow{ + {FeatureID: 1, Geom: EncodedGeometry{}}, + }, + attributes: InternalAttributeTable{ + 1: {"unknown-key": "foo"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + layer, err := BuildLayer(tt.layerName, tt.keyIndex, tt.encFeatRows, tt.attributes) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + tt.validate(t, layer) + }) + } } From 9cb5688b66b46d652e83288f9cc218e02dc5f3e1 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Thu, 13 Aug 2026 14:10:08 +0200 Subject: [PATCH 5/8] feat: wire config and layer processing in main --- config/mvtconfig.go | 16 +++++----------- go.mod | 2 +- main.go | 44 ++++++++++++++++++++++++-------------------- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/config/mvtconfig.go b/config/mvtconfig.go index 5d651bd..bc5ccee 100644 --- a/config/mvtconfig.go +++ b/config/mvtconfig.go @@ -35,18 +35,12 @@ type TomlConfig struct { Tileset []Tileset `toml:"tileset"` } -// ParseMVTConfig reads an mvt config toml file and returns all the -// DataSource and LayerConfig instances it contains. -func ParseMVTConfig(path string) ([]DataSource, []LayerConfig, error) { +// ParseMVTConfig reads an mvt config toml file and returns the resulting +// TomlConfig. +func ParseMVTConfig(path string) (TomlConfig, error) { var cfg TomlConfig if _, err := toml.DecodeFile(path, &cfg); err != nil { - return nil, nil, fmt.Errorf("decoding mvt config %q: %w", path, err) + return TomlConfig{}, fmt.Errorf("decoding mvt config %q: %w", path, err) } - - var layers []LayerConfig - for _, ts := range cfg.Tileset { - layers = append(layers, ts.Layer...) - } - - return cfg.DataSource, layers, nil + return cfg, nil } diff --git a/go.mod b/go.mod index 84723d7..59eeaf7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/pdok/texel go 1.26.2 require ( + github.com/BurntSushi/toml v1.5.0 github.com/carlmjohnson/versioninfo v0.22.5 github.com/creasty/defaults v1.8.0 github.com/go-playground/validator/v10 v10.30.3 @@ -19,7 +20,6 @@ require ( ) require ( - github.com/BurntSushi/toml v1.5.0 // indirect github.com/arolek/p v0.0.0-20191103215535-df3c295ed582 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect diff --git a/main.go b/main.go index 785856a..12a8cc6 100644 --- a/main.go +++ b/main.go @@ -40,7 +40,7 @@ const ( TILEBUFFER string = `tilebuffer` USELINETRACE string = `uselinetrace` - MVTSOURCE string = `mvtSourceGpkg` + MVTCONFIG string = `config` MVTOUTDIR string = `mvtOutDir` TILEMATRIX string = `tilematrix` ) @@ -230,11 +230,11 @@ func main() { Usage: "Build MVT tiles from an already-encoded target GeoPackage (see --" + ENCODETILES + ")", Flags: []cli.Flag{ &cli.StringFlag{ - Name: MVTSOURCE, - Aliases: []string{"s"}, - Usage: `GeoPackage containing the encoded ("
_encoded") and attribute tables`, + Name: MVTCONFIG, + Aliases: []string{"c"}, + Usage: "Config toml file describing datasources, tilesets and layers", Required: true, - EnvVars: []string{strcase.ToScreamingSnake(MVTSOURCE)}, + EnvVars: []string{strcase.ToScreamingSnake(MVTCONFIG)}, }, &cli.StringFlag{ Name: MVTOUTDIR, @@ -252,7 +252,7 @@ func main() { }, }, Action: func(c *cli.Context) error { - return runBuildMVTTiles(c.String(MVTSOURCE), c.String(MVTOUTDIR)) + return runMVT(c.String(MVTCONFIG), c.String(MVTOUTDIR), c.Uint(TILEMATRIX)) }, }, } @@ -327,6 +327,10 @@ func buildLayers(z uint, rawConfig config.TomlConfig) ([]processing.Layer, func( // Only init gpkg sources once if _, present := sources[rawLayer.DataSource]; !present { path := dataSourceDictionary[rawLayer.DataSource] + if _, err := os.Stat(path); os.IsNotExist(err) { + err = fmt.Errorf("source GeoPackage does not exist: %s", path) + panic(err) + } source := gpkg.MVTSourceGeopackage{} source.Init(path) tables := source.GetTableInfo() @@ -361,22 +365,22 @@ func buildLayers(z uint, rawConfig config.TomlConfig) ([]processing.Layer, func( return layers, closeSources } -// Initialize resources for creating vecotrtiles and delegate to processing. -func runBuildMVTTiles(sourcePath, outDir string) error { - if _, err := os.Stat(sourcePath); os.IsNotExist(err) { - return fmt.Errorf("source GeoPackage does not exist: %s", sourcePath) +// runMVT wires the config, layers and target together to build +// and write the MVT tiles for the requested zoomlevel. +func runMVT(configPath, outDir string, zoomlevel uint) error { + if _, err := os.Stat(configPath); os.IsNotExist(err) { + return fmt.Errorf("config file does not exist: %s", configPath) } - source := gpkg.MVTSourceGeopackage{} - source.Init(sourcePath) - defer source.Close() + rawConfig, err := config.ParseMVTConfig(configPath) + if err != nil { + return err + } - // mvtTarget := gpkg.MVTFileTarget{OutDir: outDir} + layers, closeSources := buildLayers(zoomlevel, rawConfig) + defer closeSources() - tables := source.GetTableInfo() - for _, table := range tables { - source.Table = table - log.Printf(" building MVT tiles for %s", table.Name) - } - return nil + mvtTarget := gpkg.MVTFileTarget{OutDir: outDir} + + return processing.BuildAndWriteMVTTiles(layers, zoomlevel, &mvtTarget) } From 2902a7537302262f469ac187291879e83a3f0d7c Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Fri, 14 Aug 2026 08:10:05 +0200 Subject: [PATCH 6/8] fix: file extension to pbf --- processing/gpkg/gpkgmvt.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/processing/gpkg/gpkgmvt.go b/processing/gpkg/gpkgmvt.go index 8ee22cb..43adbd1 100644 --- a/processing/gpkg/gpkgmvt.go +++ b/processing/gpkg/gpkgmvt.go @@ -250,7 +250,7 @@ func (t *MVTFileTarget) WriteTile(x, y, z uint, data []byte) error { return fmt.Errorf("creating tile directory %s: %w", dir, err) } - path := filepath.Join(dir, strconv.FormatUint(uint64(y), 10)+".mvt") + path := filepath.Join(dir, strconv.FormatUint(uint64(y), 10)+".pbf") if err := os.WriteFile(path, data, 0o644); err != nil { //nolint:gosec // G306 tile output does not need restrictive permissions return fmt.Errorf("writing tile file %s: %w", path, err) } From e027dd4090ffadb60a2572fd7bc12d0d825d0963 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Fri, 14 Aug 2026 10:08:56 +0200 Subject: [PATCH 7/8] refactor: split long buildLayers function --- main.go | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/main.go b/main.go index 12a8cc6..c3d9e8b 100644 --- a/main.go +++ b/main.go @@ -326,31 +326,16 @@ func buildLayers(z uint, rawConfig config.TomlConfig) ([]processing.Layer, func( } // Only init gpkg sources once if _, present := sources[rawLayer.DataSource]; !present { - path := dataSourceDictionary[rawLayer.DataSource] - if _, err := os.Stat(path); os.IsNotExist(err) { - err = fmt.Errorf("source GeoPackage does not exist: %s", path) - panic(err) - } - source := gpkg.MVTSourceGeopackage{} - source.Init(path) - tables := source.GetTableInfo() - tableMap := make(map[string]gpkg.Table, len(tables)) - for _, table := range tables { - tableMap[table.Name] = table - } + source, tableMap := initMvtSource(rawLayer, dataSourceDictionary) sources[rawLayer.DataSource] = initedSource{source, tableMap} } - initedSource, present := sources[rawLayer.DataSource] - if !present { - err := fmt.Errorf("layer %s requires datasource %s; not found", rawLayer.Name, rawLayer.DataSource) - panic(err) - } + initedSource := sources[rawLayer.DataSource] table, present := initedSource.tables[rawLayer.TableName] if !present { err := fmt.Errorf("layer %s requires table %s in datasource %s; not found", rawLayer.Name, rawLayer.TableName, rawLayer.DataSource) panic(err) } - // Create source with this table + // Pair gpkg handle with layer table source := initedSource.source source.Table = table layer := processing.BuildLayer(rawLayer.Name, source) @@ -365,6 +350,26 @@ func buildLayers(z uint, rawConfig config.TomlConfig) ([]processing.Layer, func( return layers, closeSources } +func initMvtSource(rawLayer config.LayerConfig, dataSourceDicationary map[string]string) (gpkg.MVTSourceGeopackage, map[string]gpkg.Table) { + path, present := dataSourceDicationary[rawLayer.DataSource] + if !present { + err := fmt.Errorf("configuration error for layer %s, datasource not found: %s", rawLayer.Name, rawLayer.DataSource) + panic(err) + } + if _, err := os.Stat(path); os.IsNotExist(err) { + err = fmt.Errorf("source Geopackage does not exist: %s", path) + panic(err) + } + source := gpkg.MVTSourceGeopackage{} + source.Init(path) + tables := source.GetTableInfo() + tableMap := make(map[string]gpkg.Table, len(tables)) + for _, table := range tables { + tableMap[table.Name] = table + } + return source, tableMap +} + // runMVT wires the config, layers and target together to build // and write the MVT tiles for the requested zoomlevel. func runMVT(configPath, outDir string, zoomlevel uint) error { From 894dea69da7dc36e09a80cc40e4a9c0fb86a5bbc Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Mon, 17 Aug 2026 10:28:54 +0200 Subject: [PATCH 8/8] feat: logging tile process --- processing/processingmvt.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/processing/processingmvt.go b/processing/processingmvt.go index 3202f2c..c6f4dab 100644 --- a/processing/processingmvt.go +++ b/processing/processingmvt.go @@ -4,6 +4,7 @@ package processing import ( "fmt" + "log" vectorTile "github.com/go-spatial/geom/encoding/mvt/vector_tile" "github.com/pdok/texel/config" @@ -12,14 +13,13 @@ import ( // Orchestrator for the tile creation pipeline func BuildAndWriteMVTTiles(layers []Layer, zoomlevel uint, target MVTTarget) error { - // Build key index (columns) just once. - tiles, err := listTiles(zoomlevel, layers) if err != nil { return err } for _, coord := range tiles { + log.Printf(" building (%d, %d, Z = %d)", coord.X, coord.Y, coord.Z) data, err := processTile(coord, layers) if err != nil { return fmt.Errorf("building tile (%d, %d): %w", coord.X, coord.Y, err)