From 1f70cb4e754c03a9ddf5b158aac108a60d0fad64 Mon Sep 17 00:00:00 2001 From: Josh Baker Date: Thu, 11 Jan 2018 13:57:18 -0700 Subject: [PATCH] Precalculate and store bboxes for complex objects This should increase the performance for most search operations such as WITHIN, INTERSECTS, and NEARBY when dealing with complex polygons. Geofencing should see a increase in throughput when dealing with a high frequency of point -> polygon detection. Addresses #245 --- controller/aofshrink.go | 11 - geojson/bbox.go | 32 +- geojson/feature.go | 52 +-- geojson/feature_test.go | 653 +++++++++++++++++++++++++++++++- geojson/featurecollection.go | 51 ++- geojson/geojson_test.go | 33 +- geojson/geometrycollection.go | 50 ++- geojson/levels.go | 135 ++++--- geojson/linestring.go | 24 +- geojson/multilinestring.go | 23 +- geojson/multilinestring_test.go | 4 +- geojson/multipoint.go | 21 +- geojson/multipolygon.go | 21 +- geojson/object.go | 52 +-- geojson/point.go | 21 +- geojson/polygon.go | 67 +--- geojson/position.go | 26 +- geojson/simplepoint.go | 8 +- geojson/string.go | 10 +- 19 files changed, 971 insertions(+), 323 deletions(-) diff --git a/controller/aofshrink.go b/controller/aofshrink.go index 6444a295..74f5b678 100644 --- a/controller/aofshrink.go +++ b/controller/aofshrink.go @@ -137,17 +137,6 @@ func (c *Controller) aofshrink() { values = append(values, "point") values = append(values, strconv.FormatFloat(obj.Y, 'f', -1, 64)) values = append(values, strconv.FormatFloat(obj.X, 'f', -1, 64)) - case geojson.Point: - if obj.Coordinates.Z == 0 { - values = append(values, "point") - values = append(values, strconv.FormatFloat(obj.Coordinates.Y, 'f', -1, 64)) - values = append(values, strconv.FormatFloat(obj.Coordinates.X, 'f', -1, 64)) - values = append(values, strconv.FormatFloat(obj.Coordinates.Z, 'f', -1, 64)) - } else { - values = append(values, "point") - values = append(values, strconv.FormatFloat(obj.Coordinates.Y, 'f', -1, 64)) - values = append(values, strconv.FormatFloat(obj.Coordinates.X, 'f', -1, 64)) - } } // append the values to the aof buffer diff --git a/geojson/bbox.go b/geojson/bbox.go index 180fed4a..947855b6 100644 --- a/geojson/bbox.go +++ b/geojson/bbox.go @@ -1,7 +1,6 @@ package geojson import ( - "bytes" "math" "strconv" @@ -63,28 +62,29 @@ func (b *BBox) isCordZDefined() bool { return b != nil && (b.Min.Z != nilz || b.Max.Z != nilz) } -func (b *BBox) write(buf *bytes.Buffer) { +func appendBBoxJSON(json []byte, b *BBox) []byte { if b == nil { - return + return json } hasZ := b.Min.Z != nilz && b.Max.Z != nilz - buf.WriteString(`,"bbox":[`) - buf.WriteString(strconv.FormatFloat(b.Min.X, 'f', -1, 64)) - buf.WriteByte(',') - buf.WriteString(strconv.FormatFloat(b.Min.Y, 'f', -1, 64)) + json = append(json, `,"bbox":[`...) + json = strconv.AppendFloat(json, b.Min.X, 'f', -1, 64) + json = append(json, ',') + json = strconv.AppendFloat(json, b.Min.Y, 'f', -1, 64) if hasZ { - buf.WriteByte(',') - buf.WriteString(strconv.FormatFloat(b.Min.Z, 'f', -1, 64)) + json = append(json, ',') + json = strconv.AppendFloat(json, b.Min.Z, 'f', -1, 64) } - buf.WriteByte(',') - buf.WriteString(strconv.FormatFloat(b.Max.X, 'f', -1, 64)) - buf.WriteByte(',') - buf.WriteString(strconv.FormatFloat(b.Max.Y, 'f', -1, 64)) + json = append(json, ',') + json = strconv.AppendFloat(json, b.Max.X, 'f', -1, 64) + json = append(json, ',') + json = strconv.AppendFloat(json, b.Max.Y, 'f', -1, 64) if hasZ { - buf.WriteByte(',') - buf.WriteString(strconv.FormatFloat(b.Max.Z, 'f', -1, 64)) + json = append(json, ',') + json = strconv.AppendFloat(json, b.Max.Z, 'f', -1, 64) } - buf.WriteByte(']') + json = append(json, ']') + return json } func (b BBox) center() Position { diff --git a/geojson/feature.go b/geojson/feature.go index a74f80ce..7bf7ad04 100644 --- a/geojson/feature.go +++ b/geojson/feature.go @@ -1,7 +1,6 @@ package geojson import ( - "bytes" "encoding/binary" "github.com/tidwall/gjson" @@ -10,9 +9,10 @@ import ( // Feature is a geojson object with the type "Feature" type Feature struct { - Geometry Object - BBox *BBox - idprops string // raw id and properties seperated by a '\0' + Geometry Object + BBox *BBox + bboxDefined bool + idprops string // raw id and properties seperated by a '\0' } func fillFeatureMap(json string) (Feature, error) { @@ -35,7 +35,11 @@ func fillFeatureMap(json string) (Feature, error) { if err != nil { return g, err } - + g.bboxDefined = g.BBox != nil + if !g.bboxDefined { + cbbox := g.CalculatedBBox() + g.BBox = &cbbox + } var propsExists bool props := gjson.Get(json, "properties") switch props.Type { @@ -89,7 +93,7 @@ func (g Feature) Weight() int { // MarshalJSON allows the object to be encoded in json.Marshal calls. func (g Feature) MarshalJSON() ([]byte, error) { - return []byte(g.JSON()), nil + return g.appendJSON(nil), nil } func (g Feature) getRaw() (id, props string) { @@ -128,23 +132,27 @@ func makeCompositeRaw(idRaw, propsRaw string) string { return string(raw) } -// JSON is the json representation of the object. This might not be exactly the same as the original. -func (g Feature) JSON() string { - var buf bytes.Buffer - buf.WriteString(`{"type":"Feature","geometry":`) - buf.WriteString(g.Geometry.JSON()) - g.BBox.write(&buf) +func (g Feature) appendJSON(json []byte) []byte { + json = append(json, `{"type":"Feature","geometry":`...) + json = append(json, g.Geometry.JSON()...) + if g.bboxDefined { + json = appendBBoxJSON(json, g.BBox) + } idRaw, propsRaw := g.getRaw() if propsRaw != "" { - buf.WriteString(`,"properties":`) - buf.WriteString(propsRaw) + json = append(json, `,"properties":`...) + json = append(json, propsRaw...) } if idRaw != "" { - buf.WriteString(`,"id":`) - buf.WriteString(idRaw) + json = append(json, `,"id":`...) + json = append(json, idRaw...) } - buf.WriteByte('}') - return buf.String() + return append(json, '}') +} + +// JSON is the json representation of the object. This might not be exactly the same as the original. +func (g Feature) JSON() string { + return string(g.appendJSON(nil)) } // String returns a string representation of the object. This might be JSON or something else. @@ -160,7 +168,7 @@ func (g Feature) bboxPtr() *BBox { return g.BBox } func (g Feature) hasPositions() bool { - if g.BBox != nil { + if g.bboxDefined { return true } return g.Geometry.hasPositions() @@ -168,7 +176,7 @@ func (g Feature) hasPositions() bool { // WithinBBox detects if the object is fully contained inside a bbox. func (g Feature) WithinBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).InsideRect(rectBBox(bbox)) } return g.Geometry.WithinBBox(bbox) @@ -176,7 +184,7 @@ func (g Feature) WithinBBox(bbox BBox) bool { // IntersectsBBox detects if the object intersects a bbox. func (g Feature) IntersectsBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).IntersectsRect(rectBBox(bbox)) } return g.Geometry.IntersectsBBox(bbox) @@ -213,7 +221,7 @@ func (g Feature) Nearby(center Position, meters float64) bool { // IsBBoxDefined returns true if the object has a defined bbox. func (g Feature) IsBBoxDefined() bool { - return g.BBox != nil + return g.bboxDefined } // IsGeometry return true if the object is a geojson geometry object. false if it something else. diff --git a/geojson/feature_test.go b/geojson/feature_test.go index 5c3b7a2c..168efdf6 100644 --- a/geojson/feature_test.go +++ b/geojson/feature_test.go @@ -1,6 +1,8 @@ package geojson -import "testing" +import ( + "testing" +) func TestFeature(t *testing.T) { testJSON(t, `{ @@ -116,3 +118,652 @@ func TestComplexFeature(t *testing.T) { } o = o } + +func TestIssue245(t *testing.T) { + json := `{ + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [113.840391, 23.36073], + [113.840837, 23.360618], + [113.841198, 23.360528], + [113.84295, 23.360252], + [113.844386, 23.359914], + [113.844674, 23.359847], + [113.845843, 23.359466], + [113.846787, 23.358753], + [113.84679, 23.358751], + [113.847128, 23.358254], + [113.847571, 23.357602], + [113.847573, 23.357598], + [113.848175, 23.356521], + [113.848177, 23.356517], + [113.848187, 23.3565], + [113.848189, 23.356497], + [113.84847, 23.355449], + [113.848471, 23.355445], + [113.848475, 23.355126], + [113.848486, 23.354203], + [113.848373, 23.353516], + [113.848122, 23.352467], + [113.847432, 23.351258], + [113.846102, 23.349542], + [113.845853, 23.349288], + [113.84494, 23.348358], + [113.844435, 23.347908], + [113.843638, 23.347198], + [113.840895, 23.345392], + [113.836433, 23.342373], + [113.835963, 23.342033], + [113.83469, 23.34111], + [113.833801, 23.340465], + [113.831238, 23.338917], + [113.830698, 23.33835], + [113.830644, 23.337639], + [113.830642, 23.337639], + [113.825764, 23.337862], + [113.82575, 23.337897], + [113.82575, 23.337899], + [113.825749, 23.337901], + [113.825748, 23.337905], + [113.825747, 23.337907], + [113.825746, 23.337909], + [113.825746, 23.337912], + [113.825745, 23.337914], + [113.825744, 23.337916], + [113.825743, 23.337919], + [113.825742, 23.337921], + [113.825741, 23.337923], + [113.82574, 23.337926], + [113.825739, 23.337928], + [113.825738, 23.33793], + [113.825738, 23.337933], + [113.825737, 23.337935], + [113.825736, 23.337937], + [113.825735, 23.33794], + [113.825734, 23.337942], + [113.825733, 23.337944], + [113.825732, 23.337947], + [113.825731, 23.337949], + [113.82573, 23.337951], + [113.825729, 23.337954], + [113.825728, 23.337956], + [113.825727, 23.337959], + [113.825726, 23.337962], + [113.825725, 23.337964], + [113.825725, 23.337966], + [113.825724, 23.337969], + [113.825723, 23.337971], + [113.825722, 23.337973], + [113.825721, 23.337975], + [113.82572, 23.337978], + [113.825719, 23.33798], + [113.825718, 23.337982], + [113.825717, 23.337985], + [113.825717, 23.337987], + [113.825716, 23.337989], + [113.825715, 23.337992], + [113.825715, 23.337994], + [113.825714, 23.337996], + [113.825713, 23.337999], + [113.825712, 23.338001], + [113.825711, 23.338003], + [113.82571, 23.338006], + [113.825709, 23.338008], + [113.825708, 23.33801], + [113.825707, 23.338013], + [113.825706, 23.338015], + [113.825705, 23.338018], + [113.825705, 23.338021], + [113.825704, 23.338023], + [113.825703, 23.338025], + [113.825702, 23.338028], + [113.825701, 23.33803], + [113.8257, 23.338032], + [113.825699, 23.338035], + [113.825698, 23.338037], + [113.825697, 23.338039], + [113.825696, 23.338042], + [113.825696, 23.338044], + [113.825695, 23.338046], + [113.825694, 23.338049], + [113.825693, 23.338051], + [113.825692, 23.338053], + [113.825691, 23.338056], + [113.82569, 23.338058], + [113.82569, 23.338061], + [113.825689, 23.338063], + [113.825689, 23.338065], + [113.825688, 23.338068], + [113.825687, 23.33807], + [113.825686, 23.338072], + [113.825685, 23.338075], + [113.825684, 23.338078], + [113.825683, 23.33808], + [113.825682, 23.338083], + [113.825682, 23.338085], + [113.825681, 23.338087], + [113.82568, 23.33809], + [113.825679, 23.338092], + [113.825678, 23.338094], + [113.825677, 23.338097], + [113.825676, 23.338099], + [113.825675, 23.338101], + [113.825675, 23.338104], + [113.825674, 23.338106], + [113.825673, 23.338108], + [113.825672, 23.338111], + [113.825671, 23.338113], + [113.82567, 23.338115], + [113.825669, 23.338118], + [113.825669, 23.33812], + [113.825668, 23.338123], + [113.825668, 23.338125], + [113.825667, 23.338127], + [113.825666, 23.33813], + [113.825665, 23.338132], + [113.825664, 23.338134], + [113.825664, 23.338138], + [113.825663, 23.33814], + [113.825662, 23.338142], + [113.825661, 23.338145], + [113.82566, 23.338147], + [113.825659, 23.338149], + [113.825659, 23.338152], + [113.825658, 23.338154], + [113.825657, 23.338156], + [113.825656, 23.338159], + [113.825655, 23.338161], + [113.825654, 23.338164], + [113.825654, 23.338166], + [113.825653, 23.338168], + [113.825652, 23.338171], + [113.825651, 23.338173], + [113.82565, 23.338175], + [113.82565, 23.338178], + [113.825649, 23.33818], + [113.825648, 23.338182], + [113.825647, 23.338185], + [113.825646, 23.338187], + [113.825646, 23.33819], + [113.825646, 23.338192], + [113.825645, 23.338194], + [113.825644, 23.338197], + [113.825643, 23.3382], + [113.825642, 23.338202], + [113.825642, 23.338205], + [113.825641, 23.338207], + [113.82564, 23.33821], + [113.825639, 23.338212], + [113.825639, 23.338214], + [113.825638, 23.338217], + [113.825637, 23.338219], + [113.825636, 23.338221], + [113.825635, 23.338224], + [113.825635, 23.338226], + [113.825634, 23.338229], + [113.825633, 23.338231], + [113.825632, 23.338233], + [113.825632, 23.338236], + [113.825631, 23.338238], + [113.82563, 23.33824], + [113.825629, 23.338243], + [113.825628, 23.338245], + [113.825629, 23.338248], + [113.825628, 23.33825], + [113.825627, 23.338252], + [113.825626, 23.338255], + [113.825626, 23.338257], + [113.825625, 23.338259], + [113.825624, 23.338262], + [113.825623, 23.338265], + [113.825623, 23.338268], + [113.825622, 23.33827], + [113.825621, 23.338272], + [113.82562, 23.338275], + [113.82562, 23.338277], + [113.825619, 23.33828], + [113.825618, 23.338282], + [113.825618, 23.338284], + [113.825617, 23.338287], + [113.825616, 23.338289], + [113.825615, 23.338292], + [113.825532, 23.338833], + [113.825532, 23.338835], + [113.825532, 23.338838], + [113.825532, 23.33884], + [113.825532, 23.338843], + [113.825532, 23.338845], + [113.825531, 23.338848], + [113.825532, 23.33885], + [113.825532, 23.338853], + [113.825532, 23.338855], + [113.825532, 23.338858], + [113.825532, 23.33886], + [113.825532, 23.338863], + [113.825531, 23.338865], + [113.825531, 23.338868], + [113.825531, 23.33887], + [113.825531, 23.338873], + [113.825531, 23.338875], + [113.825531, 23.338878], + [113.825532, 23.33888], + [113.825532, 23.338883], + [113.825531, 23.338885], + [113.825531, 23.338888], + [113.825531, 23.33889], + [113.825531, 23.338893], + [113.825531, 23.338895], + [113.825531, 23.338898], + [113.825531, 23.3389], + [113.825531, 23.338903], + [113.825531, 23.338905], + [113.82553, 23.338908], + [113.825531, 23.33891], + [113.825531, 23.338913], + [113.825531, 23.338915], + [113.825531, 23.338918], + [113.825531, 23.33892], + [113.825531, 23.338923], + [113.825531, 23.338925], + [113.825531, 23.338928], + [113.825531, 23.33893], + [113.825531, 23.338933], + [113.82553, 23.338935], + [113.82553, 23.338938], + [113.825531, 23.33894], + [113.825531, 23.338942], + [113.825531, 23.338945], + [113.825531, 23.338947], + [113.825531, 23.33895], + [113.825531, 23.338952], + [113.825531, 23.338956], + [113.825531, 23.338958], + [113.825531, 23.338961], + [113.825531, 23.338963], + [113.825531, 23.338966], + [113.825532, 23.338968], + [113.825532, 23.338971], + [113.825532, 23.338973], + [113.825531, 23.338976], + [113.825531, 23.338978], + [113.825531, 23.338981], + [113.825531, 23.338983], + [113.825531, 23.338986], + [113.825531, 23.338988], + [113.825531, 23.338991], + [113.825531, 23.338993], + [113.825531, 23.338996], + [113.825532, 23.338998], + [113.825532, 23.339001], + [113.825532, 23.339003], + [113.825532, 23.339006], + [113.825532, 23.339008], + [113.825532, 23.339011], + [113.825532, 23.339013], + [113.825532, 23.339016], + [113.825532, 23.339018], + [113.825532, 23.339021], + [113.825532, 23.339023], + [113.825533, 23.339026], + [113.825533, 23.339028], + [113.825533, 23.339031], + [113.825533, 23.339033], + [113.825533, 23.339036], + [113.825533, 23.339038], + [113.825533, 23.339041], + [113.825533, 23.339043], + [113.825533, 23.339046], + [113.825533, 23.339048], + [113.825533, 23.339051], + [113.825533, 23.339053], + [113.825534, 23.339056], + [113.825534, 23.339058], + [113.825534, 23.339061], + [113.825534, 23.339063], + [113.825534, 23.339066], + [113.825534, 23.339068], + [113.825534, 23.339071], + [113.825534, 23.339073], + [113.825534, 23.339076], + [113.825534, 23.339078], + [113.825534, 23.339081], + [113.825535, 23.339083], + [113.825535, 23.339086], + [113.825535, 23.339088], + [113.825535, 23.339091], + [113.825535, 23.339093], + [113.825535, 23.339096], + [113.825535, 23.339098], + [113.825535, 23.339101], + [113.825535, 23.339103], + [113.825535, 23.339106], + [113.825535, 23.339108], + [113.825536, 23.339111], + [113.825536, 23.339113], + [113.825536, 23.339116], + [113.825536, 23.339118], + [113.825536, 23.339121], + [113.825536, 23.339123], + [113.825536, 23.339125], + [113.825536, 23.339128], + [113.825536, 23.33913], + [113.825536, 23.339133], + [113.825536, 23.339135], + [113.825537, 23.339138], + [113.825537, 23.33914], + [113.825538, 23.339143], + [113.825538, 23.339145], + [113.825538, 23.339148], + [113.825538, 23.33915], + [113.825538, 23.339153], + [113.825538, 23.339156], + [113.825538, 23.339159], + [113.825538, 23.339161], + [113.825538, 23.339164], + [113.825539, 23.339166], + [113.825539, 23.339169], + [113.825539, 23.339171], + [113.825539, 23.339174], + [113.825539, 23.339176], + [113.825539, 23.339179], + [113.825539, 23.339181], + [113.825539, 23.339184], + [113.825539, 23.339186], + [113.825539, 23.339189], + [113.825539, 23.339191], + [113.82554, 23.339194], + [113.82554, 23.339196], + [113.825541, 23.339199], + [113.825541, 23.339201], + [113.825541, 23.339204], + [113.825541, 23.339206], + [113.825541, 23.339209], + [113.825541, 23.339211], + [113.825541, 23.339214], + [113.825541, 23.339216], + [113.825542, 23.339219], + [113.825542, 23.339221], + [113.825542, 23.339224], + [113.825542, 23.339226], + [113.825542, 23.339229], + [113.825542, 23.339231], + [113.825542, 23.339234], + [113.825542, 23.339236], + [113.825542, 23.339239], + [113.825542, 23.339241], + [113.825543, 23.339244], + [113.825544, 23.339246], + [113.825544, 23.339249], + [113.825544, 23.339251], + [113.825544, 23.339254], + [113.825544, 23.339256], + [113.825544, 23.339259], + [113.825544, 23.339261], + [113.825544, 23.339264], + [113.825544, 23.339266], + [113.825544, 23.339269], + [113.825544, 23.339271], + [113.825545, 23.339274], + [113.825545, 23.339276], + [113.825545, 23.339279], + [113.825545, 23.339281], + [113.825545, 23.339284], + [113.825675, 23.341405], + [113.825676, 23.341408], + [113.825676, 23.34141], + [113.825676, 23.341413], + [113.825676, 23.341415], + [113.825676, 23.341418], + [113.825676, 23.34142], + [113.825676, 23.341423], + [113.825676, 23.341425], + [113.825676, 23.341428], + [113.825676, 23.34143], + [113.825676, 23.341433], + [113.825676, 23.341435], + [113.825677, 23.341438], + [113.825677, 23.34144], + [113.825677, 23.341443], + [113.825678, 23.341445], + [113.825678, 23.341448], + [113.825678, 23.34145], + [113.825678, 23.341453], + [113.825678, 23.341455], + [113.825678, 23.341458], + [113.825678, 23.34146], + [113.825679, 23.341463], + [113.825679, 23.341465], + [113.825679, 23.341468], + [113.825679, 23.34147], + [113.825679, 23.341473], + [113.825679, 23.341475], + [113.825679, 23.341478], + [113.825679, 23.34148], + [113.825679, 23.341483], + [113.825679, 23.341485], + [113.82568, 23.341488], + [113.825681, 23.34149], + [113.825681, 23.341493], + [113.825681, 23.341495], + [113.825681, 23.341498], + [113.825681, 23.3415], + [113.825681, 23.341503], + [113.825681, 23.341505], + [113.825681, 23.341508], + [113.825681, 23.34151], + [113.825681, 23.341513], + [113.825681, 23.341515], + [113.825682, 23.341518], + [113.825682, 23.34152], + [113.825682, 23.341524], + [113.825682, 23.341526], + [113.825682, 23.341529], + [113.825682, 23.341531], + [113.825682, 23.341534], + [113.825683, 23.341536], + [113.825683, 23.341539], + [113.825683, 23.341541], + [113.825684, 23.341544], + [113.825684, 23.341546], + [113.825684, 23.341549], + [113.825684, 23.341551], + [113.825684, 23.341554], + [113.825684, 23.341556], + [113.825684, 23.341558], + [113.825684, 23.341561], + [113.825684, 23.341563], + [113.825684, 23.341566], + [113.825684, 23.341568], + [113.825684, 23.341571], + [113.825685, 23.341573], + [113.825685, 23.341576], + [113.825685, 23.341578], + [113.825685, 23.341581], + [113.825685, 23.341583], + [113.825686, 23.341586], + [113.825686, 23.341588], + [113.825686, 23.341591], + [113.825686, 23.341593], + [113.825686, 23.341596], + [113.825687, 23.341598], + [113.825687, 23.341601], + [113.825687, 23.341603], + [113.825687, 23.341606], + [113.825687, 23.341608], + [113.825687, 23.341611], + [113.825687, 23.341613], + [113.825687, 23.341616], + [113.825687, 23.341618], + [113.825687, 23.341621], + [113.825687, 23.341623], + [113.825688, 23.341626], + [113.825688, 23.341628], + [113.825688, 23.341631], + [113.825688, 23.341633], + [113.825688, 23.341636], + [113.825688, 23.341638], + [113.825688, 23.341641], + [113.825688, 23.341643], + [113.825688, 23.341646], + [113.825688, 23.341648], + [113.825689, 23.341651], + [113.82569, 23.341653], + [113.82569, 23.341656], + [113.82569, 23.341658], + [113.82569, 23.341661], + [113.82569, 23.341663], + [113.82569, 23.341666], + [113.82569, 23.341668], + [113.82569, 23.341671], + [113.82569, 23.341673], + [113.82569, 23.341676], + [113.82569, 23.341678], + [113.825691, 23.341681], + [113.825691, 23.341683], + [113.825691, 23.341686], + [113.825691, 23.341688], + [113.825691, 23.341691], + [113.825691, 23.341693], + [113.825691, 23.341696], + [113.825691, 23.341698], + [113.825691, 23.341701], + [113.825691, 23.341703], + [113.825691, 23.341706], + [113.825691, 23.341708], + [113.825692, 23.341711], + [113.825692, 23.341713], + [113.825692, 23.341716], + [113.825692, 23.341718], + [113.825692, 23.341721], + [113.825692, 23.341723], + [113.825692, 23.341726], + [113.825692, 23.341728], + [113.825692, 23.341731], + [113.825692, 23.341733], + [113.825693, 23.341736], + [113.825693, 23.341738], + [113.825693, 23.34174], + [113.825693, 23.341744], + [113.825693, 23.341746], + [113.825693, 23.341749], + [113.825693, 23.341751], + [113.825693, 23.341754], + [113.825693, 23.341756], + [113.825693, 23.341759], + [113.825693, 23.341761], + [113.825693, 23.341764], + [113.825694, 23.341766], + [113.825694, 23.341769], + [113.825694, 23.341771], + [113.825694, 23.341774], + [113.825694, 23.341776], + [113.825694, 23.341779], + [113.825694, 23.341781], + [113.825694, 23.341784], + [113.825694, 23.341786], + [113.825694, 23.341789], + [113.825694, 23.341791], + [113.825695, 23.341794], + [113.825695, 23.341796], + [113.825695, 23.341799], + [113.825695, 23.341801], + [113.825695, 23.341804], + [113.825695, 23.341806], + [113.825695, 23.341809], + [113.825695, 23.341811], + [113.825695, 23.341814], + [113.825695, 23.341816], + [113.825695, 23.341819], + [113.825696, 23.341821], + [113.825696, 23.341824], + [113.825696, 23.341826], + [113.825696, 23.341829], + [113.825696, 23.341831], + [113.825696, 23.341834], + [113.825696, 23.341836], + [113.825696, 23.341839], + [113.825696, 23.341841], + [113.825696, 23.341844], + [113.825696, 23.341846], + [113.825697, 23.341849], + [113.825697, 23.341851], + [113.825697, 23.341854], + [113.825696, 23.341856], + [113.825639, 23.342752], + [113.825909, 23.343038], + [113.826124, 23.34317], + [113.826246, 23.343207], + [113.826544, 23.343462], + [113.826663, 23.343725], + [113.826921, 23.344028], + [113.826946, 23.344045], + [113.827411, 23.344775], + [113.827452, 23.345071], + [113.827433, 23.345444], + [113.827373, 23.345688], + [113.827847, 23.346486], + [113.827867, 23.346492], + [113.828217, 23.346555], + [113.828812, 23.346578], + [113.829478, 23.346804], + [113.829926, 23.347127], + [113.830393, 23.34749], + [113.830756, 23.347827], + [113.83112, 23.348231], + [113.832415, 23.349689], + [113.833006, 23.350048], + [113.834533, 23.350448], + [113.835235, 23.350919], + [113.835526, 23.35132], + [113.836805, 23.353126], + [113.836931, 23.353474], + [113.837019, 23.35471], + [113.837041, 23.354934], + [113.837129, 23.355633], + [113.837135, 23.355731], + [113.837148, 23.35729], + [113.837124, 23.357391], + [113.837093, 23.357543], + [113.837116, 23.357842], + [113.837231, 23.358547], + [113.837488, 23.359569], + [113.837398, 23.359954], + [113.838019, 23.360873], + [113.838056, 23.360928], + [113.840391, 23.36073] + ] + ] + }, + "properties": { + "center": { + "lat": 0, + "lon": 0 + }, + "smuserid": 960, + "area": 2807456.367185, + "refname": "10301", + "name": "", + "refid": 293, + "districtid": 440183, + "ver": "v1.20170705", + "class": "–°«¯", + "id": 960, + "verdate": "20170705", + "oriid": 1724 + } +} +` + poly := testJSON(t, json).(Feature) + g, _ := ObjectJSON(`{"type":"Point","coordinates":[113.837019, 23.35471]}`) + if !g.Within(poly) { + t.Fatal("!") + } + g, _ = ObjectJSON(`{"type":"Point","coordinates":[114.837019, 23.35471]}`) + if g.Within(poly) { + t.Fatal("!") + } + +} diff --git a/geojson/featurecollection.go b/geojson/featurecollection.go index b46150b1..cb5990ce 100644 --- a/geojson/featurecollection.go +++ b/geojson/featurecollection.go @@ -1,16 +1,15 @@ package geojson import ( - "bytes" - "github.com/tidwall/gjson" "github.com/tidwall/tile38/geojson/geohash" ) // FeatureCollection is a geojson object with the type "FeatureCollection" type FeatureCollection struct { - Features []Object - BBox *BBox + Features []Object + BBox *BBox + bboxDefined bool } func fillFeatureCollectionMap(json string) (FeatureCollection, error) { @@ -40,6 +39,14 @@ func fillFeatureCollectionMap(json string) (FeatureCollection, error) { } var err error g.BBox, err = fillBBox(json) + if err != nil { + return g, err + } + g.bboxDefined = g.BBox != nil + if !g.bboxDefined { + cbbox := g.CalculatedBBox() + g.BBox = &cbbox + } return g, err } @@ -93,23 +100,27 @@ func (g FeatureCollection) Weight() int { // MarshalJSON allows the object to be encoded in json.Marshal calls. func (g FeatureCollection) MarshalJSON() ([]byte, error) { - return []byte(g.JSON()), nil + return g.appendJSON(nil), nil +} + +func (g FeatureCollection) appendJSON(json []byte) []byte { + json = append(json, `{"type":"FeatureCollection","features":[`...) + for i, g := range g.Features { + if i != 0 { + json = append(json, ',') + } + json = append(json, g.JSON()...) + } + json = append(json, ']') + if g.bboxDefined { + json = appendBBoxJSON(json, g.BBox) + } + return append(json, '}') } // JSON is the json representation of the object. This might not be exactly the same as the original. func (g FeatureCollection) JSON() string { - var buf bytes.Buffer - buf.WriteString(`{"type":"FeatureCollection","features":[`) - for i, g := range g.Features { - if i != 0 { - buf.WriteByte(',') - } - buf.WriteString(g.JSON()) - } - buf.WriteByte(']') - g.BBox.write(&buf) - buf.WriteByte('}') - return buf.String() + return string(g.appendJSON(nil)) } // String returns a string representation of the object. This might be JSON or something else. @@ -138,7 +149,7 @@ func (g FeatureCollection) hasPositions() bool { // WithinBBox detects if the object is fully contained inside a bbox. func (g FeatureCollection) WithinBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).InsideRect(rectBBox(bbox)) } if len(g.Features) == 0 { @@ -154,7 +165,7 @@ func (g FeatureCollection) WithinBBox(bbox BBox) bool { // IntersectsBBox detects if the object intersects a bbox. func (g FeatureCollection) IntersectsBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).IntersectsRect(rectBBox(bbox)) } for _, g := range g.Features { @@ -228,7 +239,7 @@ func (g FeatureCollection) Nearby(center Position, meters float64) bool { // IsBBoxDefined returns true if the object has a defined bbox. func (g FeatureCollection) IsBBoxDefined() bool { - return g.BBox != nil + return g.bboxDefined } // IsGeometry return true if the object is a geojson geometry object. false if it something else. diff --git a/geojson/geojson_test.go b/geojson/geojson_test.go index 77eb0fd9..fb1b9a6f 100644 --- a/geojson/geojson_test.go +++ b/geojson/geojson_test.go @@ -40,6 +40,7 @@ func doesJSONMatch(js1, js2 string) bool { } func testJSON(t *testing.T, jstr string) Object { + t.Helper() o, err := ObjectJSON(jstr) if err != nil { t.Fatal(err) @@ -47,11 +48,11 @@ func testJSON(t *testing.T, jstr string) Object { if !doesJSONMatch(o.JSON(), jstr) { t.Fatalf("expected '%v', got '%v'", o.JSON(), jstr) } - return o } func testInvalidJSON(t *testing.T, js string, expecting error) { + t.Helper() _, err := ObjectJSON(js) if err == nil { t.Fatalf("expecting an error for json '%s'", js) @@ -86,33 +87,3 @@ func TestInvalidJSON(t *testing.T) { testInvalidJSON(t, `{"type":"Point","coordinates":[1,2,3],"bbox":[1,2,3]}`, errBBoxInvalidNumberOfValues) testInvalidJSON(t, `{"type":"Point","coordinates":[1,2,3],"bbox":[1,2,3,"a"]}`, errBBoxInvalidValue) } - -// func TestJunk(t *testing.T) { -// type ThreePoint struct{ X, Y, Z float64 } - -// var s1 = ThreePoint{50, 50, 50} -// var s2 = &s1 -// var o1 interface{} = s1 -// var o2 interface{} = s2 - -// t1 := reflect.TypeOf(s1) -// t2 := reflect.TypeOf(s2) -// t3 := reflect.TypeOf(o1) -// t4 := reflect.TypeOf(o2) - -// fmt.Printf("typeof: %s\n", t1) -// fmt.Printf("typeof: %s\n", t2) -// fmt.Printf("typeof: %s\n", t3) -// fmt.Printf("typeof: %s\n", t4) - -// z1 := unsafe.Sizeof(s1) -// z2 := unsafe.Sizeof(s2) -// z3 := unsafe.Sizeof(o1) -// z4 := unsafe.Sizeof(o2.(*ThreePoint)) - -// fmt.Printf("sizeof: %d\n", z1) -// fmt.Printf("sizeof: %d\n", z2) -// fmt.Printf("sizeof: %d\n", z3) -// fmt.Printf("sizeof: %d\n", z4) - -// } diff --git a/geojson/geometrycollection.go b/geojson/geometrycollection.go index f28e0b51..ff139a5f 100644 --- a/geojson/geometrycollection.go +++ b/geojson/geometrycollection.go @@ -1,16 +1,15 @@ package geojson import ( - "bytes" - "github.com/tidwall/gjson" "github.com/tidwall/tile38/geojson/geohash" ) // GeometryCollection is a geojson object with the type "GeometryCollection" type GeometryCollection struct { - Geometries []Object - BBox *BBox + Geometries []Object + BBox *BBox + bboxDefined bool } func fillGeometryCollectionMap(json string) (GeometryCollection, error) { @@ -40,6 +39,14 @@ func fillGeometryCollectionMap(json string) (GeometryCollection, error) { } var err error g.BBox, err = fillBBox(json) + if err != nil { + return g, err + } + g.bboxDefined = g.BBox != nil + if !g.bboxDefined { + cbbox := g.CalculatedBBox() + g.BBox = &cbbox + } return g, err } @@ -93,23 +100,26 @@ func (g GeometryCollection) Weight() int { // MarshalJSON allows the object to be encoded in json.Marshal calls. func (g GeometryCollection) MarshalJSON() ([]byte, error) { - return []byte(g.JSON()), nil + return g.appendJSON(nil), nil +} +func (g GeometryCollection) appendJSON(json []byte) []byte { + json = append(json, `{"type":"GeometryCollection","geometries":[`...) + for i, g := range g.Geometries { + if i != 0 { + json = append(json, ',') + } + json = append(json, g.JSON()...) + } + json = append(json, ']') + if g.bboxDefined { + json = appendBBoxJSON(json, g.BBox) + } + return append(json, '}') } // JSON is the json representation of the object. This might not be exactly the same as the original. func (g GeometryCollection) JSON() string { - var buf bytes.Buffer - buf.WriteString(`{"type":"GeometryCollection","geometries":[`) - for i, g := range g.Geometries { - if i != 0 { - buf.WriteByte(',') - } - buf.WriteString(g.JSON()) - } - buf.WriteByte(']') - g.BBox.write(&buf) - buf.WriteByte('}') - return buf.String() + return string(g.appendJSON(nil)) } // String returns a string representation of the object. This might be JSON or something else. @@ -138,7 +148,7 @@ func (g GeometryCollection) hasPositions() bool { // WithinBBox detects if the object is fully contained inside a bbox. func (g GeometryCollection) WithinBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).InsideRect(rectBBox(bbox)) } if len(g.Geometries) == 0 { @@ -154,7 +164,7 @@ func (g GeometryCollection) WithinBBox(bbox BBox) bool { // IntersectsBBox detects if the object intersects a bbox. func (g GeometryCollection) IntersectsBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).IntersectsRect(rectBBox(bbox)) } for _, g := range g.Geometries { @@ -228,7 +238,7 @@ func (g GeometryCollection) Nearby(center Position, meters float64) bool { // IsBBoxDefined returns true if the object has a defined bbox. func (g GeometryCollection) IsBBoxDefined() bool { - return g.BBox != nil + return g.bboxDefined } // IsGeometry return true if the object is a geojson geometry object. false if it something else. diff --git a/geojson/levels.go b/geojson/levels.go index d7d3503e..b375b338 100644 --- a/geojson/levels.go +++ b/geojson/levels.go @@ -1,10 +1,6 @@ package geojson -import ( - "bytes" - - "github.com/tidwall/gjson" -) +import "github.com/tidwall/gjson" func resIsArray(res gjson.Result) bool { if res.Type == gjson.JSON { @@ -71,17 +67,20 @@ func level1Weight(coordinates Position, bbox *BBox) int { return level1PositionCount(coordinates, bbox) * sizeofPosition } -func level1JSON(name string, coordinates Position, bbox *BBox) string { +func appendLevel1JSON(json []byte, name string, coordinates Position, bbox *BBox, bboxDefined bool) []byte { + if bbox != nil && !bboxDefined { + bbox = nil + } isCordZ := level1IsCoordZDefined(coordinates, bbox) - var buf bytes.Buffer - buf.WriteString(`{"type":"`) - buf.WriteString(name) - buf.WriteString(`","coordinates":[`) - coordinates.writeJSON(&buf, isCordZ) - buf.WriteByte(']') - bbox.write(&buf) - buf.WriteByte('}') - return buf.String() + json = append(json, `{"type":"`...) + json = append(json, name...) + json = append(json, `","coordinates":[`...) + json = appendPositionJSON(json, coordinates, isCordZ) + json = append(json, ']') + if bboxDefined { + json = appendBBoxJSON(json, bbox) + } + return append(json, '}') } func level1IsCoordZDefined(coordinates Position, bbox *BBox) bool { @@ -149,24 +148,28 @@ func level2Weight(coordinates []Position, bbox *BBox) int { return level2PositionCount(coordinates, bbox) * sizeofPosition } -func level2JSON(name string, coordinates []Position, bbox *BBox) string { +func appendLevel2JSON(json []byte, name string, coordinates []Position, bbox *BBox, bboxDefined bool) []byte { + if bbox != nil && !bboxDefined { + bbox = nil + } isCordZ := level2IsCoordZDefined(coordinates, bbox) - var buf bytes.Buffer - buf.WriteString(`{"type":"`) - buf.WriteString(name) - buf.WriteString(`","coordinates":[`) + json = append(json, `{"type":"`...) + json = append(json, name...) + json = append(json, `","coordinates":[`...) for i, p := range coordinates { if i > 0 { - buf.WriteByte(',') + json = append(json, ',') } - buf.WriteByte('[') - p.writeJSON(&buf, isCordZ) - buf.WriteByte(']') + json = append(json, '[') + json = appendPositionJSON(json, p, isCordZ) + json = append(json, ']') } - buf.WriteByte(']') - bbox.write(&buf) - buf.WriteByte('}') - return buf.String() + json = append(json, ']') + if bboxDefined { + json = appendBBoxJSON(json, bbox) + } + json = append(json, '}') + return json } func level2IsCoordZDefined(coordinates []Position, bbox *BBox) bool { @@ -259,31 +262,34 @@ func level3PositionCount(coordinates [][]Position, bbox *BBox) int { return res } -func level3JSON(name string, coordinates [][]Position, bbox *BBox) string { +func appendLevel3JSON(json []byte, name string, coordinates [][]Position, bbox *BBox, bboxDefined bool) []byte { + if bbox != nil && !bboxDefined { + bbox = nil + } isCordZ := level3IsCoordZDefined(coordinates, bbox) - var buf bytes.Buffer - buf.WriteString(`{"type":"`) - buf.WriteString(name) - buf.WriteString(`","coordinates":[`) + json = append(json, `{"type":"`...) + json = append(json, name...) + json = append(json, `","coordinates":[`...) for i, p := range coordinates { if i > 0 { - buf.WriteByte(',') + json = append(json, ',') } - buf.WriteByte('[') + json = append(json, '[') for i, p := range p { if i > 0 { - buf.WriteByte(',') + json = append(json, ',') } - buf.WriteByte('[') - p.writeJSON(&buf, isCordZ) - buf.WriteByte(']') + json = append(json, '[') + json = appendPositionJSON(json, p, isCordZ) + json = append(json, ']') } - buf.WriteByte(']') + json = append(json, ']') } - buf.WriteByte(']') - bbox.write(&buf) - buf.WriteByte('}') - return buf.String() + json = append(json, ']') + if bboxDefined { + json = appendBBoxJSON(json, bbox) + } + return append(json, '}') } func level3IsCoordZDefined(coordinates [][]Position, bbox *BBox) bool { @@ -388,38 +394,41 @@ func level4PositionCount(coordinates [][][]Position, bbox *BBox) int { return res } -func level4JSON(name string, coordinates [][][]Position, bbox *BBox) string { +func appendLevel4JSON(json []byte, name string, coordinates [][][]Position, bbox *BBox, bboxDefined bool) []byte { + if bbox != nil && !bboxDefined { + bbox = nil + } isCordZ := level4IsCoordZDefined(coordinates, bbox) - var buf bytes.Buffer - buf.WriteString(`{"type":"`) - buf.WriteString(name) - buf.WriteString(`","coordinates":[`) + json = append(json, `{"type":"`...) + json = append(json, name...) + json = append(json, `","coordinates":[`...) for i, p := range coordinates { if i > 0 { - buf.WriteByte(',') + json = append(json, ',') } - buf.WriteByte('[') + json = append(json, '[') for i, p := range p { if i > 0 { - buf.WriteByte(',') + json = append(json, ',') } - buf.WriteByte('[') + json = append(json, '[') for i, p := range p { if i > 0 { - buf.WriteByte(',') + json = append(json, ',') } - buf.WriteByte('[') - p.writeJSON(&buf, isCordZ) - buf.WriteByte(']') + json = append(json, '[') + json = appendPositionJSON(json, p, isCordZ) + json = append(json, ']') } - buf.WriteByte(']') + json = append(json, ']') } - buf.WriteByte(']') + json = append(json, ']') } - buf.WriteByte(']') - bbox.write(&buf) - buf.WriteByte('}') - return buf.String() + json = append(json, ']') + if bboxDefined { + json = appendBBoxJSON(json, bbox) + } + return append(json, '}') } func level4IsCoordZDefined(coordinates [][][]Position, bbox *BBox) bool { diff --git a/geojson/linestring.go b/geojson/linestring.go index 533a512c..cfd2fa45 100644 --- a/geojson/linestring.go +++ b/geojson/linestring.go @@ -6,6 +6,7 @@ import "github.com/tidwall/tile38/geojson/geohash" type LineString struct { Coordinates []Position BBox *BBox + bboxDefined bool } func fillLineString(coordinates []Position, bbox *BBox, err error) (LineString, error) { @@ -14,9 +15,15 @@ func fillLineString(coordinates []Position, bbox *BBox, err error) (LineString, err = errLineStringInvalidCoordinates } } + bboxDefined := bbox != nil + if !bboxDefined { + cbbox := level2CalculatedBBox(coordinates, nil) + bbox = &cbbox + } return LineString{ Coordinates: coordinates, BBox: bbox, + bboxDefined: bboxDefined, }, err } @@ -46,14 +53,18 @@ func (g LineString) Weight() int { return level2Weight(g.Coordinates, g.BBox) } +func (g LineString) appendJSON(json []byte) []byte { + return appendLevel2JSON(json, "LineString", g.Coordinates, g.BBox, g.bboxDefined) +} + // MarshalJSON allows the object to be encoded in json.Marshal calls. func (g LineString) MarshalJSON() ([]byte, error) { - return []byte(g.JSON()), nil + return g.appendJSON(nil), nil } // JSON is the json representation of the object. This might not be exactly the same as the original. func (g LineString) JSON() string { - return level2JSON("LineString", g.Coordinates, g.BBox) + return string(g.appendJSON(nil)) } // String returns a string representation of the object. This might be JSON or something else. @@ -64,13 +75,14 @@ func (g LineString) String() string { func (g LineString) bboxPtr() *BBox { return g.BBox } + func (g LineString) hasPositions() bool { - return g.BBox != nil || len(g.Coordinates) > 0 + return g.bboxDefined || len(g.Coordinates) > 0 } // WithinBBox detects if the object is fully contained inside a bbox. func (g LineString) WithinBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).InsideRect(rectBBox(bbox)) } return polyPositions(g.Coordinates).InsideRect(rectBBox(bbox)) @@ -78,7 +90,7 @@ func (g LineString) WithinBBox(bbox BBox) bool { // IntersectsBBox detects if the object intersects a bbox. func (g LineString) IntersectsBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).IntersectsRect(rectBBox(bbox)) } return polyPositions(g.Coordinates).IntersectsRect(rectBBox(bbox)) @@ -125,7 +137,7 @@ func (g LineString) Nearby(center Position, meters float64) bool { // IsBBoxDefined returns true if the object has a defined bbox. func (g LineString) IsBBoxDefined() bool { - return g.BBox != nil + return g.bboxDefined } // IsGeometry return true if the object is a geojson geometry object. false if it something else. diff --git a/geojson/multilinestring.go b/geojson/multilinestring.go index 6c328793..25afa842 100644 --- a/geojson/multilinestring.go +++ b/geojson/multilinestring.go @@ -9,6 +9,7 @@ import ( type MultiLineString struct { Coordinates [][]Position BBox *BBox + bboxDefined bool } func fillMultiLineString(coordinates [][]Position, bbox *BBox, err error) (MultiLineString, error) { @@ -20,9 +21,15 @@ func fillMultiLineString(coordinates [][]Position, bbox *BBox, err error) (Multi } } } + bboxDefined := bbox != nil + if !bboxDefined { + cbbox := level3CalculatedBBox(coordinates, nil, false) + bbox = &cbbox + } return MultiLineString{ Coordinates: coordinates, BBox: bbox, + bboxDefined: bboxDefined, }, err } @@ -54,12 +61,16 @@ func (g MultiLineString) Weight() int { // MarshalJSON allows the object to be encoded in json.Marshal calls. func (g MultiLineString) MarshalJSON() ([]byte, error) { - return []byte(g.JSON()), nil + return g.appendJSON(nil), nil +} + +func (g MultiLineString) appendJSON(json []byte) []byte { + return appendLevel3JSON(json, "MultiLineString", g.Coordinates, g.BBox, g.bboxDefined) } // JSON is the json representation of the object. This might not be exactly the same as the original. func (g MultiLineString) JSON() string { - return level3JSON("MultiLineString", g.Coordinates, g.BBox) + return string(g.appendJSON(nil)) } // String returns a string representation of the object. This might be JSON or something else. @@ -71,7 +82,7 @@ func (g MultiLineString) bboxPtr() *BBox { return g.BBox } func (g MultiLineString) hasPositions() bool { - if g.BBox != nil { + if g.bboxDefined { return true } for _, c := range g.Coordinates { @@ -84,7 +95,7 @@ func (g MultiLineString) hasPositions() bool { // WithinBBox detects if the object is fully contained inside a bbox. func (g MultiLineString) WithinBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).InsideRect(rectBBox(bbox)) } if len(g.Coordinates) == 0 { @@ -105,7 +116,7 @@ func (g MultiLineString) WithinBBox(bbox BBox) bool { // IntersectsBBox detects if the object intersects a bbox. func (g MultiLineString) IntersectsBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).IntersectsRect(rectBBox(bbox)) } for _, ls := range g.Coordinates { @@ -183,7 +194,7 @@ func (g MultiLineString) Nearby(center Position, meters float64) bool { // IsBBoxDefined returns true if the object has a defined bbox. func (g MultiLineString) IsBBoxDefined() bool { - return g.BBox != nil + return g.bboxDefined } // IsGeometry return true if the object is a geojson geometry object. false if it something else. diff --git a/geojson/multilinestring_test.go b/geojson/multilinestring_test.go index de2f1988..2f66dee2 100644 --- a/geojson/multilinestring_test.go +++ b/geojson/multilinestring_test.go @@ -1,6 +1,8 @@ package geojson -import "testing" +import ( + "testing" +) func TestMultiLineString(t *testing.T) { testJSON(t, `{"type":"MultiLineString","coordinates":[[[100.1,5.1],[101.1,6.1]],[[102.1,7.1],[103.1,8.1]]]}`) diff --git a/geojson/multipoint.go b/geojson/multipoint.go index 87f66667..f123b887 100644 --- a/geojson/multipoint.go +++ b/geojson/multipoint.go @@ -9,12 +9,19 @@ import ( type MultiPoint struct { Coordinates []Position BBox *BBox + bboxDefined bool } func fillMultiPoint(coordinates []Position, bbox *BBox, err error) (MultiPoint, error) { + bboxDefined := bbox != nil + if !bboxDefined { + cbbox := level2CalculatedBBox(coordinates, nil) + bbox = &cbbox + } return MultiPoint{ Coordinates: coordinates, BBox: bbox, + bboxDefined: bboxDefined, }, err } @@ -46,12 +53,16 @@ func (g MultiPoint) Weight() int { // MarshalJSON allows the object to be encoded in json.Marshal calls. func (g MultiPoint) MarshalJSON() ([]byte, error) { - return []byte(g.JSON()), nil + return g.appendJSON(nil), nil +} + +func (g MultiPoint) appendJSON(json []byte) []byte { + return appendLevel2JSON(json, "MultiPoint", g.Coordinates, g.BBox, g.bboxDefined) } // JSON is the json representation of the object. This might not be exactly the same as the original. func (g MultiPoint) JSON() string { - return level2JSON("MultiPoint", g.Coordinates, g.BBox) + return string(g.appendJSON(nil)) } // String returns a string representation of the object. This might be JSON or something else. @@ -63,12 +74,12 @@ func (g MultiPoint) bboxPtr() *BBox { return g.BBox } func (g MultiPoint) hasPositions() bool { - return g.BBox != nil || len(g.Coordinates) > 0 + return g.bboxDefined || len(g.Coordinates) > 0 } // WithinBBox detects if the object is fully contained inside a bbox. func (g MultiPoint) WithinBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).InsideRect(rectBBox(bbox)) } if len(g.Coordinates) == 0 { @@ -162,7 +173,7 @@ func (g MultiPoint) Nearby(center Position, meters float64) bool { // IsBBoxDefined returns true if the object has a defined bbox. func (g MultiPoint) IsBBoxDefined() bool { - return g.BBox != nil + return g.bboxDefined } // IsGeometry return true if the object is a geojson geometry object. false if it something else. diff --git a/geojson/multipolygon.go b/geojson/multipolygon.go index 944570f5..90752dd2 100644 --- a/geojson/multipolygon.go +++ b/geojson/multipolygon.go @@ -6,6 +6,7 @@ import "github.com/tidwall/tile38/geojson/geohash" type MultiPolygon struct { Coordinates [][][]Position BBox *BBox + bboxDefined bool } func fillMultiPolygon(coordinates [][][]Position, bbox *BBox, err error) (MultiPolygon, error) { @@ -24,9 +25,15 @@ func fillMultiPolygon(coordinates [][][]Position, bbox *BBox, err error) (MultiP } } } + bboxDefined := bbox != nil + if !bboxDefined { + cbbox := level4CalculatedBBox(coordinates, nil) + bbox = &cbbox + } return MultiPolygon{ Coordinates: coordinates, BBox: bbox, + bboxDefined: bboxDefined, }, err } @@ -58,12 +65,16 @@ func (g MultiPolygon) Weight() int { // MarshalJSON allows the object to be encoded in json.Marshal calls. func (g MultiPolygon) MarshalJSON() ([]byte, error) { - return []byte(g.JSON()), nil + return g.appendJSON(nil), nil +} + +func (g MultiPolygon) appendJSON(json []byte) []byte { + return appendLevel4JSON(json, "MultiPolygon", g.Coordinates, g.BBox, g.bboxDefined) } // JSON is the json representation of the object. This might not be exactly the same as the original. func (g MultiPolygon) JSON() string { - return level4JSON("MultiPolygon", g.Coordinates, g.BBox) + return string(g.appendJSON(nil)) } // String returns a string representation of the object. This might be JSON or something else. @@ -75,7 +86,7 @@ func (g MultiPolygon) bboxPtr() *BBox { return g.BBox } func (g MultiPolygon) hasPositions() bool { - if g.BBox != nil { + if g.bboxDefined { return true } for _, c := range g.Coordinates { @@ -90,7 +101,7 @@ func (g MultiPolygon) hasPositions() bool { // WithinBBox detects if the object is fully contained inside a bbox. func (g MultiPolygon) WithinBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).InsideRect(rectBBox(bbox)) } if len(g.Coordinates) == 0 { @@ -192,7 +203,7 @@ func (g MultiPolygon) Nearby(center Position, meters float64) bool { // IsBBoxDefined returns true if the object has a defined bbox. func (g MultiPolygon) IsBBoxDefined() bool { - return g.BBox != nil + return g.bboxDefined } // IsGeometry return true if the object is a geojson geometry object. false if it something else. diff --git a/geojson/object.go b/geojson/object.go index 3d2e425a..5f7c242e 100644 --- a/geojson/object.go +++ b/geojson/object.go @@ -1,12 +1,9 @@ package geojson import ( - "bytes" - "encoding/binary" "encoding/json" "errors" "fmt" - "math" "github.com/tidwall/gjson" "github.com/tidwall/tile38/geojson/poly" @@ -61,6 +58,8 @@ const nilz = 0 type Object interface { bboxPtr() *BBox hasPositions() bool + appendJSON(dst []byte) []byte + // WithinBBox detects if the object is fully contained inside a bbox. WithinBBox(bbox BBox) bool // IntersectsBBox detects if the object intersects a bbox. @@ -93,39 +92,6 @@ type Object interface { IsGeometry() bool } -func writeHeader(buf *bytes.Buffer, objType byte, bbox *BBox, isCordZ bool) { - header := objType - if bbox != nil { - header |= 1 << 4 - if bbox.Min.Z != nilz || bbox.Max.Z != nilz { - header |= 1 << 5 - } - } - if isCordZ { - header |= 1 << 6 - } - buf.WriteByte(header) - if bbox != nil { - b := make([]byte, 8) - binary.LittleEndian.PutUint64(b, math.Float64bits(bbox.Min.X)) - buf.Write(b) - binary.LittleEndian.PutUint64(b, math.Float64bits(bbox.Min.Y)) - buf.Write(b) - if bbox.Min.Z != nilz || bbox.Max.Z != nilz { - binary.LittleEndian.PutUint64(b, math.Float64bits(bbox.Min.Z)) - buf.Write(b) - } - binary.LittleEndian.PutUint64(b, math.Float64bits(bbox.Max.X)) - buf.Write(b) - binary.LittleEndian.PutUint64(b, math.Float64bits(bbox.Max.Y)) - buf.Write(b) - if bbox.Min.Z != nilz || bbox.Max.Z != nilz { - binary.LittleEndian.PutUint64(b, math.Float64bits(bbox.Max.Z)) - buf.Write(b) - } - } -} - func positionBBox(i int, bbox BBox, ps []Position) (int, BBox) { for _, p := range ps { if i == 0 { @@ -219,7 +185,12 @@ func objectMap(json string, from int) (Object, error) { func withinObjectShared(g Object, o Object, pin func(v Polygon) bool, mpin func(v MultiPolygon) bool) bool { bbp := o.bboxPtr() if bbp != nil { - return g.WithinBBox(*bbp) + if !g.WithinBBox(*bbp) { + return false + } + if o.IsBBoxDefined() { + return true + } } switch v := o.(type) { default: @@ -266,7 +237,12 @@ func withinObjectShared(g Object, o Object, pin func(v Polygon) bool, mpin func( func intersectsObjectShared(g Object, o Object, pin func(v Polygon) bool, mpin func(v MultiPolygon) bool) bool { bbp := o.bboxPtr() if bbp != nil { - return g.IntersectsBBox(*bbp) + if !g.IntersectsBBox(*bbp) { + return false + } + if o.IsBBoxDefined() { + return true + } } switch v := o.(type) { default: diff --git a/geojson/point.go b/geojson/point.go index 5b37e01c..8a72afd0 100644 --- a/geojson/point.go +++ b/geojson/point.go @@ -10,6 +10,7 @@ import ( type Point struct { Coordinates Position BBox *BBox + bboxDefined bool } func fillSimplePointOrPoint(coordinates Position, bbox *BBox, err error) (Object, error) { @@ -20,9 +21,15 @@ func fillSimplePointOrPoint(coordinates Position, bbox *BBox, err error) (Object } func fillPoint(coordinates Position, bbox *BBox, err error) (Point, error) { + bboxDefined := bbox != nil + if !bboxDefined { + cbbox := level1CalculatedBBox(coordinates, nil) + bbox = &cbbox + } return Point{ Coordinates: coordinates, BBox: bbox, + bboxDefined: bboxDefined, }, err } @@ -47,12 +54,16 @@ func (g Point) Geohash(precision int) (string, error) { // MarshalJSON allows the object to be encoded in json.Marshal calls. func (g Point) MarshalJSON() ([]byte, error) { - return []byte(g.JSON()), nil + return g.appendJSON(nil), nil +} + +func (g Point) appendJSON(json []byte) []byte { + return appendLevel1JSON(json, "Point", g.Coordinates, g.BBox, g.bboxDefined) } // JSON is the json representation of the object. This might not be exactly the same as the original. func (g Point) JSON() string { - return level1JSON("Point", g.Coordinates, g.BBox) + return string(g.appendJSON(nil)) } // String returns a string representation of the object. This might be JSON or something else. @@ -78,7 +89,7 @@ func (g Point) hasPositions() bool { // WithinBBox detects if the object is fully contained inside a bbox. func (g Point) WithinBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).InsideRect(rectBBox(bbox)) } return poly.Point(g.Coordinates).InsideRect(rectBBox(bbox)) @@ -86,7 +97,7 @@ func (g Point) WithinBBox(bbox BBox) bool { // IntersectsBBox detects if the object intersects a bbox. func (g Point) IntersectsBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).IntersectsRect(rectBBox(bbox)) } return poly.Point(g.Coordinates).InsideRect(rectBBox(bbox)) @@ -133,7 +144,7 @@ func (g Point) Nearby(center Position, meters float64) bool { // IsBBoxDefined returns true if the object has a defined bbox. func (g Point) IsBBoxDefined() bool { - return g.BBox != nil + return g.bboxDefined } // IsGeometry return true if the object is a geojson geometry object. false if it something else. diff --git a/geojson/polygon.go b/geojson/polygon.go index 463e33c0..a4170fe4 100644 --- a/geojson/polygon.go +++ b/geojson/polygon.go @@ -1,16 +1,12 @@ package geojson -import ( - "bytes" - "strconv" - - "github.com/tidwall/tile38/geojson/geohash" -) +import "github.com/tidwall/tile38/geojson/geohash" // Polygon is a geojson object with the type "Polygon" type Polygon struct { Coordinates [][]Position BBox *BBox + bboxDefined bool } func fillPolygon(coordinates [][]Position, bbox *BBox, err error) (Polygon, error) { @@ -27,9 +23,15 @@ func fillPolygon(coordinates [][]Position, bbox *BBox, err error) (Polygon, erro } } } + bboxDefined := bbox != nil + if !bboxDefined { + cbbox := level3CalculatedBBox(coordinates, nil, true) + bbox = &cbbox + } return Polygon{ Coordinates: coordinates, BBox: bbox, + bboxDefined: bboxDefined, }, err } @@ -61,12 +63,16 @@ func (g Polygon) Weight() int { // MarshalJSON allows the object to be encoded in json.Marshal calls. func (g Polygon) MarshalJSON() ([]byte, error) { - return []byte(g.JSON()), nil + return g.appendJSON(nil), nil +} + +func (g Polygon) appendJSON(json []byte) []byte { + return appendLevel3JSON(json, "Polygon", g.Coordinates, g.BBox, g.bboxDefined) } // JSON is the json representation of the object. This might not be exactly the same as the original. func (g Polygon) JSON() string { - return level3JSON("Polygon", g.Coordinates, g.BBox) + return string(g.appendJSON(nil)) } // String returns a string representation of the object. This might be JSON or something else. @@ -78,7 +84,7 @@ func (g Polygon) bboxPtr() *BBox { return g.BBox } func (g Polygon) hasPositions() bool { - if g.BBox != nil { + if g.bboxDefined { return true } for _, c := range g.Coordinates { @@ -91,7 +97,7 @@ func (g Polygon) hasPositions() bool { // WithinBBox detects if the object is fully contained inside a bbox. func (g Polygon) WithinBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).InsideRect(rectBBox(bbox)) } if len(g.Coordinates) == 0 { @@ -110,7 +116,7 @@ func (g Polygon) WithinBBox(bbox BBox) bool { // IntersectsBBox detects if the object intersects a bbox. func (g Polygon) IntersectsBBox(bbox BBox) bool { - if g.BBox != nil { + if g.bboxDefined { return rectBBox(g.CalculatedBBox()).IntersectsRect(rectBBox(bbox)) } if len(g.Coordinates) == 0 { @@ -178,46 +184,9 @@ func (g Polygon) Nearby(center Position, meters float64) bool { return nearbyObjectShared(g, center.X, center.Y, meters) } -// KML outputs kml -func (g Polygon) KML() string { - var buf bytes.Buffer - buf.WriteString(``) - buf.WriteString(``) - buf.WriteString(``) - buf.WriteString(``) - buf.WriteString(`1`) - buf.WriteString(`relativeToGround`) - for i, c := range g.Coordinates { - if i == 0 { - buf.WriteString(``) - } else { - buf.WriteString(``) - } - buf.WriteString(``) - buf.WriteString(``) - for _, c := range c { - buf.WriteString("\n" + strconv.FormatFloat(c.X, 'f', -1, 64) + `,` + strconv.FormatFloat(c.Y, 'f', -1, 64) + `,` + strconv.FormatFloat(c.Z, 'f', -1, 64)) - } - if len(c) > 0 { - buf.WriteString("\n") - } - buf.WriteString(``) - buf.WriteString(``) - if i == 0 { - buf.WriteString(``) - } else { - buf.WriteString(``) - } - } - buf.WriteString(``) - buf.WriteString(``) - buf.WriteString(``) - return buf.String() -} - // IsBBoxDefined returns true if the object has a defined bbox. func (g Polygon) IsBBoxDefined() bool { - return g.BBox != nil + return g.bboxDefined } // IsGeometry return true if the object is a geojson geometry object. false if it something else. diff --git a/geojson/position.go b/geojson/position.go index f5d2b35a..51e70dcc 100644 --- a/geojson/position.go +++ b/geojson/position.go @@ -1,7 +1,6 @@ package geojson import ( - "bytes" "encoding/binary" "math" "strconv" @@ -38,26 +37,15 @@ func polyExteriorHoles(positions [][]Position) (exterior poly.Polygon, holes []p return } -func (p Position) writeJSON(buf *bytes.Buffer, isCordZ bool) { - buf.WriteString(strconv.FormatFloat(p.X, 'f', -1, 64)) - buf.WriteByte(',') - buf.WriteString(strconv.FormatFloat(p.Y, 'f', -1, 64)) +func appendPositionJSON(json []byte, p Position, isCordZ bool) []byte { + json = strconv.AppendFloat(json, p.X, 'f', -1, 64) + json = append(json, ',') + json = strconv.AppendFloat(json, p.Y, 'f', -1, 64) if isCordZ { - buf.WriteByte(',') - buf.WriteString(strconv.FormatFloat(p.Z, 'f', -1, 64)) - } -} - -func (p Position) writeBytes(buf *bytes.Buffer, isCordZ bool) { - b := make([]byte, 8) - binary.LittleEndian.PutUint64(b, math.Float64bits(p.X)) - buf.Write(b) - binary.LittleEndian.PutUint64(b, math.Float64bits(p.Y)) - buf.Write(b) - if isCordZ { - binary.LittleEndian.PutUint64(b, math.Float64bits(p.Z)) - buf.Write(b) + json = append(json, ',') + json = strconv.AppendFloat(json, p.Z, 'f', -1, 64) } + return json } const earthRadius = 6371e3 diff --git a/geojson/simplepoint.go b/geojson/simplepoint.go index f39439ee..3322b783 100644 --- a/geojson/simplepoint.go +++ b/geojson/simplepoint.go @@ -51,12 +51,16 @@ func (g SimplePoint) Weight() int { // MarshalJSON allows the object to be encoded in json.Marshal calls. func (g SimplePoint) MarshalJSON() ([]byte, error) { - return []byte(g.JSON()), nil + return g.appendJSON(nil), nil +} + +func (g SimplePoint) appendJSON(json []byte) []byte { + return appendLevel1JSON(json, "Point", Position{X: g.X, Y: g.Y, Z: 0}, nil, false) } // JSON is the json representation of the object. This might not be exactly the same as the original. func (g SimplePoint) JSON() string { - return level1JSON("Point", Position{X: g.X, Y: g.Y, Z: 0}, nil) + return string(g.appendJSON(nil)) } // String returns a string representation of the object. This might be JSON or something else. diff --git a/geojson/string.go b/geojson/string.go index 716f43e6..24372297 100644 --- a/geojson/string.go +++ b/geojson/string.go @@ -45,10 +45,14 @@ func (s String) CalculatedPoint() Position { return Position{} } +func (s String) appendJSON(json []byte) []byte { + b, _ := s.MarshalJSON() + return append(json, b...) +} + // JSON is the json representation of the object. This might not be exactly the same as the original. func (s String) JSON() string { - b, _ := s.MarshalJSON() - return string(b) + return string(s.appendJSON(nil)) } // String returns a string representation of the object. This might be JSON or something else. @@ -57,7 +61,7 @@ func (s String) String() string { } // IsGeometry return true if the object is a geojson geometry object. false if it something else. -func (g String) IsGeometry() bool { +func (s String) IsGeometry() bool { return false }