From 63900439d1a10f7f415e0bdd46e21560b3df5b61 Mon Sep 17 00:00:00 2001 From: Andy Yang Date: Sat, 30 Jan 2021 16:03:36 -0500 Subject: [PATCH] geo/wkt: implement parser for points with Z and M dimensions This patch adds a parser that is capable of parsing WKT representations of points with Z and M dimensions. Release note: None --- Makefile | 5 +- build/variables.mk | 1 + pkg/geo/wkt/.gitignore | 1 + pkg/geo/wkt/BUILD.bazel | 23 ++ pkg/geo/wkt/lex.go | 195 +++++++++++++ pkg/geo/wkt/wkt.go | 28 ++ pkg/geo/wkt/wkt.y | 104 +++++++ pkg/geo/wkt/wkt_generated.go | 541 +++++++++++++++++++++++++++++++++++ pkg/geo/wkt/wkt_test.go | 114 ++++++++ 9 files changed, 1011 insertions(+), 1 deletion(-) create mode 100644 pkg/geo/wkt/.gitignore create mode 100644 pkg/geo/wkt/BUILD.bazel create mode 100644 pkg/geo/wkt/lex.go create mode 100644 pkg/geo/wkt/wkt.go create mode 100644 pkg/geo/wkt/wkt.y create mode 100644 pkg/geo/wkt/wkt_generated.go create mode 100644 pkg/geo/wkt/wkt_test.go diff --git a/Makefile b/Makefile index 22e63ccd2ce7..a3190193fb27 100644 --- a/Makefile +++ b/Makefile @@ -816,6 +816,9 @@ SQLPARSER_TARGETS = \ pkg/sql/lex/keywords.go \ pkg/sql/lexbase/reserved_keywords.go +WKTPARSER_TARGETS = \ + pkg/geo/wkt/wkt.go + PROTOBUF_TARGETS := bin/.go_protobuf_sources bin/.gw_protobuf_sources DOCGEN_TARGETS := \ @@ -1135,7 +1138,7 @@ dupl: bin/.bootstrap .PHONY: generate generate: ## Regenerate generated code. -generate: protobuf $(DOCGEN_TARGETS) $(OPTGEN_TARGETS) $(LOG_TARGETS) $(SQLPARSER_TARGETS) $(SETTINGS_DOC_PAGE) bin/langgen bin/terraformgen +generate: protobuf $(DOCGEN_TARGETS) $(OPTGEN_TARGETS) $(LOG_TARGETS) $(SQLPARSER_TARGETS) $(WKTPARSER_TARGETS) $(SETTINGS_DOC_PAGE) bin/langgen bin/terraformgen $(GO) generate $(GOFLAGS) $(GOMODVENDORFLAGS) -tags '$(TAGS)' -ldflags '$(LINKFLAGS)' $(PKG) $(MAKE) execgen diff --git a/build/variables.mk b/build/variables.mk index 3c5cae048203..4132d7c81c12 100644 --- a/build/variables.mk +++ b/build/variables.mk @@ -154,6 +154,7 @@ define VALID_VARS WEBPACK WEBPACK_DASHBOARD WEBPACK_DEV_SERVER + WKTPARSER_TARGETS XCC XCMAKE_SYSTEM_NAME XCXX diff --git a/pkg/geo/wkt/.gitignore b/pkg/geo/wkt/.gitignore new file mode 100644 index 000000000000..26520536f945 --- /dev/null +++ b/pkg/geo/wkt/.gitignore @@ -0,0 +1 @@ +y.output diff --git a/pkg/geo/wkt/BUILD.bazel b/pkg/geo/wkt/BUILD.bazel new file mode 100644 index 000000000000..f5723cd57d52 --- /dev/null +++ b/pkg/geo/wkt/BUILD.bazel @@ -0,0 +1,23 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "wkt", + srcs = [ + "lex.go", + "wkt.go", + "wkt_generated.go", + ], + importpath = "github.com/cockroachdb/cockroach/pkg/geo/wkt", + visibility = ["//visibility:public"], + deps = ["@com_github_twpayne_go_geom//:go-geom"], +) + +go_test( + name = "wkt_test", + srcs = ["wkt_test.go"], + embed = [":wkt"], + deps = [ + "@com_github_stretchr_testify//require", + "@com_github_twpayne_go_geom//:go-geom", + ], +) diff --git a/pkg/geo/wkt/lex.go b/pkg/geo/wkt/lex.go new file mode 100644 index 000000000000..1071feb9f0b5 --- /dev/null +++ b/pkg/geo/wkt/lex.go @@ -0,0 +1,195 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package wkt + +import ( + "fmt" + "strconv" + "strings" + "unicode" + + "github.com/twpayne/go-geom" +) + +// LexError is an error that occurs during lexing. +type LexError struct { + problem string + pos int +} + +func (e *LexError) Error() string { + return fmt.Sprintf("lex error: %s at pos %d", e.problem, e.pos) +} + +// ParseError is an error that occurs during parsing, which happens after lexing. +type ParseError struct { + line string +} + +func (e *ParseError) Error() string { + return fmt.Sprintf("parse error: could not parse %q", e.line) +} + +// Constant expected by parser when lexer reaches EOF. +const eof = 0 + +type wktLex struct { + line string + pos int + ret geom.T + lastErr error +} + +// Lex lexes a token from the input. +func (l *wktLex) Lex(yylval *wktSymType) int { + // Skip leading spaces. + l.trimLeft() + + // Lex a token. + switch c := l.peek(); c { + case eof: + return eof + case '(', ')', ',': + return int(l.next()) + default: + if unicode.IsLetter(c) { + return l.keyword() + } else if isNumRune(c) { + return l.num(yylval) + } else { + l.lastErr = &LexError{ + problem: "unrecognized character", + pos: l.pos, + } + return eof + } + } +} + +func getKeywordToken(tokStr string) int { + switch tokStr { + case "EMPTY": + return EMPTY + case "POINT": + return POINT + case "POINTZ": + return POINTZ + case "POINTM": + return POINTM + case "POINTZM": + return POINTZM + default: + return eof + } +} + +// keyword lexes a string keyword. +func (l *wktLex) keyword() int { + startPos := l.pos + var b strings.Builder + + for { + c := l.peek() + if !unicode.IsLetter(c) { + break + } + // Add the uppercase letter to the string builder. + b.WriteRune(unicode.ToUpper(l.next())) + } + + // Check for extra dimensions for geometry types. + if b.String() != "EMPTY" { + l.trimLeft() + if unicode.ToUpper(l.peek()) == 'Z' { + l.next() + b.WriteRune('Z') + } + if unicode.ToUpper(l.peek()) == 'M' { + l.next() + b.WriteRune('M') + } + } + + ret := getKeywordToken(b.String()) + if ret == eof { + l.lastErr = &LexError{ + problem: "invalid keyword", + pos: startPos, + } + } + + return ret +} + +func isNumRune(r rune) bool { + switch r { + case '-', '.': + return true + default: + return unicode.IsDigit(r) + } +} + +// num lexes a number. +func (l *wktLex) num(yylval *wktSymType) int { + startPos := l.pos + var b strings.Builder + + for { + c := l.peek() + if !isNumRune(c) { + break + } + b.WriteRune(l.next()) + } + + fl, err := strconv.ParseFloat(b.String(), 64) + if err != nil { + l.lastErr = &LexError{ + problem: "invalid number", + pos: startPos, + } + return eof + } + yylval.coord = fl + return NUM +} + +func (l *wktLex) peek() rune { + if l.pos == len(l.line) { + return eof + } + return rune(l.line[l.pos]) +} + +func (l *wktLex) next() rune { + c := l.peek() + if c != eof { + l.pos++ + } + return c +} + +func (l *wktLex) trimLeft() { + for { + c := l.peek() + if c == eof || !unicode.IsSpace(c) { + break + } + l.next() + } +} + +func (l *wktLex) Error(s string) { + // Lex errors are set in the Lex function. + // todo (ayang) improve parse error messages + /* EMPTY */ +} diff --git a/pkg/geo/wkt/wkt.go b/pkg/geo/wkt/wkt.go new file mode 100644 index 000000000000..f1b5ea606dd8 --- /dev/null +++ b/pkg/geo/wkt/wkt.go @@ -0,0 +1,28 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +//go:generate goyacc -o wkt_generated.go -p "wkt" wkt.y + +package wkt + +import "github.com/twpayne/go-geom" + +// Unmarshal accepts a string and parses it to a geom.T. +func Unmarshal(wkt string) (geom.T, error) { + wktlex := &wktLex{line: wkt} + ret := wktParse(wktlex) + if wktlex.lastErr != nil { + return nil, wktlex.lastErr + } + if ret != 0 { + return nil, &ParseError{line: wkt} + } + return wktlex.ret, nil +} diff --git a/pkg/geo/wkt/wkt.y b/pkg/geo/wkt/wkt.y new file mode 100644 index 000000000000..0ee975c3ce70 --- /dev/null +++ b/pkg/geo/wkt/wkt.y @@ -0,0 +1,104 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +%{ + +package wkt + +import "github.com/twpayne/go-geom" + +%} + +%union { + str string + geom geom.T + coord float64 + coordList []float64 +} + +%token POINT POINTZ POINTM POINTZM +%token EMPTY +//%token LINESTRING POLYGON MULTIPOINT MULTILINESTRING MULTIPOLYGON GEOMETRYCOLLECTION +%token NUM + +%type geometry +%type point +%type two_coords three_coords four_coords + +%% + +start: + geometry + { + wktlex.(*wktLex).ret = $1 + } + +geometry: + point + +point: + POINT two_coords + { + $$ = geom.NewPointFlat(geom.XY, $2) + } +| POINT three_coords + { + $$ = geom.NewPointFlat(geom.XYZ, $2) + } +| POINT four_coords + { + $$ = geom.NewPointFlat(geom.XYZM, $2) + } +| POINTZ three_coords + { + $$ = geom.NewPointFlat(geom.XYZ, $2) + } +| POINTM three_coords + { + $$ = geom.NewPointFlat(geom.XYM, $2) + } +| POINTZM four_coords + { + $$ = geom.NewPointFlat(geom.XYZM, $2) + } +| POINT EMPTY + { + $$ = geom.NewPointEmpty(geom.XY) + } +| POINTZ EMPTY + { + $$ = geom.NewPointEmpty(geom.XYZ) + } +| POINTM EMPTY + { + $$ = geom.NewPointEmpty(geom.XYM) + } +| POINTZM EMPTY + { + $$ = geom.NewPointEmpty(geom.XYZM) + } + +two_coords: + '(' NUM NUM ')' + { + $$ = []float64{$2, $3} + } + +three_coords: + '(' NUM NUM NUM ')' + { + $$ = []float64{$2, $3, $4} + } + +four_coords: + '(' NUM NUM NUM NUM ')' + { + $$ = []float64{$2, $3, $4, $5} + } diff --git a/pkg/geo/wkt/wkt_generated.go b/pkg/geo/wkt/wkt_generated.go new file mode 100644 index 000000000000..75888d343a63 --- /dev/null +++ b/pkg/geo/wkt/wkt_generated.go @@ -0,0 +1,541 @@ +// Code generated by goyacc -o wkt_generated.go -p wkt wkt.y. DO NOT EDIT. + +//line wkt.y:12 + +package wkt + +import __yyfmt__ "fmt" + +//line wkt.y:13 + +import "github.com/twpayne/go-geom" + +//line wkt.y:19 +type wktSymType struct { + yys int + str string + geom geom.T + coord float64 + coordList []float64 +} + +const POINT = 57346 +const POINTZ = 57347 +const POINTM = 57348 +const POINTZM = 57349 +const EMPTY = 57350 +const NUM = 57351 + +var wktToknames = [...]string{ + "$end", + "error", + "$unk", + "POINT", + "POINTZ", + "POINTM", + "POINTZM", + "EMPTY", + "NUM", + "'('", + "')'", +} + +var wktStatenames = [...]string{} + +const wktEofCode = 1 +const wktErrCode = 2 +const wktInitialStackSize = 16 + +//line yacctab:1 +var wktExca = [...]int{ + -1, 1, + 1, -1, + -2, 0, +} + +const wktPrivate = 57344 + +const wktLast = 39 + +var wktAct = [...]int{ + 32, 28, 31, 27, 33, 31, 19, 17, 20, 15, + 14, 11, 15, 12, 4, 5, 6, 7, 32, 1, + 30, 29, 26, 25, 24, 23, 22, 21, 10, 8, + 3, 9, 2, 0, 0, 0, 18, 13, 16, +} + +var wktPact = [...]int{ + 10, -1000, -1000, -1000, 3, 2, -1, -2, -1000, -1000, + -1000, -1000, 18, -1000, -1000, 17, -1000, -1000, -1000, -1000, + 16, 15, 14, 13, -8, 12, 11, -1000, -9, -6, + 9, -1000, -7, -1000, +} + +var wktPgo = [...]int{ + 0, 32, 30, 29, 31, 28, 19, +} + +var wktR1 = [...]int{ + 0, 6, 1, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 3, 4, 5, +} + +var wktR2 = [...]int{ + 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 4, 5, 6, +} + +var wktChk = [...]int{ + -1000, -6, -1, -2, 4, 5, 6, 7, -3, -4, + -5, 8, 10, -4, 8, 10, -4, 8, -5, 8, + 10, 9, 9, 9, 9, 9, 9, 11, 9, 9, + 9, 11, 9, 11, +} + +var wktDef = [...]int{ + 0, -2, 1, 2, 0, 0, 0, 0, 3, 4, + 5, 9, 0, 6, 10, 0, 7, 11, 8, 12, + 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, + 0, 14, 0, 15, +} + +var wktTok1 = [...]int{ + 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 10, 11, +} + +var wktTok2 = [...]int{ + 2, 3, 4, 5, 6, 7, 8, 9, +} + +var wktTok3 = [...]int{ + 0, +} + +var wktErrorMessages = [...]struct { + state int + token int + msg string +}{} + +//line yaccpar:1 + +/* parser for yacc output */ + +var ( + wktDebug = 0 + wktErrorVerbose = false +) + +type wktLexer interface { + Lex(lval *wktSymType) int + Error(s string) +} + +type wktParser interface { + Parse(wktLexer) int + Lookahead() int +} + +type wktParserImpl struct { + lval wktSymType + stack [wktInitialStackSize]wktSymType + char int +} + +func (p *wktParserImpl) Lookahead() int { + return p.char +} + +func wktNewParser() wktParser { + return &wktParserImpl{} +} + +const wktFlag = -1000 + +func wktTokname(c int) string { + if c >= 1 && c-1 < len(wktToknames) { + if wktToknames[c-1] != "" { + return wktToknames[c-1] + } + } + return __yyfmt__.Sprintf("tok-%v", c) +} + +func wktStatname(s int) string { + if s >= 0 && s < len(wktStatenames) { + if wktStatenames[s] != "" { + return wktStatenames[s] + } + } + return __yyfmt__.Sprintf("state-%v", s) +} + +func wktErrorMessage(state, lookAhead int) string { + const TOKSTART = 4 + + if !wktErrorVerbose { + return "syntax error" + } + + for _, e := range wktErrorMessages { + if e.state == state && e.token == lookAhead { + return "syntax error: " + e.msg + } + } + + res := "syntax error: unexpected " + wktTokname(lookAhead) + + // To match Bison, suggest at most four expected tokens. + expected := make([]int, 0, 4) + + // Look for shiftable tokens. + base := wktPact[state] + for tok := TOKSTART; tok-1 < len(wktToknames); tok++ { + if n := base + tok; n >= 0 && n < wktLast && wktChk[wktAct[n]] == tok { + if len(expected) == cap(expected) { + return res + } + expected = append(expected, tok) + } + } + + if wktDef[state] == -2 { + i := 0 + for wktExca[i] != -1 || wktExca[i+1] != state { + i += 2 + } + + // Look for tokens that we accept or reduce. + for i += 2; wktExca[i] >= 0; i += 2 { + tok := wktExca[i] + if tok < TOKSTART || wktExca[i+1] == 0 { + continue + } + if len(expected) == cap(expected) { + return res + } + expected = append(expected, tok) + } + + // If the default action is to accept or reduce, give up. + if wktExca[i+1] != 0 { + return res + } + } + + for i, tok := range expected { + if i == 0 { + res += ", expecting " + } else { + res += " or " + } + res += wktTokname(tok) + } + return res +} + +func wktlex1(lex wktLexer, lval *wktSymType) (char, token int) { + token = 0 + char = lex.Lex(lval) + if char <= 0 { + token = wktTok1[0] + goto out + } + if char < len(wktTok1) { + token = wktTok1[char] + goto out + } + if char >= wktPrivate { + if char < wktPrivate+len(wktTok2) { + token = wktTok2[char-wktPrivate] + goto out + } + } + for i := 0; i < len(wktTok3); i += 2 { + token = wktTok3[i+0] + if token == char { + token = wktTok3[i+1] + goto out + } + } + +out: + if token == 0 { + token = wktTok2[1] /* unknown char */ + } + if wktDebug >= 3 { + __yyfmt__.Printf("lex %s(%d)\n", wktTokname(token), uint(char)) + } + return char, token +} + +func wktParse(wktlex wktLexer) int { + return wktNewParser().Parse(wktlex) +} + +func (wktrcvr *wktParserImpl) Parse(wktlex wktLexer) int { + var wktn int + var wktVAL wktSymType + var wktDollar []wktSymType + _ = wktDollar // silence set and not used + wktS := wktrcvr.stack[:] + + Nerrs := 0 /* number of errors */ + Errflag := 0 /* error recovery flag */ + wktstate := 0 + wktrcvr.char = -1 + wkttoken := -1 // wktrcvr.char translated into internal numbering + defer func() { + // Make sure we report no lookahead when not parsing. + wktstate = -1 + wktrcvr.char = -1 + wkttoken = -1 + }() + wktp := -1 + goto wktstack + +ret0: + return 0 + +ret1: + return 1 + +wktstack: + /* put a state and value onto the stack */ + if wktDebug >= 4 { + __yyfmt__.Printf("char %v in %v\n", wktTokname(wkttoken), wktStatname(wktstate)) + } + + wktp++ + if wktp >= len(wktS) { + nyys := make([]wktSymType, len(wktS)*2) + copy(nyys, wktS) + wktS = nyys + } + wktS[wktp] = wktVAL + wktS[wktp].yys = wktstate + +wktnewstate: + wktn = wktPact[wktstate] + if wktn <= wktFlag { + goto wktdefault /* simple state */ + } + if wktrcvr.char < 0 { + wktrcvr.char, wkttoken = wktlex1(wktlex, &wktrcvr.lval) + } + wktn += wkttoken + if wktn < 0 || wktn >= wktLast { + goto wktdefault + } + wktn = wktAct[wktn] + if wktChk[wktn] == wkttoken { /* valid shift */ + wktrcvr.char = -1 + wkttoken = -1 + wktVAL = wktrcvr.lval + wktstate = wktn + if Errflag > 0 { + Errflag-- + } + goto wktstack + } + +wktdefault: + /* default state action */ + wktn = wktDef[wktstate] + if wktn == -2 { + if wktrcvr.char < 0 { + wktrcvr.char, wkttoken = wktlex1(wktlex, &wktrcvr.lval) + } + + /* look through exception table */ + xi := 0 + for { + if wktExca[xi+0] == -1 && wktExca[xi+1] == wktstate { + break + } + xi += 2 + } + for xi += 2; ; xi += 2 { + wktn = wktExca[xi+0] + if wktn < 0 || wktn == wkttoken { + break + } + } + wktn = wktExca[xi+1] + if wktn < 0 { + goto ret0 + } + } + if wktn == 0 { + /* error ... attempt to resume parsing */ + switch Errflag { + case 0: /* brand new error */ + wktlex.Error(wktErrorMessage(wktstate, wkttoken)) + Nerrs++ + if wktDebug >= 1 { + __yyfmt__.Printf("%s", wktStatname(wktstate)) + __yyfmt__.Printf(" saw %s\n", wktTokname(wkttoken)) + } + fallthrough + + case 1, 2: /* incompletely recovered error ... try again */ + Errflag = 3 + + /* find a state where "error" is a legal shift action */ + for wktp >= 0 { + wktn = wktPact[wktS[wktp].yys] + wktErrCode + if wktn >= 0 && wktn < wktLast { + wktstate = wktAct[wktn] /* simulate a shift of "error" */ + if wktChk[wktstate] == wktErrCode { + goto wktstack + } + } + + /* the current p has no shift on "error", pop stack */ + if wktDebug >= 2 { + __yyfmt__.Printf("error recovery pops state %d\n", wktS[wktp].yys) + } + wktp-- + } + /* there is no state on the stack with an error shift ... abort */ + goto ret1 + + case 3: /* no shift yet; clobber input char */ + if wktDebug >= 2 { + __yyfmt__.Printf("error recovery discards %s\n", wktTokname(wkttoken)) + } + if wkttoken == wktEofCode { + goto ret1 + } + wktrcvr.char = -1 + wkttoken = -1 + goto wktnewstate /* try again in the same state */ + } + } + + /* reduction by production wktn */ + if wktDebug >= 2 { + __yyfmt__.Printf("reduce %v in:\n\t%v\n", wktn, wktStatname(wktstate)) + } + + wktnt := wktn + wktpt := wktp + _ = wktpt // guard against "declared and not used" + + wktp -= wktR2[wktn] + // wktp is now the index of $0. Perform the default action. Iff the + // reduced production is ε, $1 is possibly out of range. + if wktp+1 >= len(wktS) { + nyys := make([]wktSymType, len(wktS)*2) + copy(nyys, wktS) + wktS = nyys + } + wktVAL = wktS[wktp+1] + + /* consult goto table to find next state */ + wktn = wktR1[wktn] + wktg := wktPgo[wktn] + wktj := wktg + wktS[wktp].yys + 1 + + if wktj >= wktLast { + wktstate = wktAct[wktg] + } else { + wktstate = wktAct[wktj] + if wktChk[wktstate] != -wktn { + wktstate = wktAct[wktg] + } + } + // dummy call; replaced with literal code + switch wktnt { + + case 1: + wktDollar = wktS[wktpt-1 : wktpt+1] +//line wkt.y:39 + { + wktlex.(*wktLex).ret = wktDollar[1].geom + } + case 3: + wktDollar = wktS[wktpt-2 : wktpt+1] +//line wkt.y:48 + { + wktVAL.geom = geom.NewPointFlat(geom.XY, wktDollar[2].coordList) + } + case 4: + wktDollar = wktS[wktpt-2 : wktpt+1] +//line wkt.y:52 + { + wktVAL.geom = geom.NewPointFlat(geom.XYZ, wktDollar[2].coordList) + } + case 5: + wktDollar = wktS[wktpt-2 : wktpt+1] +//line wkt.y:56 + { + wktVAL.geom = geom.NewPointFlat(geom.XYZM, wktDollar[2].coordList) + } + case 6: + wktDollar = wktS[wktpt-2 : wktpt+1] +//line wkt.y:60 + { + wktVAL.geom = geom.NewPointFlat(geom.XYZ, wktDollar[2].coordList) + } + case 7: + wktDollar = wktS[wktpt-2 : wktpt+1] +//line wkt.y:64 + { + wktVAL.geom = geom.NewPointFlat(geom.XYM, wktDollar[2].coordList) + } + case 8: + wktDollar = wktS[wktpt-2 : wktpt+1] +//line wkt.y:68 + { + wktVAL.geom = geom.NewPointFlat(geom.XYZM, wktDollar[2].coordList) + } + case 9: + wktDollar = wktS[wktpt-2 : wktpt+1] +//line wkt.y:72 + { + wktVAL.geom = geom.NewPointEmpty(geom.XY) + } + case 10: + wktDollar = wktS[wktpt-2 : wktpt+1] +//line wkt.y:76 + { + wktVAL.geom = geom.NewPointEmpty(geom.XYZ) + } + case 11: + wktDollar = wktS[wktpt-2 : wktpt+1] +//line wkt.y:80 + { + wktVAL.geom = geom.NewPointEmpty(geom.XYM) + } + case 12: + wktDollar = wktS[wktpt-2 : wktpt+1] +//line wkt.y:84 + { + wktVAL.geom = geom.NewPointEmpty(geom.XYZM) + } + case 13: + wktDollar = wktS[wktpt-4 : wktpt+1] +//line wkt.y:90 + { + wktVAL.coordList = []float64{wktDollar[2].coord, wktDollar[3].coord} + } + case 14: + wktDollar = wktS[wktpt-5 : wktpt+1] +//line wkt.y:96 + { + wktVAL.coordList = []float64{wktDollar[2].coord, wktDollar[3].coord, wktDollar[4].coord} + } + case 15: + wktDollar = wktS[wktpt-6 : wktpt+1] +//line wkt.y:102 + { + wktVAL.coordList = []float64{wktDollar[2].coord, wktDollar[3].coord, wktDollar[4].coord, wktDollar[5].coord} + } + } + goto wktstack /* stack new state and value */ +} diff --git a/pkg/geo/wkt/wkt_test.go b/pkg/geo/wkt/wkt_test.go new file mode 100644 index 000000000000..707ef6811778 --- /dev/null +++ b/pkg/geo/wkt/wkt_test.go @@ -0,0 +1,114 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package wkt + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/twpayne/go-geom" +) + +func TestUnmarshal(t *testing.T) { + testCases := []struct { + desc string + equivInputs []string + expected geom.T + }{ + { + desc: "parse 2D point", + equivInputs: []string{"POINT(0 1)", "POINT (0 1)", "point(0 1)", "point ( 0 1 )"}, + expected: geom.NewPointFlat(geom.XY, []float64{0, 1}), + }, + { + desc: "parse 3D point", + equivInputs: []string{"POINT Z (2 3 4)", "POINTZ(2 3 4)", "POINT(2 3 4)"}, + expected: geom.NewPointFlat(geom.XYZ, []float64{2, 3, 4}), + }, + { + desc: "parse 2D+M point", + equivInputs: []string{"POINT M (-2 0 0.5)", "POINTM(-2 0 0.5)", "POINTM(-2 0 .5)"}, + expected: geom.NewPointFlat(geom.XYM, []float64{-2, 0, 0.5}), + }, + { + desc: "parse 4D point", + equivInputs: []string{"POINT ZM (0 5 -10 15)", "POINTZM (0 5 -10 15)", "POINT(0 5 -10 15)"}, + expected: geom.NewPointFlat(geom.XYZM, []float64{0, 5, -10, 15}), + }, + { + desc: "parse empty 2D point", + equivInputs: []string{"POINT EMPTY"}, + expected: geom.NewPointEmpty(geom.XY), + }, + { + desc: "parse empty 3D point", + equivInputs: []string{"POINT Z EMPTY", "POINTZ EMPTY"}, + expected: geom.NewPointEmpty(geom.XYZ), + }, + { + desc: "parse empty 2D+M point", + equivInputs: []string{"POINT M EMPTY", "POINTM EMPTY"}, + expected: geom.NewPointEmpty(geom.XYM), + }, + { + desc: "parse empty 4D point", + equivInputs: []string{"POINT ZM EMPTY", "POINTZM EMPTY"}, + expected: geom.NewPointEmpty(geom.XYZM), + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + want := tc.expected + for _, input := range tc.equivInputs { + got, err := Unmarshal(input) + require.NoError(t, err) + require.Equal(t, want, got) + } + }) + } +} + +func TestUnmarshalError(t *testing.T) { + errorTestCases := []struct { + desc string + input string + expectedErrStr string + }{ + { + desc: "unrecognized character", + input: "POINT{0 0}", + expectedErrStr: "lex error: unrecognized character at pos 5", + }, + { + desc: "invalid keyword", + input: "DOT(0 0)", + expectedErrStr: "lex error: invalid keyword at pos 0", + }, + { + desc: "invalid number", + input: "POINT(2 2.3.7)", + expectedErrStr: "lex error: invalid number at pos 8", + }, + { + desc: "2D point with extra comma", + input: "POINT(0, 0)", + expectedErrStr: `parse error: could not parse "POINT(0, 0)"`, + }, + } + + for _, tc := range errorTestCases { + t.Run(tc.desc, func(t *testing.T) { + _, err := Unmarshal(tc.input) + require.EqualError(t, err, tc.expectedErrStr) + }) + } +}