first commit

This commit is contained in:
Josh Baker 2016-10-06 06:56:19 -07:00
commit ba9a043346
4 changed files with 483 additions and 0 deletions

20
LICENSE.md Normal file
View File

@ -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.

25
README.md Normal file
View File

@ -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).

337
grect.go Normal file
View File

@ -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
}

101
grect_test.go Normal file
View File

@ -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]
}