Skip to content

Commit

Permalink
Added support for !ZOOM! token in GPKG provider. (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
JivanAmara committed Dec 19, 2017
1 parent 7e20669 commit 975eb0c
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 10 deletions.
44 changes: 35 additions & 9 deletions provider/gpkg/gpkgProvider.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,26 @@ func (p *GPKGProvider) Layers() ([]mvt.LayerInfo, error) {
return ls, nil
}

func replaceTokens(qtext string) string {
func replaceTokens(qtext string) (ttext string, tokensPresent map[string]bool) {
// --- Convert tokens provided to SQL
// The BBOX token requires parameters ordered as [maxx, minx, maxy, miny] and checks for overlap.
// Until support for named parameters, we'll only support one BBOX token per query.
ttext := strings.Replace(qtext, "!BBOX!", "minx <= ? AND maxx >= ? AND miny <= ? AND maxy >= ?", 1)
return ttext
// Until support for named parameters, we'll only support one BBOX token per query.
// The ZOOM token requires two parameters, both filled with the current zoom level.
// Until support for named parameters, the ZOOM token must follow the BBOX token.
ttext = string(qtext)
tokensPresent = make(map[string]bool)

if strings.Count(ttext, "!BBOX!") > 0 {
tokensPresent["BBOX"] = true
ttext = strings.Replace(qtext, "!BBOX!", "minx <= ? AND maxx >= ? AND miny <= ? AND maxy >= ?", 1)
}

if strings.Count(ttext, "!ZOOM!") > 0 {
tokensPresent["ZOOM"] = true
ttext = strings.Replace(ttext, "!ZOOM!", "min_zoom <= ? AND max_zoom >= ?", 1)
}

return ttext, tokensPresent
}

func layerFromQuery(ctx context.Context, pLayer *GPKGLayer, rows *sql.Rows, rowCount *int, dtags map[string]interface{}) (
Expand Down Expand Up @@ -165,8 +179,8 @@ func layerFromQuery(ctx context.Context, pLayer *GPKGLayer, rows *sql.Rows, rowC
} else {
log.Info("SRID already default (%v), no conversion necessary", DefaultSRID)
}
case "minx", "miny", "maxx", "maxy":
// Skip these columns used for bounding box filtering
case "minx", "miny", "maxx", "maxy", "min_zoom", "max_zoom":
// Skip these columns used for bounding box and zoom filtering
continue
default:
// Grab any non-nil, non-id, non-bounding box, & non-geometry column as a tag
Expand Down Expand Up @@ -249,6 +263,7 @@ func (p *GPKGProvider) MVTLayer(ctx context.Context, layerName string, tile tego
geomFieldname := p.layers[layerName].geomFieldname
idFieldname := p.layers[layerName].idFieldname

var tokensPresent map[string]bool
if p.layers[layerName].tablename != "" {
// If layer was specified via "tablename" in config, construct query.
geomTablename := p.layers[layerName].tablename
Expand All @@ -260,13 +275,18 @@ func (p *GPKGProvider) MVTLayer(ctx context.Context, layerName string, tile tego
}
qtext = fmt.Sprintf("%v FROM %v l JOIN %v si ON l.%v = si.id WHERE geom IS NOT NULL AND !BBOX!",
selectClause, geomTablename, rtreeTablename, idFieldname)
qtext = replaceTokens(qtext)
qtext, tokensPresent = replaceTokens(qtext)
} else {
// If layer was specified via "sql" in config, collect it.
qtext = p.layers[layerName].sql
qtext, tokensPresent = replaceTokens(qtext)
}

qparams := []interface{}{tileBBox[2], tileBBox[0], tileBBox[3], tileBBox[1]}
if tokensPresent["ZOOM"] {
// Add the zoom level, once for comparison to min, once for max.
qparams = append(qparams, tile.ZLevel(), tile.ZLevel())
}
log.Debug("qtext: %v\nqparams: %v\n", qtext, qparams)
rows, err := db.Query(qtext, qparams...)
if err != nil {
Expand Down Expand Up @@ -448,13 +468,19 @@ func NewProvider(config map[string]interface{}) (mvt.Provider, error) {
}
} else {
// --- Layer from custom sql
customSql := replaceTokens(layerConfig["sql"].(string))
customSqlTemplate := layerConfig["sql"].(string)
customSql, tokensPresent := replaceTokens(customSqlTemplate)

// Get geometry type & srid from geometry of first row.
qtext := fmt.Sprintf("SELECT geom FROM (%v) LIMIT 1;", customSql)
var geomData []byte
// Set bounds & zoom params to include all layers
// Bounds checks need params: maxx, minx, maxy, miny
qparams := []interface{}{float64(180.0), float64(-180.0), float64(85.0511), float64(-85.0511)}
if tokensPresent["ZOOM"] {
// min_zoom will always be less than 100, and max_zoom will always be greater than 0.
qparams = append(qparams, 100, 0)
}
log.Debug("qtext: %v, params: %v", qtext, qparams)
row := db.QueryRow(qtext, qparams...)
err = row.Scan(&geomData)
Expand All @@ -476,7 +502,7 @@ func NewProvider(config map[string]interface{}) (mvt.Provider, error) {

l = GPKGLayer{
name: layerName,
sql: customSql,
sql: customSqlTemplate,
srid: int(h.SRSId()),
geomType: geom,
geomFieldname: "geom",
Expand Down
79 changes: 78 additions & 1 deletion provider/gpkg/gpkgProvider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ import (
var filePath string
var directory string
var GPKGFilePath string
var GPKGNaturalEarthFilePath string

func init() {
_, filePath, _, _ = runtime.Caller(0)
directory, _ = filepath.Split(filePath)
GPKGFilePath = directory + "test_data/athens-osm-20170921.gpkg"
// This gpkg has zoom level information
GPKGNaturalEarthFilePath = directory + "test_data/natural_earth_minimal.gpkg"
}

func TestNewGPKGProvider(t *testing.T) {
Expand All @@ -44,14 +47,19 @@ func TestNewGPKGProvider(t *testing.T) {

type MockTile struct {
tegola.TegolaTile
bbox [4]float64
bbox [4]float64
zlevel int
}

func (tile *MockTile) BoundingBox() tegola.BoundingBox {
bb := tegola.BoundingBox{Minx: tile.bbox[0], Miny: tile.bbox[1], Maxx: tile.bbox[2], Maxy: tile.bbox[3]}
return bb
}

func (tile *MockTile) ZLevel() int {
return tile.zlevel
}

func TestMVTLayerFiltering(t *testing.T) {
layers := []map[string]interface{}{
{"name": "rl_lines", "tablename": "rail_lines"},
Expand Down Expand Up @@ -329,3 +337,72 @@ func TestConfigSQL(t *testing.T) {
}
}
}

func TestConfigZOOM(t *testing.T) {
// Checks the proper functioning of a "fields" config variable which specifies which
// columns of a table should be converted to tags beyond the defaults.

// --- Get provider with sql specified for layers in config.
layers := []map[string]interface{}{
{"name": "land1",
"sql": "SELECT fid, geom, featurecla, min_zoom, 22 as max_zoom, minx, miny, maxx, maxy " +
"FROM ne_110m_land t JOIN rtree_ne_110m_land_geom si ON t.fid = si.id " +
"WHERE !BBOX! AND !ZOOM!",
},
{"name": "land2",
"sql": "SELECT fid, geom, featurecla, min_zoom, 22 as max_zoom, minx, miny, maxx, maxy " +
"FROM ne_110m_land t JOIN rtree_ne_110m_land_geom si ON t.fid = si.id " +
"WHERE !BBOX! AND !ZOOM!",
},
}

config := map[string]interface{}{
"FilePath": GPKGNaturalEarthFilePath,
"layers": layers,
}
p, err := NewProvider(config)
if err != nil {
fmt.Printf("Error creating provider: %v\n", err)
t.FailNow()
}

// --- Check that features are populated
ctx := context.TODO()
// TODO: There's some confusion between pixel coordinates & WebMercator positions in the tile
// bounding box, making the smallest y-value in pos #4 instead of pos #2
// At some point, clean up this problem: https://github.com/terranodo/tegola/issues/189
pixelExtentEntireWorld := [4]float64{-20026376.39, 20048966.10, 20026376.39, -20048966.10}
mt := &MockTile{bbox: pixelExtentEntireWorld}
tags := make(map[string]interface{})

type TagLookupByFeatureId map[uint64]map[string]interface{}
type TestCase struct {
lName string
zlevel int
expectedFeatureCount int
}

testCases := []TestCase{
{
lName: "land1",
zlevel: 1,
expectedFeatureCount: 101,
},
{
lName: "land2",
zlevel: 0,
expectedFeatureCount: 44,
},
}

for i, tc := range testCases {
mt.zlevel = tc.zlevel
l, err := p.MVTLayer(ctx, tc.lName, mt, tags)
if err != nil {
t.Errorf("TestCase[%v]: Error in call to p.MVTLayer(%v): %v\n", i, tc.lName, err)
}

assert.Equal(t, tc.expectedFeatureCount, len(l.Features()))
}

}
Binary file not shown.
6 changes: 6 additions & 0 deletions tile.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type TegolaTile interface {
BoundingBox() BoundingBox
ZRes() float64
ZEpislon() float64
ZLevel() int
}

//Tile slippy map tilenames
Expand Down Expand Up @@ -68,6 +69,11 @@ func (t *Tile) BoundingBox() BoundingBox {
}
}

// Returns web mercator zoom level
func (t *Tile) ZLevel() int {
return t.Z
}

//ZRes takes a web mercator zoom level and returns the pixel resolution for that
// scale, assuming 256x256 pixel tiles. Non-integer zoom levels are accepted.
// ported from: https://raw.githubusercontent.com/mapbox/postgis-vt-util/master/postgis-vt-util.sql
Expand Down

0 comments on commit 975eb0c

Please sign in to comment.