Files
tippecanoe/evaluator.cpp
T
Claude 85547150c2 Throw instead of exiting from the tile clipping and overzooming code
The code reachable from overzooming a tile no longer calls exit(). It
reports errors by throwing tippecanoe_error, and owns its FILE * and
json_pull * handles through RAII wrappers so that nothing leaks when an
error unwinds the stack.

New files:

* errors.hpp/errors.cpp: the tippecanoe_error exception class, plus
  throw_tippecanoe_error() and throw_perror() helpers.
* raii.hpp: unique_file and unique_json_pull, both std::unique_ptr
  aliases, for FILE * and json_pull *.

exit() becomes throw in clip.cpp, mvt.cpp, mvt.hpp, read_json.cpp,
evaluator.cpp, text.cpp, geometry.cpp, attribute.cpp, and
write_json.hpp. Each tool's main() is now a try/catch around an
inner_main(), so the command-line tools exit with the status they
always did.

Along the way this fixes a z_stream leaked when compress() failed, an
exit() that could run from json_writer's destructor during unwinding,
and an exit() in mvt_value's hash function.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuCSVGssWNUCpVEE8Yp5HT
2026-08-07 18:46:09 +00:00

442 lines
12 KiB
C++

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unordered_map>
#include <functional>
#include "mvt.hpp"
#include "evaluator.hpp"
#include "errors.hpp"
#include "raii.hpp"
#include "milo/dtoa_milo.h"
#include "text.hpp"
int compare(mvt_value const &one, json_object *two, bool &fail) {
switch (one.type) {
case mvt_string:
if (two->type != JSON_STRING) {
fail = true;
return false; // string vs non-string
}
return strcmp(one.c_str(), two->value.string.string);
case mvt_double:
case mvt_float:
case mvt_int:
case mvt_uint:
case mvt_sint:
if (two->type != JSON_NUMBER) {
fail = true;
return false; // number vs non-number
}
double v;
switch (one.type) {
case mvt_double:
v = one.numeric_value.double_value;
break;
case mvt_float:
v = one.numeric_value.float_value;
break;
case mvt_int:
v = one.numeric_value.int_value;
break;
case mvt_uint:
v = one.numeric_value.uint_value;
break;
case mvt_sint:
v = one.numeric_value.sint_value;
break;
case mvt_no_such_key:
default:
throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Internal error: bad mvt type %d", one.type);
}
if (v < two->value.number.number) {
return -1;
} else if (v > two->value.number.number) {
return 1;
} else {
return 0;
}
case mvt_bool:
if (two->type != JSON_TRUE && two->type != JSON_FALSE) {
fail = true;
return false; // bool vs non-bool
}
{
bool b = two->type != JSON_FALSE;
return one.numeric_value.bool_value > b;
}
case mvt_null:
if (two->type != JSON_NULL) {
fail = true;
return false; // null vs non-null
}
return 0; // null equals null
case mvt_no_such_key:
default:
break;
}
throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Internal error: bad mvt type %d", one.type);
}
// 0: false
// 1: true
// -1: incomparable (sql null), treated as false in final output
static int eval(std::function<mvt_value(std::string const &)> feature, json_object *f, std::set<std::string> &exclude_attributes, std::vector<std::string> const &unidecode_data) {
if (f != NULL) {
if (f->type == JSON_TRUE) {
return 1;
} else if (f->type == JSON_FALSE) {
return 0;
} else if (f->type == JSON_NULL) {
return 1;
}
if (f->type == JSON_NUMBER) {
if (f->value.number.number == 0) {
return 0;
} else {
return 1;
}
}
if (f->type == JSON_STRING) {
if (f->value.string.string[0] == '\0') {
return 0;
} else {
return 1;
}
}
}
if (f == NULL || f->type != JSON_ARRAY) {
throw_tippecanoe_error(EXIT_FILTER, "Filter is not an array: %s", json_stringify(f));
}
if (f->value.array.length < 1) {
throw_tippecanoe_error(EXIT_FILTER, "Array too small in filter: %s", json_stringify(f));
}
if (f->value.array.array[0]->type != JSON_STRING) {
throw_tippecanoe_error(EXIT_FILTER, "Filter operation is not a string: %s", json_stringify(f));
}
if (strcmp(f->value.array.array[0]->value.string.string, "has") == 0 ||
strcmp(f->value.array.array[0]->value.string.string, "!has") == 0) {
if (f->value.array.length != 2) {
throw_tippecanoe_error(EXIT_FILTER, "Wrong number of array elements in filter: %s", json_stringify(f));
}
if (strcmp(f->value.array.array[0]->value.string.string, "has") == 0) {
if (f->value.array.array[1]->type != JSON_STRING) {
throw_tippecanoe_error(EXIT_FILTER, "\"has\" key is not a string: %s", json_stringify(f));
}
return feature(std::string(f->value.array.array[1]->value.string.string)).type != mvt_no_such_key;
}
if (strcmp(f->value.array.array[0]->value.string.string, "!has") == 0) {
if (f->value.array.array[1]->type != JSON_STRING) {
throw_tippecanoe_error(EXIT_FILTER, "\"!has\" key is not a string: %s", json_stringify(f));
}
return feature(std::string(f->value.array.array[1]->value.string.string)).type == mvt_no_such_key;
}
}
if (strcmp(f->value.array.array[0]->value.string.string, "==") == 0 ||
strcmp(f->value.array.array[0]->value.string.string, "!=") == 0 ||
strcmp(f->value.array.array[0]->value.string.string, ">") == 0 ||
strcmp(f->value.array.array[0]->value.string.string, ">=") == 0 ||
strcmp(f->value.array.array[0]->value.string.string, "<") == 0 ||
strcmp(f->value.array.array[0]->value.string.string, "<=") == 0) {
if (f->value.array.length != 3) {
throw_tippecanoe_error(EXIT_FILTER, "Wrong number of array elements in filter: %s", json_stringify(f));
}
if (f->value.array.array[1]->type != JSON_STRING) {
throw_tippecanoe_error(EXIT_FILTER, "comparison key is not a string: %s", json_stringify(f));
}
mvt_value ff = feature(std::string(f->value.array.array[1]->value.string.string));
if (ff.type == mvt_no_such_key) {
static bool warned = false;
if (!warned) {
const char *s = json_stringify(f);
fprintf(stderr, "Warning: attribute not found for comparison: %s\n", s);
free((void *) s);
warned = true;
}
if (strcmp(f->value.array.array[0]->value.string.string, "!=") == 0) {
return true; // attributes that aren't found are not equal
}
return false; // not found: comparison is false
}
bool fail = false;
int cmp = compare(ff, f->value.array.array[2], fail);
if (fail) {
static bool warned = false;
if (!warned) {
const char *s = json_stringify(f);
fprintf(stderr, "Warning: mismatched type in comparison: %s\n", s);
free((void *) s);
warned = true;
}
if (strcmp(f->value.array.array[0]->value.string.string, "!=") == 0) {
return true; // mismatched types are not equal
}
return false;
}
if (strcmp(f->value.array.array[0]->value.string.string, "==") == 0) {
return cmp == 0;
}
if (strcmp(f->value.array.array[0]->value.string.string, "!=") == 0) {
return cmp != 0;
}
if (strcmp(f->value.array.array[0]->value.string.string, ">") == 0) {
return cmp > 0;
}
if (strcmp(f->value.array.array[0]->value.string.string, ">=") == 0) {
return cmp >= 0;
}
if (strcmp(f->value.array.array[0]->value.string.string, "<") == 0) {
return cmp < 0;
}
if (strcmp(f->value.array.array[0]->value.string.string, "<=") == 0) {
return cmp <= 0;
}
throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Internal error: can't happen: %s", json_stringify(f));
}
if (strcmp(f->value.array.array[0]->value.string.string, "all") == 0 ||
strcmp(f->value.array.array[0]->value.string.string, "any") == 0 ||
strcmp(f->value.array.array[0]->value.string.string, "none") == 0) {
bool v;
if (strcmp(f->value.array.array[0]->value.string.string, "all") == 0) {
v = true;
} else {
v = false;
}
for (size_t i = 1; i < f->value.array.length; i++) {
int out = eval(feature, f->value.array.array[i], exclude_attributes, unidecode_data);
if (out >= 0) { // nulls are ignored in boolean and/or expressions
if (strcmp(f->value.array.array[0]->value.string.string, "all") == 0) {
v = v && out;
if (!v) {
break;
}
} else {
v = v || out;
if (v) {
break;
}
}
}
}
if (strcmp(f->value.array.array[0]->value.string.string, "none") == 0) {
return !v;
} else {
return v;
}
}
if (strcmp(f->value.array.array[0]->value.string.string, "in") == 0 ||
strcmp(f->value.array.array[0]->value.string.string, "!in") == 0) {
if (f->value.array.length < 2) {
throw_tippecanoe_error(EXIT_FILTER, "Array too small in filter: %s", json_stringify(f));
}
if (f->value.array.array[1]->type != JSON_STRING) {
throw_tippecanoe_error(EXIT_FILTER, "\"!in\" key is not a string: %s", json_stringify(f));
}
mvt_value ff = feature(std::string(f->value.array.array[1]->value.string.string));
if (ff.type == mvt_no_such_key) {
static bool warned = false;
if (!warned) {
const char *s = json_stringify(f);
fprintf(stderr, "Warning: attribute not found for comparison: %s\n", s);
free((void *) s);
warned = true;
}
if (strcmp(f->value.array.array[0]->value.string.string, "!in") == 0) {
return true; // attributes that aren't found are not in
}
return false; // not found: comparison is false
}
bool found = false;
for (size_t i = 2; i < f->value.array.length; i++) {
bool fail = false;
int cmp = compare(ff, f->value.array.array[i], fail);
if (fail) {
static bool warned = false;
if (!warned) {
const char *s = json_stringify(f);
fprintf(stderr, "Warning: mismatched type in comparison: %s\n", s);
free((void *) s);
warned = true;
}
cmp = 1;
}
if (cmp == 0) {
found = true;
break;
}
}
if (strcmp(f->value.array.array[0]->value.string.string, "in") == 0) {
return found;
} else {
return !found;
}
}
if (strcmp(f->value.array.array[0]->value.string.string, "attribute-filter") == 0) {
if (f->value.array.length != 3) {
throw_tippecanoe_error(EXIT_FILTER, "Wrong number of array elements in filter: %s", json_stringify(f));
}
if (f->value.array.array[1]->type != JSON_STRING) {
throw_tippecanoe_error(EXIT_FILTER, "\"attribute-filter\" key is not a string: %s", json_stringify(f));
}
bool ok = eval(feature, f->value.array.array[2], exclude_attributes, unidecode_data) > 0;
if (!ok) {
exclude_attributes.insert(f->value.array.array[1]->value.string.string);
}
return true;
}
throw_tippecanoe_error(EXIT_FILTER, "Unknown filter %s", json_stringify(f));
}
bool evaluate(std::function<mvt_value(std::string const &)> feature, std::string const &layer, json_object *filter, std::set<std::string> &exclude_attributes, std::vector<std::string> const &unidecode_data) {
if (filter == NULL || filter->type != JSON_HASH) {
throw_tippecanoe_error(EXIT_JSON, "Error: filter is not a hash: %s", json_stringify(filter));
}
bool ok = true;
json_object *f;
f = json_hash_get(filter, layer.c_str());
if (ok && f != NULL) {
ok = eval(feature, f, exclude_attributes, unidecode_data) > 0;
}
f = json_hash_get(filter, "*");
if (ok && f != NULL) {
ok = eval(feature, f, exclude_attributes, unidecode_data) > 0;
}
return ok;
}
json_object *read_filter(const char *fname) {
unique_file fp(fopen(fname, "r"));
if (fp.get() == NULL) {
throw_perror(EXIT_OPEN, fname);
}
unique_json_pull jp(json_begin_file(fp.get()));
json_object *filter = json_read_tree(jp.get());
if (filter == NULL) {
throw_tippecanoe_error(EXIT_JSON, "%s: %s", fname, jp.get()->error);
}
json_disconnect(filter);
return filter;
}
json_object *parse_filter(const char *s) {
unique_json_pull jp(json_begin_string(s));
json_object *filter = json_read_tree(jp.get());
if (filter == NULL) {
throw_tippecanoe_error(EXIT_JSON, "Could not parse filter %s\n%s", s, jp.get()->error);
}
json_disconnect(filter);
return filter;
}
bool evaluate(std::unordered_map<std::string, mvt_value> const &feature, std::string const &layer, json_object *filter, std::set<std::string> &exclude_attributes, std::vector<std::string> const &unidecode_data) {
std::function<mvt_value(std::string const &)> getter = [&](std::string const &key) {
auto f = feature.find(key);
if (f != feature.end()) {
return f->second;
} else {
mvt_value v;
v.type = mvt_no_such_key;
v.numeric_value.null_value = 0;
return v;
}
};
return evaluate(getter, layer, filter, exclude_attributes, unidecode_data);
}
bool evaluate(mvt_feature const &feat, mvt_layer const &layer, json_object *filter, std::set<std::string> &exclude_attributes, int z, std::vector<std::string> const &unidecode_data) {
std::function<mvt_value(std::string const &)> getter = [&](std::string const &key) {
const static std::string dollar_id = "$id";
if (key == dollar_id && feat.has_id) {
mvt_value v;
v.type = mvt_uint;
v.numeric_value.uint_value = feat.id;
return v;
}
const static std::string dollar_type = "$type";
if (key == dollar_type) {
mvt_value v;
v.type = mvt_string;
if (feat.type == mvt_point) {
const static std::string point = "Point";
v.set_string_value(point);
} else if (feat.type == mvt_linestring) {
const static std::string linestring = "LineString";
v.set_string_value(linestring);
} else if (feat.type == mvt_polygon) {
const static std::string polygon = "Polygon";
v.set_string_value(polygon);
}
return v;
}
const static std::string dollar_zoom = "$zoom";
if (key == dollar_zoom) {
mvt_value v2;
v2.type = mvt_uint;
v2.numeric_value.uint_value = z;
return v2;
}
for (size_t i = 0; i + 1 < feat.tags.size(); i += 2) {
if (layer.keys[feat.tags[i]] == key) {
return layer.values[feat.tags[i + 1]];
}
}
mvt_value v;
v.type = mvt_no_such_key;
v.numeric_value.null_value = 0;
return v;
};
return evaluate(getter, layer.name, filter, exclude_attributes, unidecode_data);
}