diff --git a/include/mbgl/style/conversion.hpp b/include/mbgl/style/conversion.hpp new file mode 100644 index 000000000..0507d452f --- /dev/null +++ b/include/mbgl/style/conversion.hpp @@ -0,0 +1,263 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace mbgl { +namespace style { +namespace conversion { + +/* + This namespace defines a generic conversion from a templated type `V` representing a JSON object conforming + to a style property from the Mapbox Style Specification, to `PropertyValue`: + + template + Result> convertPropertyValue(const V& value); + + This is used concretely for conversions from RapidJSON types in mbgl core, and from v8 types in + the node bindings. + + The requirements are that the following are legal expressions for a value `v` of type `const V&`: + + * `isArray(v)` -- returns a boolean indicating whether `v` represents a JSON array + * `arrayLength(v)` -- called only if `isArray(v)`; returns a size_t length + * `arrayMember(v)` -- called only if `isArray(v)`; returns `V` or `V&` + + * `isObject(v)` -- returns a boolean indicating whether `v` represents a JSON object + * `objectMember(v, name)` -- called only if `isObject(v)`; `name` is `const char *`; return value: + * is true when evaluated in a boolean context iff the named member exists + * is convertable to a `V` or `V&` when dereferenced + + * `toBool(v)` -- returns `optional`, absence indicating `v` is not a JSON boolean + * `toNumber(v)` -- returns `optional`, absence indicating `v` is not a JSON number + * `toString(v)` -- returns `optional`, absence indicating `v` is not a JSON string + + If for any reason the conversion fails, the result of `convertPropertyValue` will be the `Error` variant, + which includes explanatory text. +*/ + +struct Error { const char * message; }; + +template +using Result = variant; + +template +struct ConstantConverter {}; + +template +struct ConstantConverter { + Result operator()(const V& value) const { + optional converted = toBool(value); + if (!converted) { + return Error { "value must be a boolean" }; + } + return *converted; + } +}; + +template +struct ConstantConverter { + Result operator()(const V& value) const { + optional converted = toNumber(value); + if (!converted) { + return Error { "value must be a number" }; + } + return *converted; + } +}; + +template +struct ConstantConverter { + Result operator()(const V& value) const { + optional converted = toString(value); + if (!converted) { + return Error { "value must be a string" }; + } + return *converted; + } +}; + +template +struct ConstantConverter::value>> { + Result operator()(const V& value) const { + optional string = toString(value); + if (!string) { + return Error { "value must be a string" }; + } + + const auto result = Enum::toEnum(*string); + if (!result) { + return Error { "value must be a valid enumeration value" }; + } + + return *result; + } +}; + +template +struct ConstantConverter { + Result operator()(const V& value) const { + optional string = toString(value); + if (!string) { + return Error { "value must be a string" }; + } + + optional color = Color::parse(*string); + if (!color) { + return Error { "value must be a valid color" }; + } + + return *color; + } +}; + +template +struct ConstantConverter> { + Result> operator()(const V& value) const { + if (!isArray(value) || arrayLength(value) != 2) { + return Error { "value must be an array of two numbers" }; + } + + optional first = toNumber(arrayMember(value, 0)); + optional second = toNumber(arrayMember(value, 1)); + if (!first || !second) { + return Error { "value must be an array of two numbers" }; + } + + return std::array {{ *first, *second }}; + } +}; + +template +struct ConstantConverter> { + Result> operator()(const V& value) const { + if (!isArray(value) || arrayLength(value) != 4) { + return Error { "value must be an array of four numbers" }; + } + + optional first = toNumber(arrayMember(value, 0)); + optional second = toNumber(arrayMember(value, 1)); + optional third = toNumber(arrayMember(value, 2)); + optional fourth = toNumber(arrayMember(value, 3)); + if (!first || !second) { + return Error { "value must be an array of four numbers" }; + } + + return std::array {{ *first, *second, *third, *fourth }}; + } +}; + +template +struct ConstantConverter> { + Result> operator()(const V& value) const { + if (!isArray(value)) { + return Error { "value must be an array" }; + } + + std::vector result; + result.reserve(arrayLength(value)); + + for (std::size_t i = 0; i < arrayLength(value); ++i) { + optional number = toNumber(arrayMember(value, i)); + if (!number) { + return Error { "value must be an array of numbers" }; + } + result.push_back(*number); + } + + return result; + } +}; + +template +struct ConstantConverter> { + Result> operator()(const V& value) const { + if (!isArray(value)) { + return Error { "value must be an array" }; + } + + std::vector result; + result.reserve(arrayLength(value)); + + for (std::size_t i = 0; i < arrayLength(value); ++i) { + optional string = toString(arrayMember(value, i)); + if (!string) { + return Error { "value must be an array of strings" }; + } + result.push_back(*string); + } + + return result; + } +}; + +template +Result> convertPropertyValue(const V& value) { + if (!isObject(value)) { + Result constant = ConstantConverter()(value); + if (constant.template is()) { + return constant.template get(); + } + return constant.template get(); + } + + auto stopsValue = objectMember(value, "stops"); + if (!stopsValue) { + return Error { "function value must specify stops" }; + } + + if (!isArray(*stopsValue)) { + return Error { "function stops must be an array" }; + } + + std::vector> stops; + for (std::size_t i = 0; i < arrayLength(*stopsValue); ++i) { + const auto& stopValue = arrayMember(*stopsValue, i); + + if (!isArray(stopValue)) { + return Error { "function stop must be an array" }; + } + + if (arrayLength(stopValue) != 2) { + return Error { "function stop must have two elements" }; + } + + optional z = toNumber(arrayMember(stopValue, 0)); + if (!z) { + return Error { "function stop zoom level must be a number" }; + } + + Result v = ConstantConverter()(arrayMember(stopValue, 1)); + if (v.template is()) { + return v.template get(); + } + + stops.emplace_back(*z, v.template get()); + } + + auto baseValue = objectMember(value, "base"); + if (!baseValue) { + return Function(stops, 1.0f); + } + + optional base = toNumber(*baseValue); + if (!base) { + return Error { "function base must be a number"}; + } + + return Function(stops, *base); +} + +} // namespace conversion +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/property_parsing.cpp b/src/mbgl/style/property_parsing.cpp index 263516f20..16ce0f4ad 100644 --- a/src/mbgl/style/property_parsing.cpp +++ b/src/mbgl/style/property_parsing.cpp @@ -1,136 +1,8 @@ #include -#include - namespace mbgl { namespace style { -template <> -optional parseConstant(const char* name, const JSValue& value) { - if (!value.IsBool()) { - Log::Warning(Event::ParseStyle, "value of '%s' must be a boolean", name); - return {}; - } - - return value.GetBool(); -} - -template <> -optional parseConstant(const char* name, const JSValue& value) { - if (!value.IsNumber()) { - Log::Warning(Event::ParseStyle, "value of '%s' must be a number, or a number function", name); - return {}; - } - - return value.GetDouble(); -} - -template <> -optional parseConstant(const char* name, const JSValue& value) { - if (!value.IsString()) { - Log::Warning(Event::ParseStyle, "value of '%s' must be a string", name); - return {}; - } - - return std::string { value.GetString(), value.GetStringLength() }; -} - -template <> -optional parseConstant(const char* name, const JSValue& value) { - if (!value.IsString()) { - Log::Warning(Event::ParseStyle, "value of '%s' must be a string", name); - return {}; - } - - optional result = Color::parse({ value.GetString(), value.GetStringLength() }); - if (!result) { - Log::Warning(Event::ParseStyle, "value of '%s' must be a valid color", name); - return {}; - } - - return result; -} - -template <> -optional> parseConstant(const char* name, const JSValue& value) { - if (value.IsArray() && value.Size() == 2 && - value[rapidjson::SizeType(0)].IsNumber() && - value[rapidjson::SizeType(1)].IsNumber()) { - - float first = value[rapidjson::SizeType(0)].GetDouble(); - float second = value[rapidjson::SizeType(1)].GetDouble(); - return { {{ first, second }} }; - } else { - Log::Warning(Event::ParseStyle, "value of '%s' must be an array of two numbers", name); - return {}; - } -} - -template <> -optional> parseConstant(const char* name, const JSValue& value) { - if (value.IsArray() && value.Size() == 4 && - value[rapidjson::SizeType(0)].IsNumber() && - value[rapidjson::SizeType(1)].IsNumber() && - value[rapidjson::SizeType(2)].IsNumber() && - value[rapidjson::SizeType(3)].IsNumber()) { - - float first = value[rapidjson::SizeType(0)].GetDouble(); - float second = value[rapidjson::SizeType(1)].GetDouble(); - float third = value[rapidjson::SizeType(2)].GetDouble(); - float fourth = value[rapidjson::SizeType(3)].GetDouble(); - return { {{ first, second, third, fourth }} }; - } else { - Log::Warning(Event::ParseStyle, "value of '%s' must be an array of four numbers", name); - return {}; - } -} - -template <> -optional> parseConstant(const char* name, const JSValue& value) { - if (!value.IsArray()) { - Log::Warning(Event::ParseStyle, "value of '%s' must be an array of numbers", name); - return {}; - } - - std::vector result; - - for (rapidjson::SizeType i = 0; i < value.Size(); ++i) { - const JSValue& part = value[i]; - - if (!part.IsNumber()) { - Log::Warning(Event::ParseStyle, "value of '%s' must be an array of numbers", name); - return {}; - } - - result.push_back(part.GetDouble()); - } - - return result; -} - -template <> -optional> parseConstant(const char* name, const JSValue& value) { - if (!value.IsArray()) { - Log::Warning(Event::ParseStyle, "value of '%s' must be an array of strings", name); - return {}; - } - - std::vector result; - - for (rapidjson::SizeType i = 0; i < value.Size(); ++i) { - const JSValue& part = value[i]; - - if (!part.IsString()) { - Log::Warning(Event::ParseStyle, "value of '%s' must be an array of strings", name); - return {}; - } - - result.push_back({ part.GetString(), part.GetStringLength() }); - } - - return result; -} - optional parseTransitionOptions(const char *, const JSValue& value) { if (!value.IsObject()) { return {}; diff --git a/src/mbgl/style/property_parsing.hpp b/src/mbgl/style/property_parsing.hpp index d27a9e503..0c750ca29 100644 --- a/src/mbgl/style/property_parsing.hpp +++ b/src/mbgl/style/property_parsing.hpp @@ -1,117 +1,23 @@ #pragma once -#include #include #include - -#include -#include -#include -#include +#include +#include #include -#include -#include -#include - namespace mbgl { namespace style { -template ::value>> -optional parseConstant(const char* name, const JSValue&); - -template <> optional parseConstant(const char*, const JSValue&); -template <> optional parseConstant(const char*, const JSValue&); -template <> optional parseConstant(const char*, const JSValue&); -template <> optional parseConstant(const char*, const JSValue&); -template <> optional> parseConstant(const char*, const JSValue&); -template <> optional> parseConstant(const char*, const JSValue&); -template <> optional> parseConstant(const char*, const JSValue&); - -template -optional parseConstant(const char* name, const JSValue& value, - typename std::enable_if_t::value, void*> = nullptr) { - if (!value.IsString()) { - Log::Warning(Event::ParseStyle, "value of '%s' must be a string", name); - return {}; - } - - const auto result = Enum::toEnum({ value.GetString(), value.GetStringLength() }); - if (!result) { - Log::Warning(Event::ParseStyle, "value of '%s' must be a valid enumeration value", name); - } - - return result; -} - template PropertyValue parseProperty(const char* name, const JSValue& value) { - if (!value.IsObject()) { - auto constant = parseConstant(name, value); - - if (!constant) { - return {}; - } - - return *constant; - } - - if (!value.HasMember("stops")) { - Log::Warning(Event::ParseStyle, "function must specify a function type"); + conversion::Result> result = conversion::convertPropertyValue(value); + if (result.template is()) { + Log::Warning(Event::ParseStyle, "%s: %s", name, result.template get().message); return {}; } - - float base = 1.0f; - - if (value.HasMember("base")) { - const JSValue& value_base = value["base"]; - - if (!value_base.IsNumber()) { - Log::Warning(Event::ParseStyle, "base must be numeric"); - return {}; - } - - base = value_base.GetDouble(); - } - - const JSValue& stopsValue = value["stops"]; - - if (!stopsValue.IsArray()) { - Log::Warning(Event::ParseStyle, "stops function must specify a stops array"); - return {}; - } - - std::vector> stops; - - for (rapidjson::SizeType i = 0; i < stopsValue.Size(); ++i) { - const JSValue& stop = stopsValue[i]; - - if (!stop.IsArray()) { - Log::Warning(Event::ParseStyle, "function argument must be a numeric value"); - return {}; - } - - if (stop.Size() != 2) { - Log::Warning(Event::ParseStyle, "stop must have zoom level and value specification"); - return {}; - } - - const JSValue& z = stop[rapidjson::SizeType(0)]; - if (!z.IsNumber()) { - Log::Warning(Event::ParseStyle, "zoom level in stop must be a number"); - return {}; - } - - optional v = parseConstant(name, stop[rapidjson::SizeType(1)]); - if (!v) { - return {}; - } - - stops.emplace_back(z.GetDouble(), *v); - } - - return Function(stops, base); + return result.template get>(); } optional parseTransitionOptions(const char * name, const JSValue&); diff --git a/src/mbgl/style/rapidjson_conversion.hpp b/src/mbgl/style/rapidjson_conversion.hpp new file mode 100644 index 000000000..93577ac8b --- /dev/null +++ b/src/mbgl/style/rapidjson_conversion.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include + +namespace mbgl { +namespace style { +namespace conversion { + +inline bool isArray(const JSValue& value) { + return value.IsArray(); +} + +inline std::size_t arrayLength(const JSValue& value) { + return value.Size(); +} + +inline const JSValue& arrayMember(const JSValue& value, std::size_t i) { + return value[rapidjson::SizeType(i)]; +} + +inline bool isObject(const JSValue& value) { + return value.IsObject(); +} + +inline const JSValue* objectMember(const JSValue& value, const char * name) { + if (!value.HasMember(name)) { + return nullptr; + } + return &value[name]; +} + +inline optional toBool(const JSValue& value) { + if (!value.IsBool()) { + return {}; + } + return value.GetBool(); +} + +inline optional toNumber(const JSValue& value) { + if (!value.IsNumber()) { + return {}; + } + return value.GetDouble(); +} + +inline optional toString(const JSValue& value) { + if (!value.IsString()) { + return {}; + } + return {{ value.GetString(), value.GetStringLength() }}; +} + +} // namespace conversion +} // namespace style +} // namespace mbgl diff --git a/test/fixtures/style_parser/circle-blur.info.json b/test/fixtures/style_parser/circle-blur.info.json index 397e4cd4d..60306e20c 100644 --- a/test/fixtures/style_parser/circle-blur.info.json +++ b/test/fixtures/style_parser/circle-blur.info.json @@ -1,7 +1,7 @@ { "default": { "log": [ - [1, "WARNING", "ParseStyle", "value of 'circle-blur' must be a number, or a number function"] + [1, "WARNING", "ParseStyle", "circle-blur: value must be a number"] ] } } diff --git a/test/fixtures/style_parser/circle-color.info.json b/test/fixtures/style_parser/circle-color.info.json index 70375ce8f..9d569ad53 100644 --- a/test/fixtures/style_parser/circle-color.info.json +++ b/test/fixtures/style_parser/circle-color.info.json @@ -1,7 +1,7 @@ { "default": { "log": [ - [1, "WARNING", "ParseStyle", "value of 'circle-color' must be a string"] + [1, "WARNING", "ParseStyle", "circle-color: value must be a string"] ] } } diff --git a/test/fixtures/style_parser/circle-opacity.info.json b/test/fixtures/style_parser/circle-opacity.info.json index 3e8662bdb..bb77cebd1 100644 --- a/test/fixtures/style_parser/circle-opacity.info.json +++ b/test/fixtures/style_parser/circle-opacity.info.json @@ -1,7 +1,7 @@ { "default": { "log": [ - [1, "WARNING", "ParseStyle", "value of 'circle-opacity' must be a number, or a number function"] + [1, "WARNING", "ParseStyle", "circle-opacity: value must be a number"] ] } } diff --git a/test/fixtures/style_parser/circle-radius.info.json b/test/fixtures/style_parser/circle-radius.info.json index 7e87aa4fd..285e962d7 100644 --- a/test/fixtures/style_parser/circle-radius.info.json +++ b/test/fixtures/style_parser/circle-radius.info.json @@ -1,7 +1,7 @@ { "default": { "log": [ - [1, "WARNING", "ParseStyle", "value of 'circle-radius' must be a number, or a number function"] + [1, "WARNING", "ParseStyle", "circle-radius: value must be a number"] ] } } diff --git a/test/fixtures/style_parser/function-numeric.info.json b/test/fixtures/style_parser/function-numeric.info.json index e2170eed4..906cccf10 100644 --- a/test/fixtures/style_parser/function-numeric.info.json +++ b/test/fixtures/style_parser/function-numeric.info.json @@ -1,7 +1,7 @@ { "default": { "log": [ - [1, "WARNING", "ParseStyle", "function argument must be a numeric value"] + [1, "WARNING", "ParseStyle", "line-width: function stop must be an array"] ] } } diff --git a/test/fixtures/style_parser/function-type.info.json b/test/fixtures/style_parser/function-type.info.json index 1549262bb..8a844bf7c 100644 --- a/test/fixtures/style_parser/function-type.info.json +++ b/test/fixtures/style_parser/function-type.info.json @@ -1,7 +1,7 @@ { "default": { "log": [ - [1, "WARNING", "ParseStyle", "function must specify a function type"] + [1, "WARNING", "ParseStyle", "line-width: function value must specify stops"] ] } } diff --git a/test/fixtures/style_parser/line-opacity.info.json b/test/fixtures/style_parser/line-opacity.info.json index 36f02e6e0..63af59e28 100644 --- a/test/fixtures/style_parser/line-opacity.info.json +++ b/test/fixtures/style_parser/line-opacity.info.json @@ -1,7 +1,7 @@ { "default": { "log": [ - [1, "WARNING", "ParseStyle", "value of 'line-opacity' must be a number, or a number function"] + [1, "WARNING", "ParseStyle", "line-opacity: value must be a number"] ] } } diff --git a/test/fixtures/style_parser/line-translate.info.json b/test/fixtures/style_parser/line-translate.info.json index ff126bbf2..a23e17012 100644 --- a/test/fixtures/style_parser/line-translate.info.json +++ b/test/fixtures/style_parser/line-translate.info.json @@ -1,7 +1,7 @@ { "default": { "log": [ - [1, "WARNING", "ParseStyle", "value of 'line-translate' must be an array of two numbers"] + [1, "WARNING", "ParseStyle", "line-translate: value must be an array of two numbers"] ] } } diff --git a/test/fixtures/style_parser/line-width.info.json b/test/fixtures/style_parser/line-width.info.json index af2f9b284..9e549a022 100644 --- a/test/fixtures/style_parser/line-width.info.json +++ b/test/fixtures/style_parser/line-width.info.json @@ -1,7 +1,7 @@ { "default": { "log": [ - [1, "WARNING", "ParseStyle", "value of 'line-width' must be a number, or a number function"] + [1, "WARNING", "ParseStyle", "line-width: value must be a number"] ] } } diff --git a/test/fixtures/style_parser/stop-zoom-value.info.json b/test/fixtures/style_parser/stop-zoom-value.info.json index deaba65e2..3abf1867a 100644 --- a/test/fixtures/style_parser/stop-zoom-value.info.json +++ b/test/fixtures/style_parser/stop-zoom-value.info.json @@ -1,7 +1,7 @@ { "default": { "log": [ - [1, "WARNING", "ParseStyle", "stop must have zoom level and value specification"] + [1, "WARNING", "ParseStyle", "fill-opacity: function stop must have two elements"] ] } } diff --git a/test/fixtures/style_parser/stops-array.info.json b/test/fixtures/style_parser/stops-array.info.json index 3324958c8..b82f5a97d 100644 --- a/test/fixtures/style_parser/stops-array.info.json +++ b/test/fixtures/style_parser/stops-array.info.json @@ -1,7 +1,7 @@ { "default": { "log": [ - [1, "WARNING", "ParseStyle", "stops function must specify a stops array"] + [1, "WARNING", "ParseStyle", "line-width: function stops must be an array"] ] } } diff --git a/test/fixtures/style_parser/text-size.info.json b/test/fixtures/style_parser/text-size.info.json index 871a6ad49..b412c6bee 100644 --- a/test/fixtures/style_parser/text-size.info.json +++ b/test/fixtures/style_parser/text-size.info.json @@ -1,7 +1,7 @@ { "default": { "log": [ - [1, "WARNING", "ParseStyle", "value of 'text-size' must be a number, or a number function"] + [1, "WARNING", "ParseStyle", "text-size: value must be a number"] ] } }