diff --git a/provider/postgis/error.go b/provider/postgis/error.go index df6e82725..9b3180669 100644 --- a/provider/postgis/error.go +++ b/provider/postgis/error.go @@ -15,3 +15,9 @@ type ErrInvalidSSLMode string func (e ErrInvalidSSLMode) Error() string { return fmt.Sprintf("postgis: invalid ssl mode (%v)", string(e)) } + +type ErrUnclosedToken string + +func (e ErrUnclosedToken) Error() string { + return fmt.Sprintf("postgis: unclosed token in (%v)", string(e)) +} diff --git a/provider/postgis/util.go b/provider/postgis/util.go index 8af03cf4a..a78c6dd4c 100644 --- a/provider/postgis/util.go +++ b/provider/postgis/util.go @@ -6,6 +6,7 @@ import ( "log" "strconv" "strings" + "unicode" "github.com/go-spatial/tegola/basic" "github.com/go-spatial/tegola/provider" @@ -67,14 +68,20 @@ func genSQL(l *Layer, pool *pgx.ConnPool, tblname string, flds []string) (sql st } const ( - bboxToken = "!BBOX!" - zoomToken = "!ZOOM!" + bboxToken = "!BBOX!" + zoomToken = "!ZOOM!" + scaleDenominatorToken = "!SCALE_DENOMINATOR!" + pixelWidthToken = "!PIXEL_WIDTH!" + pixelHeightToken = "!PIXEL_HEIGHT!" ) // replaceTokens replaces tokens in the provided SQL string // // !BBOX! - the bounding box of the tile // !ZOOM! - the tile Z value +// !SCALE_DENOMINATOR! - scale denominator, assuming 90.7 DPI (i.e. 0.28mm pixel size) +// !PIXEL_WIDTH! - the pixel width in m, assuming 256x256 tiles +// !PIXEL_HEIGHT! - the pixel height in m, assuming 256x256 tiles func replaceTokens(sql string, srid uint64, tile provider.Tile) (string, error) { bufferedExtent, _ := tile.BufferedExtent() @@ -95,14 +102,51 @@ func replaceTokens(sql string, srid uint64, tile provider.Tile) (string, error) bbox := fmt.Sprintf("ST_MakeEnvelope(%g,%g,%g,%g,%d)", minPt.X(), minPt.Y(), maxPt.X(), maxPt.Y(), srid) + extent, _ := tile.Extent() + // TODO: Always convert to meter if we support different projections + pixelWidth := (extent.MaxX() - extent.MinX()) / 256 + pixelHeight := (extent.MaxY() - extent.MinY()) / 256 + scaleDenominator := pixelWidth / 0.00028 /* px size in m */ + // replace query string tokens z, _, _ := tile.ZXY() tokenReplacer := strings.NewReplacer( bboxToken, bbox, zoomToken, strconv.FormatUint(uint64(z), 10), + scaleDenominatorToken, strconv.FormatFloat(scaleDenominator, 'f', -1, 64), + pixelWidthToken, strconv.FormatFloat(pixelWidth, 'f', -1, 64), + pixelHeightToken, strconv.FormatFloat(pixelHeight, 'f', -1, 64), ) - return tokenReplacer.Replace(sql), nil + uppercaseTokenSQL, err := uppercaseTokens(sql) + if err != nil { + return "", err + } + + return tokenReplacer.Replace(uppercaseTokenSQL), nil +} + +// uppercaseTokens will sniff for ! chars and uppercase everything between them +// if an odd number of ! are found an error is thrown +func uppercaseTokens(str string) (string, error) { + rs := []rune(str) + + uppercase := false + for i := range rs { + if rs[i] == '!' { + uppercase = !uppercase + continue + } + if uppercase { + rs[i] = unicode.ToUpper(rs[i]) + } + } + + if uppercase { + return str, ErrUnclosedToken(str) + } + + return string(rs), nil } func transformVal(valType pgtype.OID, val interface{}) (interface{}, error) { diff --git a/provider/postgis/util_internal_test.go b/provider/postgis/util_internal_test.go index fa970e59a..55a5decfd 100644 --- a/provider/postgis/util_internal_test.go +++ b/provider/postgis/util_internal_test.go @@ -53,6 +53,12 @@ func TestReplaceTokens(t *testing.T) { tile: slippy.NewTile(16, 11241, 26168, 64, tegola.WebMercator), expected: "SELECT id, scalerank=16 FROM foo WHERE geom && ST_MakeEnvelope(-1.3163688815956049e+07,4.0352540420407774e+06,-1.3163058210472783e+07,4.035884647524042e+06,3857)", }, + "replace pixel_width/height and scale_denominator": { + sql: "SELECT id, !pixel_width! as width, !pixel_height! as height, !scale_denominator! as scale_denom FROM foo WHERE geom && !BBOX!", + srid: tegola.WebMercator, + tile: slippy.NewTile(11, 1070, 676, 64, tegola.WebMercator), + expected: "SELECT id, 76.43702827453626 as width, 76.43702827453671 as height, 272989.3866947724 as scale_denom FROM foo WHERE geom && ST_MakeEnvelope(899816.6968478388,6.789748347570495e+06,919996.0723123164,6.809927723034973e+06,3857)", + }, } for name, tc := range tests { @@ -61,6 +67,56 @@ func TestReplaceTokens(t *testing.T) { } } +func TestUppercaseTokens(t *testing.T) { + type tcase struct { + str string + expected string + expectedErr error + } + + fn := func(tc tcase) func(t *testing.T) { + return func(t *testing.T) { + out, err := uppercaseTokens(tc.str) + if err != nil { + if tc.expectedErr.Error() != err.Error() { + t.Errorf("unexpected error, expected %v got %v", tc.expectedErr, err) + return + } + + return + } + + if out != tc.expected { + t.Errorf("expected \n \t%v\n out \n \t%v", tc.expected, out) + return + } + } + } + + tests := map[string]tcase{ + "uppercase tokens": { + str: "this !lower! case !STrInG! should uppercase !TOKENS!", + expected: "this !LOWER! case !STRING! should uppercase !TOKENS!", + }, + "no tokens": { + str: "no token", + expected: "no token", + }, + "empty string": { + str: "", + expected: "", + }, + "unclosed token": { + str: "unclosed !token", + expectedErr: ErrUnclosedToken("unclosed !token"), + }, + } + + for name, tc := range tests { + t.Run(name, fn(tc)) + } +} + func TestDecipherFields(t *testing.T) { ttools.ShouldSkip(t, TESTENV)