Guidance on translating GeoJSON from/to Boost Geometry
Hi Stephen,
Thank you for the awesome library, however I am struggling to write GeoJSON from Boost Geometry and need guidance.
Given a generic Point of dimension N and using the design rationale defined in Boost Geometry docs, where the components of the points are accessed through free functions boost::geometry::get<N>(Point p) how to model writing it to JSON to have an output such as for a 3D point:
{
"type" : "Point",
"coordinates" : [3.4,2.3,1.2]
}
The current hurdles are:
- Custom Read/Write example in README.md https://github.com/stephenberry/glaze/blob/main/README.md?plain=1#L360 uses member functions and not free functions.
- The examples for custom writing & reading (https://github.com/stephenberry/glaze/blob/main/docs/custom-serialization.md) has a
JSONidentifier which can't be resolved in version 3.4.1.
So please, can you give some guidance on how one would model generic Points, Linestrings, MultiLinestrings, etc... of Boost Geometry with Glaze in order to produce GeoJSON geometries? https://datatracker.ietf.org/doc/html/rfc7946
I currently work around the custom serialization with the generic json type glz::json_t json; and parse the strings into my model classes, but I believe you would be able to provide a more elegant way to solve the problem with a generic implementation, for which I am eager to learn.
Thank you,
Reference of a Linestring:
{
"type" : "LineString",
"coordinates" : [
[13.5, 43, 35.8],
[10.73, 59.92,3.0]
]
}
Reference of a MultiLinestring:
{
"type" : "MultiLineString",
"coordinates" : [
[[12.1, 40.3], [11.42, 60.34]],
[[43.5, 9.8], [-32.23, 23.40]]
]
}
Version 3.5 changed the custom API from glz::detail::to_json to glz::detail::to<JSON. If you look back on the documentation under the tag v3.4.1 you'll see the old documentation if you're planning to stay on that version.
However, rather than using the to/from customization. I think you should be able to use glz::custom. The example you pointed to uses member functions, but you can actually use lambda functions defined within the glz::meta.
Consider this example:
struct custom_buffer_input
{
std::string str{};
};
template <>
struct glz::meta<custom_buffer_input>
{
static constexpr auto read_x = [](custom_buffer_input& s, const std::string& input) { s.str = input; };
static constexpr auto write_x = [](auto& s) -> auto& { return s.str; };
static constexpr auto value = glz::object("str", glz::custom<read_x, write_x>);
};
This is defining lambda functions for how to read/write. The auto& s refers to self, which will be a reference to the struct referred to by the meta. The read_x shows how you can handle the input if you need to, but it is an option parameter.
I'm not familiar with Boost Geometry, so I'd have to do more research to figure out a precise solution. Let me know if this is enough direction to help you solve the translation. And, ask any more questions that you have.
Thank you so much for your prompt reply.
Implementation did improve towards a more generic type, although I am not able to handle yet varying number of Dimensions of the point, here is the current implementation for a 2D point:
template<typename Point> requires boost::geometry::util::is_point<Point>::value
struct glz::meta<Point> {
static constexpr auto read_coordinates = [](Point &s, const std::array<typename boost::geometry::traits::coordinate_type<Point>::type, boost::geometry::traits::dimension<Point>::value> &coords) {
boost::geometry::set<0>(s, coords[0]);
boost::geometry::set<1>(s, coords[1]);
};
static constexpr auto write_coordinates = [](auto const &s) -> auto {
return std::array {boost::geometry::get<0>(s), boost::geometry::get<1>(s)};
};
static constexpr auto value = glz::object("coordinates", glz::custom<read_coordinates, write_coordinates>);
};
This results in the following JSON:
{
"coordinates": [2.4,5.6]
}
Now I'm missing two pieces of this puzzle:
1. Handle N dimension points
I think I would need something along the lines of the below, for which I am not able to get the syntax yet.
template<int Dim>
static constexpr auto get_coordinates(auto const &p) {
return boost::geometry::get<Dim>(p), get_coordinates<Dim - 1>(p);
}
template<>
static constexpr auto get_coordinates<0>(auto const &p) {
return boost::geometry::get<0>(p)
}
static constexpr auto write_coordinates = [](auto const &s) -> auto {
return std::array {get_coordinates<boost::geometry::tratis::dimension<Point>::value>(s)...};
};
// something analogous for the read the JSON into the object
2. The "type" attribute
I would need to put this "type" attribute as a constant when writing the JSON, but discard it (or validate it?) when reading the JSON.
This would allow the wellformed GeoJSON such as
{
"type" : "Point",
"coordinates" : [3.4,2.3,1.2]
}
Any tips on how to cover these two gaps? Any material you would recommend to read up on Meta programming for C++20?
This task seems a little daunting for new comers.
Thanks for any help you can provide.
This additional info is helpful. I think because of the specificity needed here, that specializing to/from functions is the way to go.
Consider this code for the Eigen C++ library:
template <matrix_t T>
requires(T::RowsAtCompileTime >= 0 && T::ColsAtCompileTime >= 0)
struct from<JSON, T>
{
template <auto Opts>
static void op(auto& value, is_context auto&& ctx, auto&& it, auto&& end)
{
std::span<typename T::Scalar, T::RowsAtCompileTime * T::ColsAtCompileTime> view(value.data(), value.size());
detail::read<JSON>::op<Opts>(view, ctx, it, end);
}
};
template <matrix_t T>
requires(T::RowsAtCompileTime >= 0 && T::ColsAtCompileTime >= 0)
struct to<JSON, T>
{
template <auto Opts>
static void op(auto&& value, is_context auto&& ctx, auto&& b, auto&& ix) noexcept
{
std::span<typename T::Scalar, T::RowsAtCompileTime * T::ColsAtCompileTime> view(value.data(), value.size());
detail::write<JSON>::op<Opts>(view, ctx, b, ix);
}
};
Notice how the data is converted to a std::span, which the rest of Glaze then knows how to work with. Is there a way to convert your coordinates into a std::span kind of like this? It seems like Eigen here, you know the dimensions at compile time.
This would just encode/decode to a JSON array. The object structure you can then define higher level with fields like "type" and "coordinates".
The handling of "type" : "Point" can be done by making wrapper structs that just takes a reference to the coordinates type.
// This may need to be templated on CoordinatesType
struct coordinates_write
{
static constexpr std::string_view type = "Point";
CoordinatesType& coordinates;
};
template <>
struct glz::meta<coordinates_write>
{
using T = coordinates_write;
static constexpr auto value = object(&T::type, &T:: coordinates);
};
struct coordinates_read
{
CoordinatesType& coordinates;
};
template <>
struct glz::meta< coordinates_read >
{
using T = coordinates_read;
static constexpr auto value = object(&T:: coordinates);
};
You don't need to use this in the rest of your codebase, just within your glz::custom for your Point:
template<typename Point> requires boost::geometry::util::is_point<Point>::value
struct glz::meta<Point> {
static constexpr auto read_coordinates = [](Point &s) {
return coordinates_read{s};
};
static constexpr auto write_coordinates = [](auto const &s) {
return coordinates_write{s};
};
static constexpr auto value{glz::custom<read_coordinates, write_coordinates>};
};
Does this make sense? Let me know what is confusing. As far as meta-programming for C++20, just try to get up to speed on C++20 concepts and refresh on template specialization. Understanding those two features will help a ton. I'm not sure of a particularly good read off the top of my head, but if you do some searching on those topics you should find some good documentation.
Upon looking at https://en.cppreference.com/w/cpp/container/span ; span models contiguous sequence of objects with the first element of the sequence at position zero. Boost Geometry on the other hand, makes no assumption about the layout of the coordinates in memory, but rather requires traits specialization to access different dimensions of a point. Hence I don't think std::span is an option.
An example of these traits follows below which models a Geo referenced Point with latitude & longitude:
template<>
struct access<Point, 0> { // accessing the 0 dimension, here the X axis
static inline CoordinateType get(Point const &p) { return p.longitude(); }
static inline void set(Point &p, CoordinateType const &value) { p.set_longitude(value); }
};
template<>
struct access<Point, 1> { // accessing the 1 dimension, here the Y axis
static inline CoordinateType get(Point const &p) { return p.latitude(); }
static inline void set(Point &p, CoordinateType const &value) { p.set_latitude(value); }
};
This is the reason I was trying the approach of using the template recursion, where one specializes the last recursion step, to break out of the recursion. However I'm struggling to get the syntax right in order to have a compile time array returned (or a tuple?) with varying number of elements defined at compile time.
I will dig some more in the to<JSON, T> and from<JSON, T> approaches and see how can this template recursion can be achieved there.
For type handling "type" : "Point", yes, the wrapper type is an option, thank you.
Edit: was able to progress with the code below which handles N dimension points:
template<typename Point, int Dim>
struct write_coordinates {
template <auto Opts>
static void apply(Point& value, auto&&... args) {
write_coordinates<Point, Dim-1>::template apply<Opts>(value, args...);
write<JSON>::op<Opts>(boost::geometry::get<Dim>(value), args...);
}
};
template<typename Point>
struct write_coordinates<Point, 0> {
template <auto Opts>
static void apply(Point& value, auto&&... args) {
write<JSON>::op<Opts>(boost::geometry::get<0>(value), args...);
}
};
template <typename Point > requires boost::geometry::util::is_point<Point>::value
struct to<JSON, Point>
{
template <auto Opts>
static void op(Point& value, auto&&... args) noexcept
{
write_coordinates<Point, boost::geometry::traits::dimension<Point>::value-1>::template apply<Opts>(value, args...);
}
};
Which converts a point with coordinates 2.4 and 5.6 to:
2.45.6
Cool! args... can be broken out as glz::context&& ctx, B&& b, auto&& ix.
To write out a bracket for your array use: dump<'['>(b, ix);. This will allocate more memory in the output buffer only when needed.
You can do the same for commas and the closing bracket. I think this should then be sufficient for writing.
Thank you, dump<'['>(b, ix) is very helpful!
Would it make any sense to make the Format a template argument?
Right now I have the following, with uint32_t Format as a template argument, in the hopes of a format agnostic implementation:
template<uint32_t Format, typename Point, int Dim>
struct write_coordinates {
template <auto Opts>
static void apply(Point& value, auto&&... args) {
write_coordinates<Format, Point, Dim-1>::template apply<Opts>(value, args...);
write<Format>::template op<Opts>(boost::geometry::get<Dim>(value), args...);
}
};
template<uint32_t Format, typename Point>
struct write_coordinates<Format, Point, 0> {
template <auto Opts>
static void apply(Point& value, auto&&... args) {
write<Format>::template op<Opts>(boost::geometry::get<0>(value), args...);
}
};
template <typename Point > requires boost::geometry::util::is_point<Point>::value
struct to<JSON, Point>
{
template <auto Opts>
static void op(Point& value, auto&&... args) noexcept
{
write_coordinates<JSON, Point, boost::geometry::traits::dimension<Point>::value-1>::template apply<Opts>(value, args...);
}
};
The Format field is only really needed if you're going to implement BEVE, CSV, or other formats. Adding it will let you more easily support other formats if you want to in the future. Glaze also intends to add more formats in the future. If all you care about is JSON then it isn't a big deal either way.
Thanks for the help so far! I am now able to generate a JSON list of coordinates and parse it back with the following code, which does yet not handle white spaces when parsing.
I believe for handing whitespace I should use the following code as reference, is this correct? https://github.com/stephenberry/glaze/blob/4b043646397b55fe764b2d19bb5c72d6dd1a39d9/include/glaze/json/read.hpp#L1161-L1163
to and from JSON code working so far:
template<uint32_t Format, typename Point, int Dim>
struct write_coordinates {
template<auto Opts>
static void apply(Point &value, is_context auto &&ctx, auto &&b, auto &&ix) {
write_coordinates<Format, Point, Dim - 1>::template apply<Opts>(
value,
std::forward<decltype(ctx)>(ctx),
std::forward<decltype(b)>(b),
std::forward<decltype(ix)>(ix)
);
dump<','>(b, ix);
write<Format>::template op<Opts>(
boost::geometry::get<Dim>(value),
std::forward<decltype(ctx)>(ctx),
std::forward<decltype(b)>(b),
std::forward<decltype(ix)>(ix)
);
}
};
template<uint32_t Format, typename Point>
struct write_coordinates<Format, Point, 0> {
template<auto Opts>
static void apply(Point &value, is_context auto &&ctx, auto &&b, auto &&ix) {
write<Format>::template op<Opts>(
boost::geometry::get<0>(value),
std::forward<decltype(ctx)>(ctx),
std::forward<decltype(b)>(b),
std::forward<decltype(ix)>(ix)
);
}
};
template<uint32_t Format, typename Point>
struct write_coordinate_list {
template<auto Opts>
static void apply(Point &value, is_context auto &&ctx, auto &&b, auto &&ix) {
dump<'['>(b, ix);
write_coordinates<Format, Point,
boost::geometry::traits::dimension<Point>::value - 1>::template apply<Opts>(
value,
std::forward<decltype(ctx)>(ctx),
std::forward<decltype(b)>(b),
std::forward<decltype(ix)>(ix)
);
dump<']'>(b, ix);
}
};
template<typename Point> requires boost::geometry::util::is_point<Point>::value
struct to<JSON, Point> {
template<auto Opts>
static void op(Point &value, is_context auto &&ctx, auto &&b, auto &&ix) {
write_coordinate_list<JSON, Point>::template apply<Opts>(
value,
std::forward<decltype(ctx)>(ctx),
std::forward<decltype(b)>(b),
std::forward<decltype(ix)>(ix)
);
}
};
template<uint32_t Format, typename Point, int Dim>
struct read_coordinates {
template<auto Opts>
static void apply(Point &value, is_context auto&& ctx, auto&& it, auto&& end) {
read_coordinates<Format, Point, Dim - 1>::template apply<Opts>(
value,
std::forward<decltype(ctx)>(ctx),
std::forward<decltype(it)>(it),
std::forward<decltype(end)>(end)
);
match<','>(ctx, it);
typename boost::geometry::traits::coordinate_type<Point>::type dimension_value;
read<Format>::template op<Opts>(
dimension_value,
std::forward<decltype(ctx)>(ctx),
std::forward<decltype(it)>(it),
std::forward<decltype(end)>(end)
);
boost::geometry::set<Dim>(value, dimension_value);
}
};
template<uint32_t Format, typename Point>
struct read_coordinates<Format, Point, 0> {
template<auto Opts>
static void apply(Point &value, is_context auto&& ctx, auto&& it, auto&& end) {
typename boost::geometry::traits::coordinate_type<Point>::type dimension_value;
read<Format>::template op<Opts>(
dimension_value,
std::forward<decltype(ctx)>(ctx),
std::forward<decltype(it)>(it),
std::forward<decltype(end)>(end)
);
boost::geometry::set<0>(value, dimension_value);
}
};
template<uint32_t Format, typename Point>
struct read_coordinate_list {
template<auto Opts>
static void apply(Point &value, is_context auto &&ctx, auto &&it, auto &&end) {
match<'['>(ctx, it);
read_coordinates<Format, Point,
boost::geometry::traits::dimension<Point>::value - 1>::template apply<Opts>(
value,
std::forward<decltype(ctx)>(ctx),
std::forward<decltype(it)>(it),
std::forward<decltype(end)>(end)
);
match<']'>(ctx, it);
}
};
template<typename Point> requires boost::geometry::util::is_point<Point>::value
struct from<JSON, Point> {
template<auto Opts>
static void op(Point &value, is_context auto &&ctx, auto &&it, auto &&end) {
read_coordinate_list<JSON, Point>::template apply<Opts>(
value,
std::forward<decltype(ctx)>(ctx),
std::forward<decltype(it)>(it),
std::forward<decltype(end)>(end)
);
}
};
Is there any difference between:
write<JSON>::op<Opts>(
"coordinates",
std::forward<decltype(ctx)>(ctx),
std::forward<decltype(b)>(b),
std::forward<decltype(ix)>(ix)
);
and:
dump<'"'>(b, ix);
dump<"coordinates">(b, ix);
dump<'"'>(b, ix);
in regards to runtime costs? Asking as they seem to yield the same behaviour.
You can just use GLZ_SKIP_WS();. You shouldn't need the if constexpr (!has_ws_handled(Opts)) { , unless perhaps for the opening if you want avoid some potential double parsing, but this doesn't really matter.
You don't need to use std::forward with ctx, b, and ix, because these are almost always lvalue references, so it won't make a performance difference.
Thank you so much! Now the next challenge has been to write Linestrings to JSON.
The issue that I suspect that I'm facing is that, since a Linestring can be modeled as a std::vector of Points:
https://github.com/boostorg/geometry/blob/develop/include/boost/geometry/geometries/linestring.hpp#L54-L60
I think Glaze's std::vector specialization is being picked up before my specialization of struct to<JSON, Geometry> with requires boost::geometry::util::is_geometry<Geometry>::value.
The reason I suspect that is because, if I fully specialize to<JSON, Linestring> then it works.
Anyway to overcome this? 🤔 My current approach will be to start fiddling around with include order, but that would lead to a brittle implementation which is dependent on include order.
// iterate through the coordinates of a Linestring
template<uint32_t Format, typename Linestring> requires boost::geometry::util::is_linestring<Linestring>::value
struct write_coordinates<Format, Linestring> {
template<auto Opts>
static void apply(Linestring &value, is_context auto &&ctx, auto &&b, auto &&ix) {
dump<'['>(b, ix);
auto begin = boost::begin(value);
auto const end = boost::end(value);
bool first = true;
for (auto it = begin; it != end; ++it)
{
if (not first) {
dump<','>(b, ix);
}
write_coordinates<Format, typename std::remove_reference<decltype(*it)>::type>::template apply<Opts>(*it, ctx, b, ix);
first = false;
}
dump<']'>(b, ix);
}
};
// write coordinates of a Point
template<uint32_t Format, typename Point> requires boost::geometry::util::is_point<Point>::value
struct write_coordinates<Format, Point> {
template<auto Opts>
static void apply(Point &value, is_context auto &&ctx, auto &&b, auto &&ix) {
dump<'['>(b, ix);
write_coordinate<Format, Point,
boost::geometry::traits::dimension<Point>::value - 1>::template apply<Opts>(value, ctx, b, ix);
dump<']'>(b, ix);
}
};
// If full specialization is done, then it works, a JSON is generated.
// template<>
// struct to<JSON, Linestring> {
// template<auto Opts>
// static void op(Linestring &value, is_context auto &&ctx, auto &&b, auto &&ix) {
// dump<'{'>(b, ix);
// dump<R"("type")">(b, ix);
// dump<':'>(b, ix);
// write_geo_json_type<Linestring>::template apply<Opts>(ctx, b, ix);
// dump<','>(b, ix);
// dump<R"("coordinates")">(b, ix);
// dump<':'>(b, ix);
// write_coordinates<JSON, Linestring>::template apply<Opts>(value, ctx, b, ix);
// dump<'}'>(b, ix);
// }
// };
template<typename Geometry> requires boost::geometry::util::is_geometry<Geometry>::value // I suspect because a Linestring can be a std::vector of points, Glaze's `std::vector` handling is being picked up before this specialization.
struct to<JSON, Geometry> {
template<auto Opts>
static void op(Geometry &value, is_context auto &&ctx, auto &&b, auto &&ix) {
dump<'{'>(b, ix);
dump<R"("type")">(b, ix);
dump<':'>(b, ix);
write_geo_json_type<Geometry>::template apply<Opts>(ctx, b, ix);
dump<','>(b, ix);
dump<R"("coordinates")">(b, ix);
dump<':'>(b, ix);
write_coordinates<JSON, Geometry>::template apply<Opts>(value, ctx, b, ix);
dump<'}'>(b, ix);
}
};
Comping error:
In file included from <directory>/libraries/glaze_geom/test/point_traits.cpp:14:
In file included from <directory>/cmake-build-debugpolly/vcpkg_installed/arm64-osx/include/glaze/glaze_exceptions.hpp:12:
<directory>/cmake-build-debugpolly/vcpkg_installed/arm64-osx/include/glaze/exceptions/json_exceptions.hpp:140:21: error: no matching function for call to 'write_json'
140 | auto result = glz::write_json(std::forward<T>(value));
| ^~~~~~~~~~~~~~~
<directory>/libraries/glaze_geom/test/point_traits.cpp:266:22: note: in instantiation of function template specialization 'glz::ex::write_json<boost::geometry::model::linestring<boost::geometry::model::point<double, 3, boost::geometry::cs::cartesian>> &>' requested here
266 | r = glz::ex::write_json(bg_linestring);
| ^
<directory>/cmake-build-debugpolly/vcpkg_installed/arm64-osx/include/glaze/json/write.hpp:1627:56: note: candidate template ignored: constraints not satisfied [with T = boost::geometry::model::linestring<boost::geometry::model::point<double, 3, boost::geometry::cs::cartesian>> &]
1627 | [[nodiscard]] glz::expected<std::string, error_ctx> write_json(T&& value) noexcept
| ^
<directory>/cmake-build-debugpolly/vcpkg_installed/arm64-osx/include/glaze/json/write.hpp:1626:14: note: because 'boost::geometry::model::linestring<boost::geometry::model::point<double, 3, boost::geometry::cs::cartesian>> &' does not satisfy 'write_json_supported'
1626 | template <write_json_supported T>
| ^
<directory>/cmake-build-debugpolly/vcpkg_installed/arm64-osx/include/glaze/core/opts.hpp:347:46: note: because 'detail::to<JSON, std::remove_cvref_t<T>>{}' would be invalid: ambiguous partial specializations of 'to<10, boost::geometry::model::linestring<boost::geometry::model::point<double, 3, boost::geometry::cs::cartesian>>>'
347 | concept write_json_supported = requires { detail::to<JSON, std::remove_cvref_t<T>>{}; };
| ^
<directory>/cmake-build-debugpolly/vcpkg_installed/arm64-osx/include/glaze/json/write.hpp:1615:28: note: candidate function template not viable: requires 2 arguments, but 1 was provided
1615 | [[nodiscard]] error_ctx write_json(T&& value, Buffer&& buffer) noexcept
| ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~
<directory>/cmake-build-debugpolly/vcpkg_installed/arm64-osx/include/glaze/json/write.hpp:1621:51: note: candidate function template not viable: requires 2 arguments, but 1 was provided
1621 | [[nodiscard]] glz::expected<size_t, error_ctx> write_json(T&& value, Buffer&& buffer) noexcept
| ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~
<directory>/cmake-build-debugpolly/vcpkg_installed/arm64-osx/include/glaze/json/write.hpp:1633:28: note: candidate function template not viable: requires 2 arguments, but 1 was provided
1633 | [[nodiscard]] error_ctx write_json(T&& value, Buffer&& buffer) noexcept
| ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~
<directory>/cmake-build-debugpolly/vcpkg_installed/arm64-osx/include/glaze/json/write.hpp:1639:51: note: candidate function template not viable: requires 2 arguments, but 1 was provided
1639 | [[nodiscard]] glz::expected<size_t, error_ctx> write_json(T&& value, Buffer&& buffer) noexcept
| ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.
I setup a compiler explorer with the code here: link removed.
I'm trying to understand why don't I need a template specialization for the Boost Geometry Point model, but I need one for the Linestring which is modelled as a std::vector<Point>?
This is, why if you comment lines 342 to 348 from the compiler explorer link above there is a compilation error?
Why don't I need a similar specialization for Point?
Code from lines 342 to 348 is:
template<>
struct glz::detail::to<glz::JSON, ModelLinestring> {
template<auto Opts>
static void op(const ModelLinestring &value, is_context auto &&ctx, auto &&b, auto &&ix) {
write_geometry<JSON, ModelLinestring>::template apply<Opts>(value, ctx, b, ix);
}
};
Thanks for any insight you can provide.
When you comment out those lines this error is helpful: note: because 'detail::to<JSON, std::remove_cvref_t<T>>{}' would be invalid: ambiguous partial specializations of 'to<10, boost::geometry::model::linestring<boost::geometry::model::point<double, 3, boost::geometry::cs::cartesian>>>'
I don't have much time to dig into this, but this code must be a more specialized version of glz::detail::to. If this doesn't exist it means that two different specializations would support this type.
This is probably one of them. But, I can't actually tell where the other option is, which causes the ambiguity.
template<typename Geometry> requires boost::geometry::util::is_geometry<Geometry>::value
struct to<JSON, Geometry> {
template<auto Opts>
static void op(const Geometry &value, is_context auto &&ctx, auto &&b, auto &&ix) {
write_geometry<JSON, Geometry>::template apply<Opts>(value, ctx, b, ix);
}
};
Awesome, thank you!
Any insight was very helpful. Sorry to bug with C++ related questions, will close this issue, thank you.
It becomes ambiguous because it clashes with Glaze's handling of std::vector<T>, explicitly removing the Linestring from the to<JSON, Geometry> with requires (boost::geometry::util::is_geometry<Geometry>::value and not boost::geometry::util::is_linestring<Geometry>::value):
template<typename Geometry> requires (boost::geometry::util::is_geometry<Geometry>::value and not boost::geometry::util::is_linestring<Geometry>::value)
struct to<JSON, Geometry> {
template<auto Opts>
static void op(const Geometry &value, is_context auto &&ctx, auto &&b, auto &&ix) {
write_geometry<JSON, Geometry>::template apply<Opts>(value, ctx, b, ix);
}
};
And removing the specialization for Linestring, it results in a list of Point types, handled by glaze's mechanism to handle std::vector<T>:
[{"type":"Point","coordinates":[2.4,5.6,5.9]},{"type":"Point","coordinates":[2.4,5.6,5.9]}]
This was very interesting, I'm glad you were able to figure it out and get it working! Nice job!
Now looking for a mechanism of how can glaze's handling of std::vector<T> can be used as a fallback in case no other specialization is provided, rather than creating an ambiguity.
Yay, this was covered in the docs! 🥳
https://github.com/stephenberry/glaze/blob/7d942ae07caf37792d1ec62df29f51c8b71e7bfc/docs/custom-serialization.md?plain=1#L104-L118
So fixed with:
template <typename Geometry>
requires boost::geometry::util::is_geometry<Geometry>::value
struct glz::meta<Geometry> {
static constexpr auto custom_read = true;
static constexpr auto custom_write = true;
};
Which makes the custom serializer take precedence.
Damn, this library is awesome!
Fantastic!