Skip to content

Commit 09f837c

Browse files
authored
round rapidjson, round z in geobuf (#17)
* add round_z * fix Encoder default constructor * minor fix * round rapidjson * update * fix windows
1 parent 287d6cc commit 09f837c

File tree

9 files changed

+173
-39
lines changed

9 files changed

+173
-39
lines changed

docs/about/release-notes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ To upgrade `pybind11-geobuf` to the latest version, use pip:
1010
pip install -U pybind11-geobuf
1111
```
1212

13+
## Version 0.1.0 (2023-04-01)
14+
15+
* round z in geobuf encoding
16+
* round rapidjson
17+
1318
## Version 0.0.9 (2023-03-30)
1419

1520
* Export to geobuf with only xy (no z), `only_xy=False`

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def build_extension(self, ext):
127127
# logic and declaration, and simpler if you include description/version in a file.
128128
setup(
129129
name="pybind11_geobuf",
130-
version="0.0.9",
130+
version="0.1.0",
131131
author="tzx",
132132
author_email="dvorak4tzx@gmail.com",
133133
url="https://geobuf-cpp.readthedocs.io",

src/geobuf/geobuf.cpp

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ std::string Encoder::encode(const mapbox::geojson::geojson &geojson)
221221
keys.clear();
222222
analyze(geojson);
223223
if (onlyXY) {
224-
dim = 2u;
224+
dim = dimXY;
225225
}
226226

227227
{
@@ -357,7 +357,7 @@ inline double ROUND(double v, double s)
357357

358358
void Encoder::analyzePoint(const mapbox::geojson::point &point)
359359
{
360-
if (!onlyXY) {
360+
if (!onlyXY && dim < dimXYZ) {
361361
dim = std::max(point.z == 0 ? dimXY : dimXYZ, dim);
362362
}
363363
if (e >= maxPrecision) {
@@ -509,13 +509,14 @@ void Encoder::writeValue(const mapbox::feature::value &value, Encoder::Pbf &pbf)
509509

510510
void Encoder::writePoint(const mapbox::geojson::point &point, Encoder::Pbf &pbf)
511511
{
512-
std::vector<int64_t> coords;
513-
coords.reserve(dim);
514-
const double *ptr = &point.x;
515-
for (int i = 0; i < dim; ++i) {
516-
coords.push_back(static_cast<int64_t>(std::floor(ptr[i] * e + 0.5)));
512+
std::array<int64_t, 3> coords;
513+
coords[0] = static_cast<int64_t>(std::floor(point.x * e + 0.5));
514+
coords[1] = static_cast<int64_t>(std::floor(point.y * e + 0.5));
515+
if (dim == 3) {
516+
double z = zScale ? ROUND(point.z, *zScale) : point.z;
517+
coords[2] = static_cast<int64_t>(std::floor(z * e + 0.5));
517518
}
518-
pbf.add_packed_sint64(3, coords.begin(), coords.end());
519+
pbf.add_packed_sint64(3, &coords[0], &coords[0] + dim);
519520
}
520521

521522
void Encoder::writeLine(const PointsType &line, Encoder::Pbf &pbf)
@@ -579,12 +580,18 @@ void Encoder::populateLine(std::vector<int64_t> &coords, //
579580
int len = line.size() - (closed ? 1 : 0);
580581
auto sum = std::array<int64_t, 3>{0, 0, 0};
581582
for (int i = 0; i < len; ++i) {
582-
const double *ptr = &line[i].x;
583-
for (int j = 0; j < dim; ++j) {
584-
auto n =
585-
static_cast<int64_t>(std::floor(ptr[j] * e + 0.5)) - sum[j];
583+
const auto &pt = line[i];
584+
auto n = static_cast<int64_t>(std::floor(pt.x * e + 0.5)) - sum[0];
585+
coords.push_back(n);
586+
sum[0] += n;
587+
n = static_cast<int64_t>(std::floor(pt.y * e + 0.5)) - sum[1];
588+
coords.push_back(n);
589+
sum[1] += n;
590+
if (dim == 3) {
591+
double z = zScale ? ROUND(pt.z, *zScale) : pt.z;
592+
n = static_cast<int64_t>(std::floor(z * e + 0.5)) - sum[2];
586593
coords.push_back(n);
587-
sum[j] += n;
594+
sum[2] += n;
588595
}
589596
}
590597
}

src/geobuf/geobuf.hpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <protozero/pbf_reader.hpp>
88

99
#include <map>
10+
#include <optional>
1011
#include <unordered_map>
1112

1213
#define MAPBOX_GEOBUF_DEFAULT_PRECISION 6
@@ -81,8 +82,11 @@ struct Encoder
8182
using Pbf = protozero::pbf_writer;
8283
Encoder(uint32_t maxPrecision = std::pow(10,
8384
MAPBOX_GEOBUF_DEFAULT_PRECISION),
84-
bool onlyXY = false)
85-
: maxPrecision(maxPrecision), onlyXY(onlyXY)
85+
bool onlyXY = false, //
86+
std::optional<int> roundZ = std::nullopt)
87+
: maxPrecision(maxPrecision), onlyXY(onlyXY),
88+
zScale(roundZ ? std::make_optional(std::pow(10, *roundZ))
89+
: std::nullopt)
8690
{
8791
}
8892
std::string encode(const mapbox::geojson::geojson &geojson);
@@ -103,6 +107,14 @@ struct Encoder
103107
std::string encode(const RapidjsonValue &json);
104108
bool encode(const std::string &input_path, const std::string &output_path);
105109

110+
auto __maxPrecision() const { return maxPrecision; }
111+
auto __onlyXY() const { return onlyXY; }
112+
auto __roundZ() const
113+
{
114+
return zScale ? std::make_optional(std::log10(*zScale)) : std::nullopt;
115+
}
116+
auto __dim() const { return dim; }
117+
auto __e() const { return e; }
106118
std::map<std::string, std::uint32_t> __keys() const
107119
{
108120
return std::map<std::string, std::uint32_t>(keys.begin(), keys.end());
@@ -140,6 +152,7 @@ struct Encoder
140152

141153
const uint32_t maxPrecision;
142154
const bool onlyXY;
155+
const std::optional<double> zScale;
143156
uint32_t dim = MAPBOX_GEOBUF_DEFAULT_DIM;
144157
uint32_t e = 1;
145158
std::unordered_map<std::string, std::uint32_t> keys;

src/geobuf/rapidjson_helpers.hpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,36 @@ inline void sort_keys_inplace(RapidjsonValue &json)
6868
}
6969
}
7070

71+
inline void round_rapidjson(RapidjsonValue &json, double scale, int depth = 1,
72+
const std::vector<std::string> &skip_keys = {})
73+
{
74+
if (--depth < 0) {
75+
return;
76+
}
77+
if (json.IsArray()) {
78+
for (auto &e : json.GetArray()) {
79+
round_rapidjson(e, scale, depth, skip_keys);
80+
}
81+
} else if (json.IsObject()) {
82+
auto obj = json.GetObject();
83+
for (auto &kv : obj) {
84+
if (!skip_keys.empty() &&
85+
std::find(skip_keys.begin(), skip_keys.end(),
86+
std::string(kv.name.GetString(),
87+
kv.name.GetStringLength())) !=
88+
skip_keys.end()) {
89+
continue;
90+
}
91+
round_rapidjson(kv.value, scale, depth, skip_keys);
92+
}
93+
} else if (json.IsDouble()) {
94+
// see round_coords in geojson_helpers
95+
json.SetDouble(std::floor(json.GetDouble() * scale + 0.5) / scale);
96+
} else if (json.IsFloat()) {
97+
json.SetFloat(std::floor(json.GetFloat() * scale + 0.5) / scale);
98+
}
99+
}
100+
71101
inline RapidjsonValue sort_keys(const RapidjsonValue &json)
72102
{
73103
RapidjsonAllocator allocator;

src/main.cpp

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,20 @@ PYBIND11_MODULE(_pybind11_geobuf, m)
9191
py::kw_only(), //
9292
"indent"_a = "");
9393

94-
py::class_<Encoder>(m, "Encoder", py::module_local()) //
95-
.def(py::init<uint32_t, bool>(), //
94+
py::class_<Encoder>(m, "Encoder", py::module_local()) //
95+
.def(py::init<uint32_t, bool, std::optional<int>>(), //
9696
py::kw_only(),
97-
"max_precision"_a = std::pow(10, MAPBOX_GEOBUF_DEFAULT_PRECISION),
98-
"only_xy"_a = false)
97+
"max_precision"_a = static_cast<uint32_t>(
98+
std::pow(10, MAPBOX_GEOBUF_DEFAULT_PRECISION)),
99+
"only_xy"_a = false, //
100+
"round_z"_a = std::nullopt)
99101
//
102+
.def("max_precision", &Encoder::__maxPrecision)
103+
.def("only_xy", &Encoder::__onlyXY)
104+
.def("round_z", &Encoder::__roundZ)
105+
.def("dim", &Encoder::__dim)
106+
.def("e", &Encoder::__e)
107+
.def("keys", &Encoder::__keys)
100108
.def(
101109
"encode",
102110
[](Encoder &self, const mapbox::geojson::geojson &geojson) {

src/pybind11_geojson.cpp

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,17 @@ void bind_geojson(py::module &geojson)
134134
rvp::reference_internal)
135135
.def(
136136
"to_geobuf",
137-
[](const mapbox::geojson::geojson &self, int precision,
138-
bool only_xy) {
137+
[](const mapbox::geojson::geojson &self, //
138+
int precision, bool only_xy, std::optional<int> round_z) {
139139
auto bytes = mapbox::geobuf::Encoder(
140-
std::pow(10, precision), only_xy)
140+
std::pow(10, precision), only_xy, round_z)
141141
.encode(self);
142142
return py::bytes(bytes);
143143
},
144-
py::kw_only(), "precision"_a = 8, "only_xy"_a = false)
144+
py::kw_only(), //
145+
"precision"_a = 8, //
146+
"only_xy"_a = false, //
147+
"round_z"_a = std::nullopt)
145148
//
146149
.def(
147150
"load",
@@ -503,12 +506,16 @@ void bind_geojson(py::module &geojson)
503506
},
504507
rvp::reference_internal)
505508
.def("to_geobuf",
506-
[](const mapbox::geojson::geometry &self, int precision, bool only_xy) {
509+
[](const mapbox::geojson::geometry &self, //
510+
int precision, bool only_xy, std::optional<int> round_z) {
507511
auto bytes =
508-
mapbox::geobuf::Encoder(std::pow(10, precision), only_xy)
512+
mapbox::geobuf::Encoder(std::pow(10, precision), only_xy, round_z)
509513
.encode(self);
510514
return py::bytes(bytes);
511-
}, py::kw_only(), "precision"_a = 8, "only_xy"_a = false)
515+
}, py::kw_only(), //
516+
"precision"_a = 8, //
517+
"only_xy"_a = false, //
518+
"round_z"_a = std::nullopt)
512519
.def("load",
513520
[](mapbox::geojson::geometry &self, const std::string &path) -> mapbox::geojson::geometry & {
514521
if (endswith(path, ".pbf")) {
@@ -1763,14 +1770,17 @@ void bind_geojson(py::module &geojson)
17631770
rvp::reference_internal)
17641771
.def(
17651772
"to_geobuf",
1766-
[](const mapbox::geojson::feature &self, int precision,
1767-
bool only_xy) {
1768-
auto bytes =
1769-
mapbox::geobuf::Encoder(std::pow(10, precision), only_xy)
1770-
.encode(self);
1773+
[](const mapbox::geojson::feature &self, //
1774+
int precision, bool only_xy, std::optional<int> round_z) {
1775+
auto bytes = mapbox::geobuf::Encoder(std::pow(10, precision),
1776+
only_xy, round_z)
1777+
.encode(self);
17711778
return py::bytes(bytes);
17721779
},
1773-
py::kw_only(), "precision"_a = 8, "only_xy"_a = false)
1780+
py::kw_only(), //
1781+
"precision"_a = 8, //
1782+
"only_xy"_a = false, //
1783+
"round_z"_a = std::nullopt)
17741784
.def(
17751785
"load",
17761786
[](mapbox::geojson::feature &self,
@@ -1902,14 +1912,17 @@ void bind_geojson(py::module &geojson)
19021912
rvp::reference_internal)
19031913
.def(
19041914
"to_geobuf",
1905-
[](const mapbox::geojson::feature_collection &self, int precision,
1906-
bool only_xy) {
1907-
auto bytes =
1908-
mapbox::geobuf::Encoder(std::pow(10, precision), only_xy)
1909-
.encode(self);
1915+
[](const mapbox::geojson::feature_collection &self, //
1916+
int precision, bool only_xy, std::optional<int> round_z) {
1917+
auto bytes = mapbox::geobuf::Encoder(std::pow(10, precision),
1918+
only_xy, round_z)
1919+
.encode(self);
19101920
return py::bytes(bytes);
19111921
},
1912-
py::kw_only(), "precision"_a = 8, "only_xy"_a = false)
1922+
py::kw_only(), //
1923+
"precision"_a = 8, //
1924+
"only_xy"_a = false, //
1925+
"round_z"_a = std::nullopt)
19131926
.def(
19141927
"load",
19151928
[](mapbox::geojson::feature_collection &self,

src/pybind11_rapidjson.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ void bind_rapidjson(py::module &m)
145145
sort_keys_inplace(self);
146146
return self;
147147
}, rvp::reference_internal)
148+
.def("round", [](RapidjsonValue &self, double precision, int depth, //
149+
const std::vector<std::string> &skip_keys) -> RapidjsonValue & {
150+
round_rapidjson(self, std::pow(10, precision), depth, skip_keys);
151+
return self;
152+
}, rvp::reference_internal, py::kw_only(), //
153+
"precision"_a = 3, //
154+
"depth"_a = 32, //
155+
"skip_keys"_a = std::vector<std::string>{})
148156
.def(
149157
"get",
150158
[](RapidjsonValue &self,

tests/test_geobuf.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,44 @@ def test_geobuf():
6262
print(str2geojson2str(json.dumps(geojson), indent=True, sort_keys=True))
6363

6464
# precision: 6(default), 7, 8(recommand), 9
65+
encoder = Encoder()
66+
assert encoder.max_precision() == 10**6
67+
assert not encoder.only_xy()
68+
assert encoder.round_z() is None
69+
6570
encoder = Encoder(max_precision=int(10**8))
71+
assert encoder.max_precision() == 10**8
6672
encoded = encoder.encode(geojson=json.dumps(geojson))
6773
print("encoded pbf bytes")
6874
print(pbf_decode(encoded))
6975

76+
encoder = Encoder(round_z=3)
77+
assert encoder.round_z() == 3
78+
79+
with pytest.raises(Exception) as excinfo:
80+
Encoder(max_precision=int(10**10)) # uint32_t overflow
81+
assert "incompatible constructor arguments" in repr(excinfo)
82+
7083
decoder = Decoder()
7184
geojson_text = decoder.decode(encoded, indent=True)
7285
print(geojson_text)
7386

7487

88+
def test_geobuf_roundz():
89+
f = geojson.Feature().from_rapidjson(sample_geojson())
90+
llas0 = geojson.Feature().from_geobuf(f.to_geobuf()).to_numpy()
91+
92+
llas = geojson.Feature().from_geobuf(f.to_geobuf(precision=3)).to_numpy()
93+
assert np.all(np.round(llas, 3) == llas)
94+
95+
llas = geojson.Feature().from_geobuf(f.to_geobuf(only_xy=True)).to_numpy()
96+
assert np.all(llas[:, 2] == np.zeros(len(llas)))
97+
98+
llas = geojson.Feature().from_geobuf(f.to_geobuf(round_z=3)).to_numpy()
99+
assert np.all(np.round(llas0[:, :2], 8) == llas[:, :2])
100+
assert np.all(np.round(llas0[:, 2], 3) == llas[:, 2])
101+
102+
75103
def test_rapidjson_empty():
76104
assert not bool(rapidjson())
77105
assert not bool(rapidjson([]))
@@ -247,6 +275,28 @@ def test_rapidjson_sort_dump():
247275
assert obj6 == obj5
248276

249277

278+
def test_rapidjson_round():
279+
arr = rapidjson([1.23456, [3.2, 2.345]])
280+
assert arr() == [1.23456, [3.2, 2.345]]
281+
arr.round()
282+
assert arr() == [1.235, [3.2, 2.345]]
283+
arr.round(precision=2)
284+
assert arr() == [1.24, [3.2, 2.35]]
285+
286+
obj = rapidjson(sample_geojson())
287+
assert obj() == sample_geojson()
288+
assert (
289+
obj.clone().round(precision=1).dumps(sort_keys=True)
290+
== '{"geometry":{"coordinates":[[120.4,31.4,1.1],[120.3,31.3,2.2],[120.4,31.2,3.3],[120.7,31.3,4.4]],"extra_key":"extra_value","type":"LineString"},"my_key":"my_value","properties":{"dict":{"key":42,"value":3.1},"double":3.1,"int":42,"int2":-101,"list":["a","list","is","a","list"],"string":"string"},"type":"Feature"}' # noqa
291+
)
292+
assert (
293+
obj.clone()
294+
.round(precision=1, skip_keys=["geometry"])
295+
.dumps(sort_keys=True) # noqa
296+
== '{"geometry":{"coordinates":[[120.40317479950272,31.416966084052177,1.111111],[120.28451900911591,31.30578266928819,2.22],[120.35592249359615,31.21781895672254,3.3333333333333],[120.67093786630113,31.299502266522722,4.4]],"extra_key":"extra_value","type":"LineString"},"my_key":"my_value","properties":{"dict":{"key":42,"value":3.1},"double":3.1,"int":42,"int2":-101,"list":["a","list","is","a","list"],"string":"string"},"type":"Feature"}' # noqa
297+
)
298+
299+
250300
def test_geojson_point():
251301
# as_numpy
252302
g1 = geojson.Point()

0 commit comments

Comments
 (0)