From ba9a043346eba55344e40d66a5e74cfda3a9d293 Mon Sep 17 00:00:00 2001 From: Josh Baker Date: Thu, 6 Oct 2016 06:56:19 -0700 Subject: [PATCH] first commit --- LICENSE.md | 20 +++ README.md | 25 ++++ grect.go | 337 ++++++++++++++++++++++++++++++++++++++++++++++++++ grect_test.go | 101 +++++++++++++++ 4 files changed, 483 insertions(+) create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 grect.go create mode 100644 grect_test.go diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..58f5819 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2016 Josh Baker + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..04a8bf0 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +GRECT +==== + +Quickly get the outer rectangle for GeoJSON, WKT, WKB. + +```go + r := grect.Get(`{ + "type": "Polygon", + "coordinates": [ + [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], + [100.0, 1.0], [100.0, 0.0] ] + ] + }`) + fmt.Printf("%v %v\n", r.Min, r.Max) + // Output: + // [100 0] [101 1] +``` + +## Contact +Josh Baker [@tidwall](http://twitter.com/tidwall) + +## License + +GRECT source code is available under the MIT [License](/LICENSE). + diff --git a/grect.go b/grect.go new file mode 100644 index 0000000..13eb761 --- /dev/null +++ b/grect.go @@ -0,0 +1,337 @@ +package grect + +import ( + "strconv" + "strings" + + "github.com/tidwall/gjson" +) + +type Rect struct { + Min, Max []float64 +} + +func (r Rect) String() string { + diff := len(r.Min) != len(r.Max) + if !diff { + for i := 0; i < len(r.Min); i++ { + if r.Min[i] != r.Max[i] { + diff = true + break + } + } + } + var buf []byte + buf = append(buf, '[') + for i, v := range r.Min { + if i > 0 { + buf = append(buf, ' ') + } + buf = append(buf, strconv.FormatFloat(v, 'f', -1, 64)...) + } + if diff { + buf = append(buf, ']', ',', '[') + for i, v := range r.Max { + if i > 0 { + buf = append(buf, ' ') + } + buf = append(buf, strconv.FormatFloat(v, 'f', -1, 64)...) + } + } + buf = append(buf, ']') + return string(buf) +} + +func normalize(min, max []float64) (nmin, nmax []float64) { + if len(max) == 0 { + return min, min + } else if len(max) != len(min) { + if len(max) < len(min) { + max = append(max, min[len(max):]...) + } else if len(min) < len(max) { + min = append(min, max[len(min):]...) + } + } + match := true + for i := 0; i < len(min); i++ { + if min[i] != max[i] { + if match { + match = false + } + if min[i] > max[i] { + min[i], max[i] = max[i], min[i] + } + } + } + if match { + return min, min + } + return min, max +} + +func Get(s string) Rect { + var i int + var ws bool + var min, max []float64 + for ; i < len(s); i++ { + switch s[i] { + default: + continue + case ' ', '\t', '\r', '\n': + ws = true + continue + case '[': + min, max, i = getRect(s, i) + case '{': + min, max, i = getGeoJSON(s, i) + case 0x00, 0x01: + if !ws { + // return parseWKB(s, i) + } + case 'p', 'P', 'l', 'L', 'm', 'M', 'g', 'G': + min, max, i = getWKT(s, i) + } + break + } + min, max = normalize(min, max) + return Rect{Min: min, Max: max} +} + +func getRect(s string, i int) (min, max []float64, ri int) { + a := s[i:] + parts := strings.Split(a, ",") + for i := 0; i < len(parts) && i < 2; i++ { + part := parts[i] + if len(part) > 0 && (part[0] <= ' ' || part[len(part)-1] <= ' ') { + part = strings.TrimSpace(part) + } + if len(part) >= 2 && part[0] == '[' && part[len(part)-1] == ']' { + pieces := strings.Split(part[1:len(part)-1], " ") + if i == 0 { + min = make([]float64, 0, len(pieces)) + } else { + max = make([]float64, 0, len(pieces)) + } + for j := 0; j < len(pieces); j++ { + piece := pieces[j] + if piece != "" { + n, _ := strconv.ParseFloat(piece, 64) + if i == 0 { + min = append(min, n) + } else { + max = append(max, n) + } + } + } + } + } + + // normalize + if len(parts) == 1 { + max = min + } else { + min, max = normalize(min, max) + } + + return min, max, len(s) +} + +func union(min1, max1, min2, max2 []float64) (umin, umax []float64) { + for i := 0; i < len(min1) || i < len(min2); i++ { + if i >= len(min1) { + // just copy min2 + umin = append(umin, min2[i]) + umax = append(umax, max2[i]) + } else if i >= len(min2) { + // just copy min1 + umin = append(umin, min1[i]) + umax = append(umax, max1[i]) + } else { + if min1[i] < min2[i] { + umin = append(umin, min1[i]) + } else { + umin = append(umin, min2[i]) + } + if max1[i] > max2[i] { + umax = append(umax, max1[i]) + } else { + umax = append(umax, max2[i]) + } + } + } + return umin, umax +} + +func getWKT(s string, i int) (min, max []float64, ri int) { + switch s[i] { + default: + for ; i < len(s); i++ { + if s[i] == ',' { + return nil, nil, i + } + if s[i] == '(' { + return getWKTAny(s, i) + } + } + return nil, nil, i + case 'g', 'G': + if len(s)-i < 18 { + return nil, nil, i + } + return getWKTGeometryCollection(s, i+18) + } +} + +func getWKTAny(s string, i int) (min, max []float64, ri int) { + min, max = make([]float64, 0, 4), make([]float64, 0, 4) + var depth int + var ni int + var idx int +loop: + for ; i < len(s); i++ { + switch s[i] { + default: + if ni == 0 { + ni = i + } + case '(': + depth++ + case ')', ' ', '\t', '\r', '\n', ',': + if ni != 0 { + n, _ := strconv.ParseFloat(s[ni:i], 64) + if idx >= len(min) { + min = append(min, n) + max = append(max, n) + } else { + if n < min[idx] { + min[idx] = n + } else if n > max[idx] { + max[idx] = n + } + } + idx++ + ni = 0 + } + switch s[i] { + case ')': + idx = 0 + depth-- + if depth == 0 { + i++ + break loop + } + case ',': + idx = 0 + } + } + } + return min, max, i +} + +func getWKTGeometryCollection(s string, i int) (min, max []float64, ri int) { + var depth int + for ; i < len(s); i++ { + if s[i] == ',' || s[i] == ')' { + // do not increment the index + return nil, nil, i + } + if s[i] == '(' { + depth++ + i++ + break + } + } +next: + for ; i < len(s); i++ { + switch s[i] { + case 'p', 'P', 'l', 'L', 'm', 'M', 'g', 'G': + var min2, max2 []float64 + min2, max2, i = getWKT(s, i) + min, max = union(min, max, min2, max2) + for ; i < len(s); i++ { + if s[i] == ',' { + i++ + goto next + } + if s[i] == ')' { + i++ + goto done + } + } + case ' ', '\t', '\r', '\n': + continue + default: + goto end_early + } + } +end_early: + // just balance the parens + for ; i < len(s); i++ { + if s[i] == '(' { + depth++ + } else if s[i] == ')' { + depth-- + if depth == 0 { + i++ + break + } + } + } +done: + return min, max, i +} +func getGeoJSON(s string, i int) (min, max []float64, ri int) { + json := s[i:] + switch gjson.Get(json, "type").String() { + default: + min, max = getMinMaxBrackets(gjson.Get(json, "coordinates").Raw) + case "Feature": + min, max, _ = getGeoJSON(gjson.Get(json, "geometry").String(), 0) + case "FeatureCollection": + for _, json := range gjson.Get(json, "features").Array() { + nmin, nmax, _ := getGeoJSON(json.String(), 0) + min, max = union(min, max, nmin, nmax) + } + case "GeometryCollection": + for _, json := range gjson.Get(json, "geometries").Array() { + nmin, nmax, _ := getGeoJSON(json.String(), 0) + min, max = union(min, max, nmin, nmax) + } + } + return min, max, len(json) +} + +func getMinMaxBrackets(s string) (min, max []float64) { + var ni int + var idx int + for i := 0; i < len(s); i++ { + switch s[i] { + default: + if ni == 0 { + ni = i + } + case '[', ',', ']', ' ', '\t', '\r', '\n': + if ni > 0 { + n, _ := strconv.ParseFloat(s[ni:i], 64) + if idx >= len(min) { + min = append(min, n) + max = append(max, n) + } else { + if n < min[idx] { + min[idx] = n + } else if n > max[idx] { + max[idx] = n + } + } + ni = 0 + idx++ + } + if s[i] == ']' { + idx = 0 + } + + } + } + + return +} diff --git a/grect_test.go b/grect_test.go new file mode 100644 index 0000000..61470b7 --- /dev/null +++ b/grect_test.go @@ -0,0 +1,101 @@ +package grect + +import ( + "fmt" + "math/rand" + "testing" + "time" +) + +func testGet(t *testing.T, s, expect string) { + if Get(s).String() != expect { + t.Fatalf("for '%v': expected '%v', got '%v'", s, expect, Get(s).String()) + } +} + +func TestRect(t *testing.T) { + testGet(t, "", "[]") + testGet(t, "[]", "[]") + testGet(t, "[],[]", "[]") + testGet(t, "[],[],[]", "[]") + testGet(t, "[10]", "[10]") + testGet(t, "[10],[10]", "[10]") + testGet(t, "[10 11],[10]", "[10 11]") + testGet(t, "[10 11],[11 10]", "[10 10],[11 11]") + testGet(t, ",[10]", "[10]") + testGet(t, "[10 11]", "[10 11]") + testGet(t, "[-3 -2 -1 0 1 2 3],[3 2 1 0 -1 -2 -3]", "[-3 -2 -1 0 -1 -2 -3],[3 2 1 0 1 2 3]") +} + +func TestWKT(t *testing.T) { + testGet(t, "POINT(1 2)", "[1 2]") + testGet(t, "LINESTRING(3 4, -1 -3, (-20 15 18 ))", "[-20 -3 18],[3 15 18]") + testGet(t, "POLYGON (((1 2 0), 3 4 1, -1 -3 123))", "[-1 -3 0],[3 4 123]") + testGet(t, "MULTIPOINT (1 2, 3 4, -1 0)", "[-1 0],[3 4]") + testGet(t, " mUltiLineString ( (1 2, 3 4),(3 4, (5 6)), (-1 -2 -3)) ", "[-1 -2 -3],[5 6 -3]") + testGet(t, ` MULTIPOLYGON ( + ((1 2,2 3),(2 3,8 9)), + ((4 5,6 7)) + )`, "[1 2],[8 9]") + testGet(t, ` + GEOMETRYCOLLECTION ( + POLYGON EMPTY, + POINT EMPTY, + POINT(1000 2), + POINT EMPTY, + LINESTRING(3 4, -1 -3, (-20 15 18)), + GEOMETRYCOLLECTION EMPTY, + GEOMETRYCOLLECTION(POINT(-1000),POLYGON((10 20,-50 1500))), + )`, "[-1000 -3 18],[1000 1500 18]") +} + +func TestGeoJSON(t *testing.T) { + testGet(t, `{"type":"Point","coordinates":[1,2]}`, "[1 2]") + testGet(t, `{"type":"LineString","coordinates":[[3,4], [-1,-3], [-20,15,18]]}`, "[-20 -3 18],[3 15 18]") + testGet(t, `{"type":"Polygon", "coordinates": [[[[1,2,0]],[ 3,4,1], [-1,-3,123]]]}`, "[-1 -3 0],[3 4 123]") + testGet(t, `{"type":"MultiPoint", "coordinates":[[1,2], [3,4], [-1,0]]}`, "[-1 0],[3 4]") + testGet(t, `{"type":"mUltiLineString","coordinates": [ [[1,2], [3,4]],[[3,4], [5,6]], [-1,-2,-3]]} `, "[-1 -2 -3],[5 6 -3]") + testGet(t, `{"type":"MULTIPOLYGON","coordinates": [ + [[[1 2],[2 3]],[[2 3],[8 9]]], + [[[4 5],[6 7]]] + )`, "[1 2],[8 9]") + testGet(t, `{"type":"GeometryCollection", "geometries":[ + {"type":"Feature","geometry":{"type":"Point","coordinates":[0 -10, 17]}}, + {"type":"FeatureCollection","features":[ + {"type":"Feature","geometry":{"type":"Point","coordinates":[0 -10]}}, + {"type":"Feature","geometry":{"type":"Point","coordinates":[0 -11]}} + ]}, + {"type":"POLYGON","coordinates":[]}, + {"type":"POINT","coordinates":[]}, + {"type":"POINT","coordinates":[1000,2]}, + {"type":"POINT","coordinates":[]}, + {"type":"LINESTRING","coordinates":[[3,4], [-1,-3], [[-20,15,18]]]}, + {"type":"GeometryCollection","geometries":[]}, + {"type":"GeometryCollection","geometries":[ + {"type":"Point","coordinates":[-1000]}, + {"type":"Polygon","coordinates":[[[10,20],[-50,1500]]]} + ], + ]}`, "[-1000 -11 17],[1000 1500 18]") +} + +func TestRandom(t *testing.T) { + buf := make([]byte, 50) + rand.Seed(time.Now().UnixNano()) + for i := 0; i < 10000000; i++ { + rand.Read(buf) + Get(string(buf)) + } +} + +func ExampleGet() { + r := Get(`{ + "type": "Polygon", + "coordinates": [ + [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], + [100.0, 1.0], [100.0, 0.0] ] + ] + }`) + fmt.Printf("%v %v\n", r.Min, r.Max) + // Output: + // [100 0] [101 1] +}