Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the cellToBoundary command for the h3 binary #818

Merged
merged 8 commits into from Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions CMakeLists.txt
Expand Up @@ -180,8 +180,9 @@ set(EXAMPLE_SOURCE_FILES
examples/neighbors.c
examples/compactCells.c
examples/edge.c)
set(H3_BIN_SOURCE_FILES
src/apps/filters/h3.c)
set(OTHER_SOURCE_FILES
src/apps/filters/h3.c
src/apps/filters/cellToLatLng.c
src/apps/filters/cellToLocalIj.c
src/apps/filters/localIjToCell.c
Expand Down Expand Up @@ -289,7 +290,7 @@ set(OTHER_SOURCE_FILES
src/apps/benchmarks/benchmarkH3Api.c)

set(ALL_SOURCE_FILES
${LIB_SOURCE_FILES} ${APP_SOURCE_FILES} ${TEST_APP_SOURCE_FILES} ${OTHER_SOURCE_FILES})
${LIB_SOURCE_FILES} ${APP_SOURCE_FILES} ${TEST_APP_SOURCE_FILES} ${H3_BIN_SOURCE_FILES} ${OTHER_SOURCE_FILES})

# This is done for quality control purposes (to detect if any source files are missing from
# our list), but is not done as the authoritative list as per CMake developer recommendations.
Expand Down
5 changes: 3 additions & 2 deletions CMakeTests.cmake
@@ -1,4 +1,4 @@
# Copyright 2017-2022 Uber Technologies, Inc.
# Copyright 2017-2022, 2024 Uber Technologies, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -228,8 +228,9 @@ add_h3_test(testGridDistanceExhaustive src/apps/testapps/testGridDistanceExhaust
add_h3_test(testH3CellAreaExhaustive src/apps/testapps/testH3CellAreaExhaustive.c)
add_h3_test(testCellToBBoxExhaustive src/apps/testapps/testCellToBBoxExhaustive.c)

add_h3_cli_test(testCliCellToLatLng "cellToLatLng -c 8928342e20fffff" "37.5012466151, -122.5003039349")
add_h3_cli_test(testCliCellToLatLng "cellToLatLng -c 8928342e20fffff" "POINT(-122.5003039349 37.5012466151)")
add_h3_cli_test(testCliLatLngToCell "latLngToCell --lat 20 --lng 123 -r 2" "824b9ffffffffff")
add_h3_cli_test(testCliCellToBoundary "cellToBoundary -c 8928342e20fffff" "POLYGON((-122.4990471431 37.4997389893, -122.4979805011 37.5014245698, -122.4992373065 37.5029321860, -122.5015607527 37.5027541980, -122.5026273256 37.5010686174, -122.5013705214 37.4995610248, -122.4990471431 37.4997389893))")

if(BUILD_ALLOC_TESTS)
add_h3_library(h3WithTestAllocators test_prefix_)
Expand Down
71 changes: 60 additions & 11 deletions src/apps/filters/h3.c
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Uber Technologies, Inc.
* Copyright 2021, 2024 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -43,19 +43,24 @@ bool cellToLatLngCmd(int argc, char *argv[]) {
Arg cellToLatLngArg = {
.names = {"cellToLatLng"},
.required = true,
.helpText = "Convert an H3 cell to a latitude/longitude coordinate",
.helpText = "Convert an H3 cell to a WKT POINT coordinate",
};
Arg helpArg = ARG_HELP;
DEFINE_CELL_ARG(cell, cellArg);
Arg *args[] = {&cellToLatLngArg, &helpArg, &cellArg};
if (parseArgs(argc, argv, 3, args, &helpArg,
"Convert an H3 cell to a latitude/longitude coordinate")) {
if (parseArgs(argc, argv, sizeof(args) / sizeof(Arg *), args, &helpArg,
"Convert an H3 cell to a WKT POINT coordinate")) {
return helpArg.found;
}
LatLng ll;
H3_EXPORT(cellToLatLng)(cell, &ll);
printf("%.10lf, %.10lf\n", H3_EXPORT(radsToDegs)(ll.lat),
H3_EXPORT(radsToDegs)(ll.lng));
H3Error err = H3_EXPORT(cellToLatLng)(cell, &ll);
if (err) {
return false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my info, why return true/false from these functions? Wouldn't we just want to return the error code, so callers would get a 0 exit code on success and a meaningful non-zero code on failure?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I think we started work on this binary before H3Error was a thing and it was doing h3IsValid checks on the inputs/outputs to determine if it should exit with an error or not.

Then the conversion of that was done this way because it was the most straightforward. Bubbling up the H3Error does make sense to me. If you don't mind, I'll keep that refactor for when I add in the next sub-command.

}
// Using WKT formatting for the output. TODO: Add support for JSON
// formatting
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about plans for output and input formats here. I assume for output we'd want a -f or --format param similar to ogr2ogr. It would be easy to go overboard here, but I think at a minimum:

  • csv: 37.5012466151,-122.5003039349
  • wkt: POINT(-122.5003039349 37.5012466151)
  • geojson: {"type": "Point", "coordinates": [-122.5003039349 37.5012466151]}

We might also support json-array ([-122.5003039349 37.5012466151]) or json-object ({"lat":-122.5003039349, "lng": 37.5012466151}) and maybe an --ordering param specifying latlng or lnglat?

The parallel question is what inputs we support - accepting these formats as inputs is nicely symmetrical, but more painful than --lat=x --lng=y or bare arguments.

Not saying we should do any of this, but it might be nice to know where we plan to draw the line. I think a good yardstick should be how easy it is to pipe data from a tool like ogr2ogr to and from these CLI commands.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this can definitely become a rabbit hole, here. There are other things to consider, for instance: should the csv output type include a headers row for the first one with lat,lng?

But also, how much of this can we generalize across these functions, and how much has to be hand-written per sub-command? The more of the latter the less I want to support everything under the sun and just suggest users to do things like h3 cellToLatLng -c 8928342e20fffff --geojson | jq .coordinates if they want other formats.

That's part of why I was thinking of going with a JSON-based output by default, because of the possibility of mangling the format more easily than any other, but went with WKT since that's what @isaacbrodsky suggested as an expected output.

I'm not personally familiar with ogr2ogr, though, so that's part of my bias towards a JSON-based format.

printf("POINT(%.10lf %.10lf)\n", H3_EXPORT(radsToDegs)(ll.lng),
H3_EXPORT(radsToDegs)(ll.lat));
return true;
}

Expand Down Expand Up @@ -92,7 +97,7 @@ bool latLngToCellCmd(int argc, char *argv[]) {

Arg *args[] = {&latLngToCellArg, &helpArg, &resArg, &latArg, &lngArg};
if (parseArgs(
argc, argv, 5, args, &helpArg,
argc, argv, sizeof(args) / sizeof(Arg *), args, &helpArg,
"Convert degrees latitude/longitude coordinate to an H3 cell.")) {
return helpArg.found;
}
Expand All @@ -102,6 +107,7 @@ bool latLngToCellCmd(int argc, char *argv[]) {
H3Index c;
H3Error e = H3_EXPORT(latLngToCell)(&ll, res, &c);

// TODO: Add support for JSON formatting
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does JSON formatting mean "8928342e20fffff" in this case? Also, does h3Println make piping harder than h3Print?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That or {"cell": "8928342e20fffff"}. I'm not sure which would be a better idea.

if (e == E_SUCCESS) {
h3Println(c);
} else {
Expand All @@ -110,23 +116,63 @@ bool latLngToCellCmd(int argc, char *argv[]) {
return true;
}

bool cellToBoundaryCmd(int argc, char *argv[]) {
Arg cellToBoundaryArg = {
.names = {"cellToBoundary"},
.required = true,
.helpText = "Convert an H3 cell to a WKT POLYGON defining its boundary",
};
Arg helpArg = ARG_HELP;
DEFINE_CELL_ARG(cell, cellArg);
Arg *args[] = {&cellToBoundaryArg, &helpArg, &cellArg};
if (parseArgs(
argc, argv, sizeof(args) / sizeof(Arg *), args, &helpArg,
"Convert an H3 cell to a WKT POLYGON defining its boundary")) {
return helpArg.found;
}
CellBoundary cb;
H3Error err = H3_EXPORT(cellToBoundary)(cell, &cb);
if (err) {
return false;
}
// Using WKT formatting for the output. TODO: Add support for JSON
// formatting
printf("POLYGON((");
for (int i = 0; i < cb.numVerts; i++) {
LatLng *ll = &cb.verts[i];
printf("%.10lf %.10lf, ", H3_EXPORT(radsToDegs)(ll->lng),
H3_EXPORT(radsToDegs)(ll->lat));
}
// WKT has the first and last points match, so re-print the first one
printf("%.10lf %.10lf))\n", H3_EXPORT(radsToDegs)(cb.verts[0].lng),
H3_EXPORT(radsToDegs)(cb.verts[0].lat));
Comment on lines +147 to +148
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit/note if this function gets reused, that it depends on numVerts > 0.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for it to be zero and not produce an H3Error value that short-circuits it, though?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know it should always have vertices if the return code is success

return true;
}

bool generalHelp(int argc, char *argv[]) {
Arg helpArg = ARG_HELP;
Arg cellToLatLngArg = {
.names = {"cellToLatLng"},
.helpText = "Convert an H3 cell to a latitude/longitude coordinate",
.helpText = "Convert an H3 cell to a WKT POINT coordinate",
};
Arg latLngToCellArg = {
.names = {"latLngToCell"},
.helpText =
"Convert degrees latitude/longitude coordinate to an H3 cell.",
};
Arg *args[] = {&helpArg, &cellToLatLngArg, &latLngToCellArg};
Arg cellToBoundaryArg = {
.names = {"cellToBoundary"},
.helpText = "Convert an H3 cell to a WKT POLYGON defining its boundary",
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Is it possible to define this a top-level static const so that we can reuse it instead of repeating it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my prior comment. Leaving DRY-ing it to another PR focused solely on that so we can better debate the merits/long-term maintenance.

Arg *args[] = {&helpArg, &cellToLatLngArg, &latLngToCellArg,
&cellToBoundaryArg};

const char *helpText =
"Please use one of the subcommands listed to perform an H3 "
"calculation. Use h3 <SUBCOMMAND> --help for details on the usage of "
"any subcommand.";
return parseArgs(argc, argv, 3, args, &helpArg, helpText);
return parseArgs(argc, argv, sizeof(args) / sizeof(Arg *), args, &helpArg,
helpText);
}

int main(int argc, char *argv[]) {
Expand All @@ -140,6 +186,9 @@ int main(int argc, char *argv[]) {
if (has("latLngToCell", 1, argv) && latLngToCellCmd(argc, argv)) {
return 0;
}
if (has("cellToBoundary", 1, argv) && cellToBoundaryCmd(argc, argv)) {
return 0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, I would have gone with return cellToBoundaryCmd(argc, argv) and have each command return H3_ERROR

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you, but would like that to also be a separate PR.

}
if (generalHelp(argc, argv)) {
return 0;
}
Expand Down