diff --git a/CMakeLists.txt b/CMakeLists.txt index afdb84b16d7b..8570476c2437 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -247,6 +247,7 @@ list(APPEND INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/fill_layer_factory.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/heatmap_layer_factory.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/hillshade_layer_factory.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/color_relief_layer_factory.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/layer_factory.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/layer_manager.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/line_layer_factory.hpp @@ -358,6 +359,7 @@ list(APPEND INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/fill_layer.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/heatmap_layer.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/hillshade_layer.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/color_relief_layer.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/line_layer.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/location_indicator_layer.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/raster_layer.hpp @@ -497,6 +499,7 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/fill_layer_factory.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/heatmap_layer_factory.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/hillshade_layer_factory.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/color_relief_layer_factory.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/layer_factory.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/layer_manager.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/line_layer_factory.cpp @@ -578,6 +581,10 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_heatmap_layer.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_hillshade_layer.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_hillshade_layer.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_color_relief_layer.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_color_relief_layer.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/color_relief_layer_tweaker.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/color_relief_layer_tweaker.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_line_layer.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_line_layer.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_raster_layer.cpp @@ -676,6 +683,7 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/style/conversion/light.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/conversion/position.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/conversion/property_value.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/style/conversion/hillshade_conversions.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/conversion/rotation.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/conversion/source.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/conversion/source_options.cpp @@ -761,6 +769,11 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/hillshade_layer_impl.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/hillshade_layer_properties.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/hillshade_layer_properties.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/color_relief_layer.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/color_relief_layer_impl.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/color_relief_layer_impl.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/color_relief_layer_properties.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/color_relief_layer_properties.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/line_layer.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/line_layer_impl.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/line_layer_impl.hpp @@ -1012,6 +1025,7 @@ if(MLN_WITH_OPENGL) ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/gl/heatmap_texture.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/gl/hillshade_prepare.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/gl/hillshade.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/gl/color_relief.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/gl/line.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/gl/line_gradient.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/gl/line_pattern.hpp @@ -1088,6 +1102,7 @@ if(MLN_WITH_OPENGL) ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/heatmap_layer_ubo.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/heatmap_texture_layer_ubo.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/hillshade_layer_ubo.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/color_relief_layer_ubo.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/hillshade_prepare_layer_ubo.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/layer_ubo.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/line_layer_ubo.hpp @@ -1163,6 +1178,7 @@ if(MLN_WITH_METAL) ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/mtl/heatmap_texture.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/mtl/hillshade.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/mtl/hillshade_prepare.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/mtl/color_relief.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/mtl/line.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/mtl/location_indicator.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/mtl/raster.hpp @@ -1204,6 +1220,7 @@ if(MLN_WITH_METAL) ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/mtl/heatmap_texture.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/mtl/hillshade.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/mtl/hillshade_prepare.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/mtl/color_relief.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/mtl/line.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/mtl/location_indicator.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/mtl/raster.cpp @@ -1264,6 +1281,7 @@ if(MLN_WITH_VULKAN) ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/vulkan/heatmap_texture.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/vulkan/hillshade.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/vulkan/hillshade_prepare.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/vulkan/color_relief.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/vulkan/line.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/vulkan/location_indicator.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/vulkan/raster.hpp @@ -1307,6 +1325,7 @@ if(MLN_WITH_VULKAN) ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/vulkan/heatmap_texture.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/vulkan/hillshade.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/vulkan/hillshade_prepare.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/vulkan/color_relief.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/vulkan/line.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/vulkan/location_indicator.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/vulkan/raster.cpp @@ -1350,6 +1369,7 @@ if(MLN_WITH_WEBGPU) ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/heatmap_texture.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/hillshade.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/hillshade_prepare.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/color_relief.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/line.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/location_indicator.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/raster.cpp @@ -1388,6 +1408,7 @@ if(MLN_WITH_WEBGPU) ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/heatmap_texture.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/hillshade.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/hillshade_prepare.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/color_relief.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/line.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/location_indicator.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/raster.hpp @@ -1587,7 +1608,7 @@ target_link_libraries( mbgl-vendor-unique_resource mbgl-vendor-vector-tile mbgl-vendor-wagyu - mlt-cpp + mlt-cpp $<$:mbgl-vendor-metal-cpp> $,mbgl-rustutils,mbgl-vendor-csscolorparser> $<$:mbgl-freetype> diff --git a/bazel/core.bzl b/bazel/core.bzl index e431a9598114..3a5b36435efd 100644 --- a/bazel/core.bzl +++ b/bazel/core.bzl @@ -23,6 +23,7 @@ MLN_PUBLIC_GENERATED_STYLE_HEADERS = [ "include/mbgl/style/layers/fill_extrusion_layer.hpp", "include/mbgl/style/layers/raster_layer.hpp", "include/mbgl/style/layers/hillshade_layer.hpp", + "include/mbgl/style/layers/color_relief_layer.hpp", "include/mbgl/style/layers/background_layer.hpp", "include/mbgl/style/layers/location_indicator_layer.hpp", "include/mbgl/style/light.hpp", @@ -58,6 +59,8 @@ MLN_GENERATED_STYLE_SOURCE = [ "src/mbgl/style/layers/raster_layer.cpp", "src/mbgl/style/layers/hillshade_layer_properties.cpp", "src/mbgl/style/layers/hillshade_layer.cpp", + "src/mbgl/style/layers/color_relief_layer_properties.cpp", + "src/mbgl/style/layers/color_relief_layer.cpp", "src/mbgl/style/layers/background_layer_properties.cpp", "src/mbgl/style/layers/background_layer.cpp", "src/mbgl/style/layers/location_indicator_layer_properties.cpp", @@ -95,6 +98,7 @@ MLN_GENERATED_OPENGL_SHADER_HEADERS = [ "include/mbgl/shaders/gl/heatmap_texture.hpp", "include/mbgl/shaders/gl/hillshade_prepare.hpp", "include/mbgl/shaders/gl/hillshade.hpp", + "include/mbgl/shaders/gl/color_relief.hpp", "include/mbgl/shaders/gl/line_gradient.hpp", "include/mbgl/shaders/gl/line_pattern.hpp", "include/mbgl/shaders/gl/line_sdf.hpp", @@ -161,6 +165,7 @@ MLN_CORE_SOURCE = [ "src/mbgl/layermanager/fill_layer_factory.cpp", "src/mbgl/layermanager/heatmap_layer_factory.cpp", "src/mbgl/layermanager/hillshade_layer_factory.cpp", + "src/mbgl/layermanager/color_relief_layer_factory.cpp", "src/mbgl/layermanager/layer_factory.cpp", "src/mbgl/layermanager/layer_manager.cpp", "src/mbgl/layermanager/line_layer_factory.cpp", @@ -237,6 +242,8 @@ MLN_CORE_SOURCE = [ "src/mbgl/renderer/layers/render_heatmap_layer.hpp", "src/mbgl/renderer/layers/render_hillshade_layer.cpp", "src/mbgl/renderer/layers/render_hillshade_layer.hpp", + "src/mbgl/renderer/layers/render_color_relief_layer.cpp", + "src/mbgl/renderer/layers/render_color_relief_layer.hpp", "src/mbgl/renderer/layers/render_line_layer.cpp", "src/mbgl/renderer/layers/render_line_layer.hpp", "src/mbgl/renderer/layers/render_location_indicator_layer.cpp", @@ -336,6 +343,7 @@ MLN_CORE_SOURCE = [ "src/mbgl/style/conversion/light.cpp", "src/mbgl/style/conversion/position.cpp", "src/mbgl/style/conversion/property_value.cpp", + "src/mbgl/style/conversion/hillshade_conversions.cpp", "src/mbgl/style/conversion/rotation.cpp", "src/mbgl/style/conversion/source.cpp", "src/mbgl/style/conversion/source_options.cpp", @@ -409,6 +417,8 @@ MLN_CORE_SOURCE = [ "src/mbgl/style/layers/heatmap_layer_impl.hpp", "src/mbgl/style/layers/hillshade_layer_impl.cpp", "src/mbgl/style/layers/hillshade_layer_impl.hpp", + "src/mbgl/style/layers/color_relief_layer_impl.cpp", + "src/mbgl/style/layers/color_relief_layer_impl.hpp", "src/mbgl/style/layers/line_layer_impl.cpp", "src/mbgl/style/layers/line_layer_impl.hpp", "src/mbgl/style/layers/location_indicator_layer_impl.cpp", @@ -649,6 +659,7 @@ MLN_CORE_HEADERS = [ "include/mbgl/layermanager/fill_layer_factory.hpp", "include/mbgl/layermanager/heatmap_layer_factory.hpp", "include/mbgl/layermanager/hillshade_layer_factory.hpp", + "include/mbgl/layermanager/color_relief_layer_factory.hpp", "include/mbgl/layermanager/layer_factory.hpp", "include/mbgl/layermanager/layer_manager.hpp", "include/mbgl/layermanager/line_layer_factory.hpp", @@ -984,6 +995,7 @@ MLN_DRAWABLES_HEADERS = [ "include/mbgl/shaders/heatmap_layer_ubo.hpp", "include/mbgl/shaders/heatmap_texture_layer_ubo.hpp", "include/mbgl/shaders/hillshade_layer_ubo.hpp", + "include/mbgl/shaders/color_relief_layer_ubo.hpp", "include/mbgl/shaders/hillshade_prepare_layer_ubo.hpp", "include/mbgl/shaders/layer_ubo.hpp", "include/mbgl/shaders/line_layer_ubo.hpp", @@ -1057,6 +1069,7 @@ MLN_DRAWABLES_MTL_SOURCE = [ "src/mbgl/shaders/mtl/heatmap_texture.cpp", "src/mbgl/shaders/mtl/hillshade.cpp", "src/mbgl/shaders/mtl/hillshade_prepare.cpp", + "src/mbgl/shaders/mtl/color_relief.cpp", "src/mbgl/shaders/mtl/line.cpp", "src/mbgl/shaders/mtl/location_indicator.cpp", "src/mbgl/shaders/mtl/raster.cpp", @@ -1098,6 +1111,7 @@ MLN_DRAWABLES_MTL_HEADERS = [ "include/mbgl/shaders/mtl/heatmap_texture.hpp", "include/mbgl/shaders/mtl/hillshade.hpp", "include/mbgl/shaders/mtl/hillshade_prepare.hpp", + "include/mbgl/shaders/mtl/color_relief.hpp", "include/mbgl/shaders/mtl/line.hpp", "include/mbgl/shaders/mtl/location_indicator.hpp", "include/mbgl/shaders/mtl/raster.hpp", @@ -1142,6 +1156,7 @@ MLN_DRAWABLES_WEBGPU_SOURCE = [ "src/mbgl/shaders/webgpu/heatmap_texture.cpp", "src/mbgl/shaders/webgpu/hillshade.cpp", "src/mbgl/shaders/webgpu/hillshade_prepare.cpp", + "src/mbgl/shaders/webgpu/color_relief.cpp", "src/mbgl/shaders/webgpu/line.cpp", "src/mbgl/shaders/webgpu/location_indicator.cpp", "src/mbgl/shaders/webgpu/raster.cpp", @@ -1183,6 +1198,7 @@ MLN_DRAWABLES_WEBGPU_HEADERS = [ "include/mbgl/shaders/webgpu/heatmap_texture.hpp", "include/mbgl/shaders/webgpu/hillshade.hpp", "include/mbgl/shaders/webgpu/hillshade_prepare.hpp", + "include/mbgl/shaders/webgpu/color_relief.hpp", "include/mbgl/shaders/webgpu/line.hpp", "include/mbgl/shaders/webgpu/location_indicator.hpp", "include/mbgl/shaders/webgpu/raster.hpp", diff --git a/include/mbgl/layermanager/color_relief_layer_factory.hpp b/include/mbgl/layermanager/color_relief_layer_factory.hpp new file mode 100644 index 000000000000..4e5222c117b0 --- /dev/null +++ b/include/mbgl/layermanager/color_relief_layer_factory.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace mbgl { + +class ColorReliefLayerFactory : public LayerFactory { +protected: + const style::LayerTypeInfo* getTypeInfo() const noexcept final; + std::unique_ptr createLayer(const std::string& id, + const style::conversion::Convertible& value) noexcept final; + std::unique_ptr createRenderLayer(Immutable) noexcept final; +}; + +} // namespace mbgl diff --git a/include/mbgl/shaders/color_relief_layer_ubo.hpp b/include/mbgl/shaders/color_relief_layer_ubo.hpp new file mode 100644 index 000000000000..4a655dc65d7f --- /dev/null +++ b/include/mbgl/shaders/color_relief_layer_ubo.hpp @@ -0,0 +1,32 @@ +#pragma once +#include + +namespace mbgl { +namespace shaders { + +struct alignas(16) ColorReliefDrawableUBO { + /* 0 */ std::array matrix; + /* 64 */ +}; +static_assert(sizeof(ColorReliefDrawableUBO) == 64); + +struct alignas(16) ColorReliefTilePropsUBO { + /* 0 */ std::array unpack; // DEM unpack vector + /* 16 */ std::array dimension; // Texture dimensions + /* 24 */ int32_t color_ramp_size; // Number of color stops + /* 28 */ float pad_tile0; // Padding for alignment + /* 32 */ +}; +static_assert(sizeof(ColorReliefTilePropsUBO) == 32); + +struct alignas(16) ColorReliefEvaluatedPropsUBO { + /* 0 */ float opacity; + /* 4 */ float pad_eval0; + /* 8 */ float pad_eval1; + /* 12 */ float pad_eval2; + /* 16 */ +}; +static_assert(sizeof(ColorReliefEvaluatedPropsUBO) == 16); + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/gl/color_relief.hpp b/include/mbgl/shaders/gl/color_relief.hpp new file mode 100644 index 000000000000..e9497292dc8e --- /dev/null +++ b/include/mbgl/shaders/gl/color_relief.hpp @@ -0,0 +1,115 @@ +// Generated code, do not modify this file! +#pragma once +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "ColorReliefShader"; + static constexpr const char* vertex = R"(layout(std140) uniform ColorReliefDrawableUBO { + highp mat4 u_matrix; +}; + +layout(std140) uniform ColorReliefTilePropsUBO { + highp vec4 u_unpack; + highp vec2 u_dimension; + int u_color_ramp_size; + float pad_tile0; +}; + +in vec2 a_pos; +out vec2 v_pos; + +void main() { + gl_Position = u_matrix * vec4(a_pos, 0, 1); + + highp vec2 epsilon = 1.0 / u_dimension; + float scale = (u_dimension.x - 2.0) / u_dimension.x; + v_pos = (a_pos / 8192.0) * scale + epsilon; + + // Handle poles + if (a_pos.y < -32767.5) v_pos.y = 0.0; + if (a_pos.y > 32766.5) v_pos.y = 1.0; +} +)"; + static constexpr const char* fragment = R"(layout(std140) uniform ColorReliefDrawableUBO { + highp mat4 u_matrix; +}; + +layout(std140) uniform ColorReliefTilePropsUBO { + highp vec4 u_unpack; + highp vec2 u_dimension; + int u_color_ramp_size; + float pad_tile0; +}; + +layout(std140) uniform ColorReliefEvaluatedPropsUBO { + float u_opacity; + float pad_eval0; + float pad_eval1; + float pad_eval2; +}; + +uniform sampler2D u_image; +uniform sampler2D u_elevation_stops; +uniform sampler2D u_color_stops; + +in vec2 v_pos; + +float getElevation(vec2 coord) { + // Convert encoded elevation value to meters + vec4 data = texture(u_image, coord) * 255.0; + data.a = -1.0; + return dot(data, u_unpack); +} + +float getElevationStop(int stop) { + // Elevation stops are plain float values, not terrain-RGB encoded + float x = (float(stop) + 0.5) / float(u_color_ramp_size); + return texture(u_elevation_stops, vec2(x, 0.0)).r; +} + +vec4 getColorStop(int stop) { + float x = (float(stop) + 0.5) / float(u_color_ramp_size); + return texture(u_color_stops, vec2(x, 0.0)); +} + +void main() { + float el = getElevation(v_pos); + + // Binary search for color stops + int r = (u_color_ramp_size - 1); + int l = 0; + + while (r - l > 1) { + int m = (r + l) / 2; + float el_m = getElevationStop(m); + if (el < el_m) { + r = m; + } else { + l = m; + } + } + + // Get elevation values for interpolation + float el_l = getElevationStop(l); + float el_r = getElevationStop(r); + + // Get colors for interpolation + vec4 color_l = getColorStop(l); + vec4 color_r = getColorStop(r); + + // Interpolate between the two colors + float t = clamp((el - el_l) / (el_r - el_l), 0.0, 1.0); + fragColor = u_opacity * mix(color_l, color_r, t); + +#ifdef OVERDRAW_INSPECTOR + fragColor = vec4(1.0); +#endif +})"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/gl/hillshade.hpp b/include/mbgl/shaders/gl/hillshade.hpp index 8d186538040b..48b61392426b 100644 --- a/include/mbgl/shaders/gl/hillshade.hpp +++ b/include/mbgl/shaders/gl/hillshade.hpp @@ -25,55 +25,183 @@ void main() { static constexpr const char* fragment = R"(in vec2 v_pos; uniform sampler2D u_image; -layout (std140) uniform HillshadeTilePropsUBO { +layout(std140) uniform HillshadeTilePropsUBO { highp vec2 u_latrange; - highp vec2 u_light; + highp float u_exaggeration; + highp int u_method; // Hillshade method (0: STANDARD, 1: COMBINED, 2: IGOR, 3: MULTIDIRECTIONAL, 4: BASIC) + highp int u_num_lights; // Number of light sources (1-4) + highp float u_pad0; + highp float u_pad1; + highp float u_pad2; }; - -layout (std140) uniform HillshadeEvaluatedPropsUBO { - highp vec4 u_highlight; - highp vec4 u_shadow; +layout(std140) uniform HillshadeEvaluatedPropsUBO { highp vec4 u_accent; + highp vec4 u_altitudes; // Up to 4 altitude values (in radians) + highp vec4 u_azimuths; // Up to 4 azimuth values (in radians) + highp vec4 u_shadows[4]; // Shadow colors (up to 4 lights) + highp vec4 u_highlights[4]; // Highlight colors (up to 4 lights) }; #define PI 3.141592653589793 -void main() { - vec4 pixel = texture(u_image, v_pos); +#define STANDARD 0 +#define COMBINED 1 +#define IGOR 2 +#define MULTIDIRECTIONAL 3 +#define BASIC 4 - vec2 deriv = ((pixel.rg * 2.0) - 1.0); +float get_aspect(vec2 deriv) { + return deriv.x != 0.0 ? + atan(deriv.y, -deriv.x) : PI / 2.0 * (deriv.y > 0.0 ? 1.0 : -1.0); +} - // We divide the slope by a scale factor based on the cosin of the pixel's approximate latitude - // to account for mercator projection distortion. see #4807 for details - float scaleFactor = cos(radians((u_latrange[0] - u_latrange[1]) * (1.0 - v_pos.y) + u_latrange[1])); - // We also multiply the slope by an arbitrary z-factor of 1.25 - float slope = atan(1.25 * length(deriv) / scaleFactor); - float aspect = deriv.x != 0.0 ? atan(deriv.y, -deriv.x) : PI / 2.0 * (deriv.y > 0.0 ? 1.0 : -1.0); - - float intensity = u_light.x; - // We add PI to make this property match the global light object, which adds PI/2 to the light's azimuthal - // position property to account for 0deg corresponding to north/the top of the viewport in the style spec - // and the original shader was written to accept (-illuminationDirection - 90) as the azimuthal. - float azimuth = u_light.y + PI; - - // We scale the slope exponentially based on intensity, using a calculation similar to - // the exponential interpolation function in the style spec: - // https://github.com/mapbox/mapbox-gl-js/blob/master/src/style-spec/expression/definitions/interpolate.js#L217-L228 - // so that higher intensity values create more opaque hillshading. +// MapLibre's legacy hillshade algorithm (Method 0) +void standard_hillshade(vec2 deriv) { + float azimuth = u_azimuths.x + PI; + float slope = atan(0.625 * length(deriv)); + float aspect = get_aspect(deriv); + + // Note: This implementation uses u_exaggeration as intensity, though it typically should be derived from u_altitudes.x. + float intensity = u_exaggeration; + + // Scale the slope exponentially based on intensity float base = 1.875 - intensity * 1.75; float maxValue = 0.5 * PI; float scaledSlope = intensity != 0.5 ? ((pow(base, slope) - 1.0) / (pow(base, maxValue) - 1.0)) * maxValue : slope; - - // The accent color is calculated with the cosine of the slope while the shade color is calculated with the sine - // so that the accent color's rate of change eases in while the shade color's eases out. + float accent = cos(scaledSlope); - // We multiply both the accent and shade color by a clamped intensity value - // so that intensities >= 0.5 do not additionally affect the color values - // while intensity values < 0.5 make the overall color more transparent. vec4 accent_color = (1.0 - accent) * u_accent * clamp(intensity * 2.0, 0.0, 1.0); + + // Calculate shadow/highlight based on aspect float shade = abs(mod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); - vec4 shade_color = mix(u_shadow, u_highlight, shade) * sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0); + vec4 shade_color = mix(u_shadows[0], u_highlights[0], shade) * sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0); + fragColor = accent_color * (1.0 - shade_color.a) + shade_color; +} + +// Basic directional hillshade (Method 4) +void basic_hillshade(vec2 deriv) { + deriv = deriv * u_exaggeration * 2.0; // Exaggerate the slope derivative + float azimuth = u_azimuths.x + PI; + float cos_az = cos(azimuth); + float sin_az = sin(azimuth); + float cos_alt = cos(u_altitudes.x); + float sin_alt = sin(u_altitudes.x); + + // Calculate the cosine of the angle between the light vector and the surface normal + float cang = (sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / sqrt(1.0 + dot(deriv, deriv)); + float shade = clamp(cang, 0.0, 1.0); // cang is the hillshade intensity [0, 1] + + // Blend shadow and highlight based on intensity + if (shade > 0.5) { + fragColor = u_highlights[0] * (2.0 * shade - 1.0); // Highlight strength [0, 1] + } else { + fragColor = u_shadows[0] * (1.0 - 2.0 * shade); // Shadow strength [0, 1] + } +} + +// Multidirectional hillshade (Method 3) +void multidirectional_hillshade(vec2 deriv) { + deriv = deriv * u_exaggeration * 2.0; // Exaggerate the slope derivative + fragColor = vec4(0, 0, 0, 0); + + // Iterate through all light sources (up to u_num_lights, max 4) + for (int i = 0; i < u_num_lights; i++) { + // Access altitude and azimuth from vec4 UBOs + float altitude = (i == 0) ? u_altitudes.x : (i == 1) ? u_altitudes.y : (i == 2) ? u_altitudes.z : u_altitudes.w; + float azimuth = (i == 0) ? u_azimuths.x : (i == 1) ? u_azimuths.y : (i == 2) ? u_azimuths.z : u_azimuths.w; + + float cos_alt = cos(altitude); + float sin_alt = sin(altitude); + // Negate cos/sin azimuth for correct light direction in the normal calculation + float cos_az = -cos(azimuth); + float sin_az = -sin(azimuth); + + // Calculate the cosine of the angle between the light vector and the surface normal + float cang = (sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / + sqrt(1.0 + dot(deriv, deriv)); + float shade = clamp(cang, 0.0, 1.0); // cang is the hillshade intensity [0, 1] + + // Accumulate shadow/highlight contribution from each light + if (shade > 0.5) { + fragColor += u_highlights[i] * (2.0 * shade - 1.0) / float(u_num_lights); + } else { + fragColor += u_shadows[i] * (1.0 - 2.0 * shade) / float(u_num_lights); + } + } +} + +// Combined shadow and highlight method (Method 1) +void combined_hillshade(vec2 deriv) { + // Only supports one light source (index 0) + deriv = deriv * u_exaggeration * 2.0; + float azimuth = u_azimuths.x + PI; + float cos_az = cos(azimuth); + float sin_az = sin(azimuth); + float cos_alt = cos(u_altitudes.x); + float sin_alt = sin(u_altitudes.x); + + // Calculate the angle between the light vector and the surface normal + float cang = acos((sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / + sqrt(1.0 + dot(deriv, deriv))); + + cang = clamp(cang, 0.0, PI / 2.0); // Clamp angle to 90 degrees (half hemisphere) + + // The "shade" and "highlight" components are calculated from cang (angle) and the magnitude of the slope + float shade = cang * atan(length(deriv)) * 4.0 / PI / PI; + float highlight = (PI / 2.0 - cang) * atan(length(deriv)) * 4.0 / PI / PI; + + fragColor = u_shadows[0] * shade + u_highlights[0] * highlight; +} + +// Igor's shadow/highlight method (Method 2) +void igor_hillshade(vec2 deriv) { + // Only supports one light source (index 0) + deriv = deriv * u_exaggeration * 2.0; + float aspect = get_aspect(deriv); + float azimuth = u_azimuths.x + PI; + + // Slope strength is magnitude of slope vector, normalized to [0, 1] + float slope_strength = atan(length(deriv)) * 2.0 / PI; + + // Aspect strength is difference between aspect and light azimuth, normalized to [0, 1] + float aspect_strength = 1.0 - abs(mod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); + + float shadow_strength = slope_strength * aspect_strength; + float highlight_strength = slope_strength * (1.0 - aspect_strength); + + fragColor = u_shadows[0] * shadow_strength + u_highlights[0] * highlight_strength; +} + +void main() { + vec4 pixel = texture(u_image, v_pos); + + // Scale the derivative based on the mercator distortion at this latitude (v_pos.y) + float scaleFactor = cos(radians((u_latrange[0] - u_latrange[1]) * (1.0 - v_pos.y) + u_latrange[1])); + + // The derivative is scaled back from [0, 1] texture range to world-space slope + // Texture range [0, 1] corresponds to slope range [-4, 4] (8.0 * 0.5 * deriv) + vec2 deriv = ((pixel.rg * 8.0) - 4.0) / scaleFactor; + + // Dispatch to the selected hillshade method + switch (u_method) { + case BASIC: + basic_hillshade(deriv); + break; + case COMBINED: + combined_hillshade(deriv); + break; + case IGOR: + igor_hillshade(deriv); + break; + case MULTIDIRECTIONAL: + multidirectional_hillshade(deriv); + break; + case STANDARD: + default: + standard_hillshade(deriv); + break; + } #ifdef OVERDRAW_INSPECTOR fragColor = vec4(1.0); diff --git a/include/mbgl/shaders/gl/hillshade_prepare.hpp b/include/mbgl/shaders/gl/hillshade_prepare.hpp index 2a2bf01b5334..ab94ee055e1a 100644 --- a/include/mbgl/shaders/gl/hillshade_prepare.hpp +++ b/include/mbgl/shaders/gl/hillshade_prepare.hpp @@ -38,7 +38,6 @@ precision highp float; in vec2 v_pos; uniform sampler2D u_image; - layout (std140) uniform HillshadePrepareTilePropsUBO { highp vec4 u_unpack; highp vec2 u_dimension; @@ -50,13 +49,14 @@ float getElevation(vec2 coord, float bias) { // Convert encoded elevation value to meters vec4 data = texture(u_image, coord) * 255.0; data.a = -1.0; - return dot(data, u_unpack) / 4.0; + return dot(data, u_unpack); } void main() { vec2 epsilon = 1.0 / u_dimension; + float tileSize = u_dimension.x - 2.0; - // queried pixels: + // queried pixels (using Sobel operator kernel): // +-----------+ // | | | | // | a | b | c | @@ -80,31 +80,28 @@ void main() { float g = getElevation(v_pos + vec2(-epsilon.x, epsilon.y), 0.0); float h = getElevation(v_pos + vec2(0, epsilon.y), 0.0); float i = getElevation(v_pos + vec2(epsilon.x, epsilon.y), 0.0); - - // here we divide the x and y slopes by 8 * pixel size - // where pixel size (aka meters/pixel) is: - // circumference of the world / (pixels per tile * number of tiles) - // which is equivalent to: 8 * 40075016.6855785 / (512 * pow(2, u_zoom)) - // which can be reduced to: pow(2, 19.25619978527 - u_zoom) - // we want to vertically exaggerate the hillshading though, because otherwise - // it is barely noticeable at low zooms. to do this, we multiply this by some - // scale factor pow(2, (u_zoom - u_maxzoom) * a) where a is an arbitrary value - // Here we use a=0.3 which works out to the expression below. see - // nickidlugash's awesome breakdown for more info + + // Convert the raw pixel-space derivative (slope) into world-space slope. + // The conversion factor is: tileSize / (8 * meters_per_pixel). + // meters_per_pixel is calculated as pow(2.0, 28.2562 - u_zoom). + // The exaggeration factor is applied to scale the effect at lower zooms. + // See nickidlugash's awesome breakdown for more info // https://github.com/mapbox/mapbox-gl-js/pull/5286#discussion_r148419556 float exaggeration = u_zoom < 2.0 ? 0.4 : u_zoom < 4.5 ? 0.35 : 0.3; vec2 deriv = vec2( (c + f + f + i) - (a + d + d + g), (g + h + h + i) - (a + b + b + c) - ) / pow(2.0, (u_zoom - u_maxzoom) * exaggeration + 19.2562 - u_zoom); - + ) * tileSize / pow(2.0, (u_zoom - u_maxzoom) * exaggeration + 28.2562 - u_zoom); + + // Encode the derivative into the color channels (r and g) + // The derivative is scaled from world-space slope to the range [0, 1] for texture storage. + // The maximum possible world-space derivative is assumed to be 4 (hence division by 8.0). fragColor = clamp(vec4( - deriv.x / 2.0 + 0.5, - deriv.y / 2.0 + 0.5, + deriv.x / 8.0 + 0.5, + deriv.y / 8.0 + 0.5, 1.0, 1.0), 0.0, 1.0); - #ifdef OVERDRAW_INSPECTOR fragColor = vec4(1.0); #endif diff --git a/include/mbgl/shaders/gl/shader_info.hpp b/include/mbgl/shaders/gl/shader_info.hpp index 1d9c6c96998e..f8c111ba4f87 100644 --- a/include/mbgl/shaders/gl/shader_info.hpp +++ b/include/mbgl/shaders/gl/shader_info.hpp @@ -163,6 +163,13 @@ struct ShaderInfo { static const std::vector textures; }; +template <> +struct ShaderInfo { + static const std::vector attributes; + static const std::vector uniformBlocks; + static const std::vector textures; +}; + template <> struct ShaderInfo { static const std::vector attributes; diff --git a/include/mbgl/shaders/hillshade_layer_ubo.hpp b/include/mbgl/shaders/hillshade_layer_ubo.hpp index 7edee8f77290..ead35c71bcea 100644 --- a/include/mbgl/shaders/hillshade_layer_ubo.hpp +++ b/include/mbgl/shaders/hillshade_layer_ubo.hpp @@ -1,10 +1,20 @@ #pragma once - #include namespace mbgl { namespace shaders { +// Maximum number of illumination sources supported +constexpr int MAX_ILLUMINATION_SOURCES = 4; + +enum class HillshadeMethod : int32_t { + Standard = 0, + Combined = 1, + Igor = 2, + Multidirectional = 3, + Basic = 4 +}; + struct alignas(16) HillshadeDrawableUBO { /* 0 */ std::array matrix; /* 64 */ @@ -13,19 +23,26 @@ static_assert(sizeof(HillshadeDrawableUBO) == 4 * 16); struct alignas(16) HillshadeTilePropsUBO { /* 0 */ std::array latrange; - /* 8 */ std::array light; - /* 16 */ + /* 8 */ float exaggeration; // NEW: replaces light[0] (intensity) + /* 12 */ int32_t method; // NEW: hillshade method (0-4) + /* 16 */ int32_t num_lights; // NEW: number of light sources (1-4) + /* 20 */ float pad0; // Padding for alignment + /* 24 */ float pad1; // Padding for alignment + /* 28 */ float pad2; // Padding for alignment + /* 32 */ }; -static_assert(sizeof(HillshadeTilePropsUBO) == 16); +static_assert(sizeof(HillshadeTilePropsUBO) == 32); /// Evaluated properties that do not depend on the tile struct alignas(16) HillshadeEvaluatedPropsUBO { - /* 0 */ Color highlight; - /* 16 */ Color shadow; - /* 32 */ Color accent; - /* 48 */ + /* 0 */ Color accent; + /* 16 */ std::array altitudes; // NEW: altitude values in radians (up to 4) + /* 32 */ std::array azimuths; // NEW: azimuth values in radians (up to 4) + /* 48 */ std::array shadows; // NEW: 4 shadow colors (4 colors × 4 RGBA) + /* 112 */ std::array highlights; // NEW: 4 highlight colors (4 colors × 4 RGBA) + /* 176 */ }; -static_assert(sizeof(HillshadeEvaluatedPropsUBO) == 3 * 16); +static_assert(sizeof(HillshadeEvaluatedPropsUBO) == 176); } // namespace shaders } // namespace mbgl diff --git a/include/mbgl/shaders/mtl/color_relief.hpp b/include/mbgl/shaders/mtl/color_relief.hpp new file mode 100644 index 000000000000..5b86c976af95 --- /dev/null +++ b/include/mbgl/shaders/mtl/color_relief.hpp @@ -0,0 +1,161 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +constexpr auto colorReliefShaderPrelude = R"( + +enum { + idColorReliefDrawableUBO = idDrawableReservedVertexOnlyUBO, + idColorReliefTilePropsUBO = idDrawableReservedFragmentOnlyUBO, + idColorReliefEvaluatedPropsUBO = drawableReservedUBOCount, + colorReliefUBOCount +}; + +struct alignas(16) ColorReliefDrawableUBO { + /* 0 */ float4x4 matrix; + /* 64 */ +}; +static_assert(sizeof(ColorReliefDrawableUBO) == 4 * 16, "wrong size"); + +struct alignas(16) ColorReliefTilePropsUBO { + /* 0 */ float4 unpack; + /* 16 */ float2 dimension; + /* 24 */ int32_t color_ramp_size; + /* 28 */ float pad_tile0; + /* 32 */ +}; +static_assert(sizeof(ColorReliefTilePropsUBO) == 2 * 16, "wrong size"); + +struct alignas(16) ColorReliefEvaluatedPropsUBO { + /* 0 */ float opacity; + /* 4 */ float pad_eval0; + /* 8 */ float pad_eval1; + /* 12 */ float pad_eval2; + /* 16 */ +}; +static_assert(sizeof(ColorReliefEvaluatedPropsUBO) == 16, "wrong size"); + +)"; + +template <> +struct ShaderSource { + static constexpr auto name = "ColorReliefShader"; + static constexpr auto vertexMainFunction = "vertexMain"; + static constexpr auto fragmentMainFunction = "fragmentMain"; + + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto prelude = colorReliefShaderPrelude; + static constexpr auto source = R"( + +struct VertexStage { + short2 pos [[attribute(colorReliefUBOCount + 0)]]; + short2 texture_pos [[attribute(colorReliefUBOCount + 1)]]; +}; + +struct FragmentStage { + float4 position [[position, invariant]]; + float2 pos; +}; + +FragmentStage vertex vertexMain(thread const VertexStage vertx [[stage_in]], + device const ColorReliefDrawableUBO& drawable [[buffer(idColorReliefDrawableUBO)]], + device const ColorReliefTilePropsUBO& tileProps [[buffer(idColorReliefTilePropsUBO)]]) { + + const float4 position = drawable.matrix * float4(float2(vertx.pos), 0, 1); + + // Calculate texture coordinate + float2 epsilon = 1.0 / tileProps.dimension; + float scale = (tileProps.dimension.x - 2.0) / tileProps.dimension.x; + float2 pos = (float2(vertx.pos) / 8192.0) * scale + epsilon; + + // Handle poles (use vertx.pos, not texture_pos, to match GLSL a_pos) + if (float(vertx.pos.y) < -32767.5) pos.y = 0.0; + if (float(vertx.pos.y) > 32766.5) pos.y = 1.0; + + return { + .position = position, + .pos = pos, + }; +} + +// Function to convert terrain-RGB encoded elevation value to meters +float getElevation(float2 coord, texture2d image, sampler image_sampler, float4 unpack) { + float4 data = image.sample(image_sampler, coord) * 255.0; + data.a = -1.0; + return dot(data, unpack); +} + +// Function to get the elevation value at a specific color ramp stop +float getElevationStop(int stop, int color_ramp_size, texture2d elevationStops, sampler stops_sampler) { + // Elevation stops are plain float values, read from the R channel + float x = (float(stop) + 0.5) / float(color_ramp_size); + return elevationStops.sample(stops_sampler, float2(x, 0.0)).r; +} + +// Function to get the color value at a specific color ramp stop +float4 getColorStop(int stop, int color_ramp_size, texture2d colorStops, sampler stops_sampler) { + float x = (float(stop) + 0.5) / float(color_ramp_size); + return colorStops.sample(stops_sampler, float2(x, 0.0)); +} + +half4 fragment fragmentMain(FragmentStage in [[stage_in]], + device const uint32_t& uboIndex [[buffer(idGlobalUBOIndex)]], + device const ColorReliefTilePropsUBO* tilePropsVector [[buffer(idColorReliefTilePropsUBO)]], + device const ColorReliefEvaluatedPropsUBO& props [[buffer(idColorReliefEvaluatedPropsUBO)]], + texture2d image [[texture(0)]], + texture2d elevationStops [[texture(1)]], + texture2d colorStops [[texture(2)]], + sampler image_sampler [[sampler(0)]], + sampler stops_sampler [[sampler(1)]]) { +#if defined(OVERDRAW_INSPECTOR) + return half4(1.0); +#endif + + device const ColorReliefTilePropsUBO& tileProps = tilePropsVector[uboIndex]; + + // 1. Get elevation at this pixel from DEM + float el = getElevation(in.pos, image, image_sampler, tileProps.unpack); + + // 2. Binary search to find surrounding elevation stops (l and r indices) + int r = tileProps.color_ramp_size - 1; + int l = 0; + + // Perform binary search + while (r - l > 1) { + int m = (r + l) / 2; + float el_m = getElevationStop(m, tileProps.color_ramp_size, elevationStops, stops_sampler); + + if (el < el_m) { + r = m; + } else { + l = m; + } + } + + // 3. Get the elevation values and colors at the stops + float el_l = getElevationStop(l, tileProps.color_ramp_size, elevationStops, stops_sampler); + float el_r = getElevationStop(r, tileProps.color_ramp_size, elevationStops, stops_sampler); + + float4 color_l = getColorStop(l, tileProps.color_ramp_size, colorStops, stops_sampler); + float4 color_r = getColorStop(r, tileProps.color_ramp_size, colorStops, stops_sampler); + + // 4. Interpolate color based on elevation + float t = clamp((el - el_l) / (el_r - el_l), 0.0, 1.0); + + float4 fragColor = props.opacity * mix(color_l, color_r, t); + + return half4(fragColor); +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/mtl/hillshade.hpp b/include/mbgl/shaders/mtl/hillshade.hpp index b50b34fd6e83..73d7aae07e9a 100644 --- a/include/mbgl/shaders/mtl/hillshade.hpp +++ b/include/mbgl/shaders/mtl/hillshade.hpp @@ -24,19 +24,26 @@ static_assert(sizeof(HillshadeDrawableUBO) == 4 * 16, "wrong size"); struct alignas(16) HillshadeTilePropsUBO { /* 0 */ float2 latrange; - /* 8 */ float2 light; - /* 16 */ + /* 8 */ float exaggeration; + /* 12 */ int32_t method; + /* 16 */ int32_t num_lights; + /* 20 */ float pad0; + /* 24 */ float pad1; + /* 28 */ float pad2; + /* 32 */ }; -static_assert(sizeof(HillshadeTilePropsUBO) == 16, "wrong size"); +static_assert(sizeof(HillshadeTilePropsUBO) == 2 * 16, "wrong size"); /// Evaluated properties that do not depend on the tile struct alignas(16) HillshadeEvaluatedPropsUBO { - /* 0 */ float4 highlight; - /* 16 */ float4 shadow; - /* 32 */ float4 accent; - /* 48 */ + /* 0 */ float4 accent; + /* 16 */ float4 altitudes; // Up to 4 altitude values (in radians) + /* 32 */ float4 azimuths; // Up to 4 azimuth values (in radians) + /* 48 */ float4 shadows[4]; // Shadow colors (up to 4 lights) + /* 112 */ float4 highlights[4]; // Highlight colors (up to 4 lights) + /* 176 */ }; -static_assert(sizeof(HillshadeEvaluatedPropsUBO) == 3 * 16, "wrong size"); +static_assert(sizeof(HillshadeEvaluatedPropsUBO) == 11 * 16, "wrong size"); )"; @@ -53,6 +60,13 @@ struct ShaderSource { static constexpr auto prelude = hillshadeShaderPrelude; static constexpr auto source = R"( +#define PI 3.141592653589793 +#define STANDARD 0 +#define COMBINED 1 +#define IGOR 2 +#define MULTIDIRECTIONAL 3 +#define BASIC 4 + struct VertexStage { short2 pos [[attribute(hillshadeUBOCount + 0)]]; short2 texture_pos [[attribute(hillshadeUBOCount + 1)]]; @@ -71,7 +85,7 @@ FragmentStage vertex vertexMain(thread const VertexStage vertx [[stage_in]], const float4 position = drawable.matrix * float4(float2(vertx.pos), 0, 1); float2 pos = float2(vertx.texture_pos) / 8192.0; - pos.y = 1.0 - pos.y; + pos.y = 1.0 - pos.y; // Flip Y for Metal texture coordinates return { .position = position, @@ -79,6 +93,129 @@ FragmentStage vertex vertexMain(thread const VertexStage vertx [[stage_in]], }; } +// Helper function to calculate aspect (normalized direction of slope) +float get_aspect(float2 deriv) { + return deriv.x != 0.0 ? atan2(deriv.y, -deriv.x) : PI / 2.0 * (deriv.y > 0.0 ? 1.0 : -1.0); +} + +// MapLibre's legacy hillshade algorithm (Method 0: STANDARD) +void standard_hillshade(float2 deriv, device const HillshadeTilePropsUBO& tileProps, device const HillshadeEvaluatedPropsUBO& props, thread half4& fragColor) { + float azimuth = props.azimuths.x + PI; + float slope = atan(0.625 * length(deriv)); + float aspect = get_aspect(deriv); + + float intensity = tileProps.exaggeration; + + // Scale the slope exponentially based on intensity + float base = 1.875 - intensity * 1.75; + float maxValue = 0.5 * PI; + float scaledSlope = intensity != 0.5 ? ((pow(base, slope) - 1.0) / (pow(base, maxValue) - 1.0)) * maxValue : slope; + + float accent = cos(scaledSlope); + float4 accent_color = (1.0 - accent) * props.accent * clamp(intensity * 2.0, 0.0, 1.0); + + float shade = abs(glMod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); + float4 shade_color = mix(props.shadows[0], props.highlights[0], shade) * sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0); + + fragColor = half4(accent_color * (1.0 - shade_color.a) + shade_color); +} + +// Basic directional hillshade (Method 4: BASIC) +void basic_hillshade(float2 deriv, device const HillshadeTilePropsUBO& tileProps, device const HillshadeEvaluatedPropsUBO& props, thread half4& fragColor) { + deriv = deriv * tileProps.exaggeration * 2.0; + float azimuth = props.azimuths.x + PI; + float cos_az = cos(azimuth); + float sin_az = sin(azimuth); + float cos_alt = cos(props.altitudes.x); + float sin_alt = sin(props.altitudes.x); + + // Calculate the cosine of the angle between the light vector and the surface normal + float cang = (sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / sqrt(1.0 + dot(deriv, deriv)); + float shade = clamp(cang, 0.0, 1.0); + + // Blend shadow and highlight based on intensity + if (shade > 0.5) { + fragColor = half4(props.highlights[0] * (2.0 * shade - 1.0)); + } else { + fragColor = half4(props.shadows[0] * (1.0 - 2.0 * shade)); + } +} + +// Multidirectional hillshade (Method 3: MULTIDIRECTIONAL) +void multidirectional_hillshade(float2 deriv, device const HillshadeTilePropsUBO& tileProps, device const HillshadeEvaluatedPropsUBO& props, thread half4& fragColor) { + deriv = deriv * tileProps.exaggeration * 2.0; + float4 total_color = float4(0, 0, 0, 0); + + int num_lights = min(tileProps.num_lights, 4); + + // Iterate through all light sources + for (int i = 0; i < num_lights; i++) { + // Access altitude and azimuth from vec4 UBOs + float altitude = (i == 0) ? props.altitudes.x : (i == 1) ? props.altitudes.y : (i == 2) ? props.altitudes.z : props.altitudes.w; + float azimuth = (i == 0) ? props.azimuths.x : (i == 1) ? props.azimuths.y : (i == 2) ? props.azimuths.z : props.azimuths.w; + + float cos_alt = cos(altitude); + float sin_alt = sin(altitude); + // Negate cos/sin azimuth for correct light direction + float cos_az = -cos(azimuth); + float sin_az = -sin(azimuth); + + // Calculate the cosine of the angle between the light vector and the surface normal + float cang = (sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / sqrt(1.0 + dot(deriv, deriv)); + float shade = clamp(cang, 0.0, 1.0); + + // Accumulate shadow/highlight contribution from each light + if (shade > 0.5) { + total_color += props.highlights[i] * (2.0 * shade - 1.0) / float(num_lights); + } else { + total_color += props.shadows[i] * (1.0 - 2.0 * shade) / float(num_lights); + } + } + + fragColor = half4(total_color); +} + +// Combined shadow and highlight method (Method 1: COMBINED) +void combined_hillshade(float2 deriv, device const HillshadeTilePropsUBO& tileProps, device const HillshadeEvaluatedPropsUBO& props, thread half4& fragColor) { + // Only supports one light source (index 0) + deriv = deriv * tileProps.exaggeration * 2.0; + float azimuth = props.azimuths.x + PI; + float cos_az = cos(azimuth); + float sin_az = sin(azimuth); + float cos_alt = cos(props.altitudes.x); + float sin_alt = sin(props.altitudes.x); + + // Calculate the angle between the light vector and the surface normal + float cang = acos((sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / sqrt(1.0 + dot(deriv, deriv))); + + cang = clamp(cang, 0.0, PI / 2.0); + + // Calculate shade and highlight components from angle and slope magnitude + float shade = cang * atan(length(deriv)) * 4.0 / PI / PI; + float highlight = (PI / 2.0 - cang) * atan(length(deriv)) * 4.0 / PI / PI; + + fragColor = half4(props.shadows[0] * shade + props.highlights[0] * highlight); +} + +// Igor's shadow/highlight method (Method 2: IGOR) +void igor_hillshade(float2 deriv, device const HillshadeTilePropsUBO& tileProps, device const HillshadeEvaluatedPropsUBO& props, thread half4& fragColor) { + // Only supports one light source (index 0) + deriv = deriv * tileProps.exaggeration * 2.0; + float aspect = get_aspect(deriv); + float azimuth = props.azimuths.x + PI; + + // Slope strength is magnitude of slope vector, normalized to [0, 1] + float slope_strength = atan(length(deriv)) * 2.0 / PI; + + // Aspect strength is difference between aspect and light azimuth, normalized to [0, 1] + float aspect_strength = 1.0 - abs(glMod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); + + float shadow_strength = slope_strength * aspect_strength; + float highlight_strength = slope_strength * (1.0 - aspect_strength); + + fragColor = half4(props.shadows[0] * shadow_strength + props.highlights[0] * highlight_strength); +} + half4 fragment fragmentMain(FragmentStage in [[stage_in]], device const uint32_t& uboIndex [[buffer(idGlobalUBOIndex)]], device const HillshadeTilePropsUBO* tilePropsVector [[buffer(idHillshadeTilePropsUBO)]], @@ -90,44 +227,32 @@ half4 fragment fragmentMain(FragmentStage in [[stage_in]], #endif device const HillshadeTilePropsUBO& tileProps = tilePropsVector[uboIndex]; + thread half4 fragColor; float4 pixel = image.sample(image_sampler, in.pos); - float2 deriv = ((pixel.rg * 2.0) - 1.0); - - // We divide the slope by a scale factor based on the cosin of the pixel's approximate latitude - // to account for mercator projection distortion. see #4807 for details - float scaleFactor = cos(radians((tileProps.latrange[0] - tileProps.latrange[1]) * in.pos.y + tileProps.latrange[1])); - // We also multiply the slope by an arbitrary z-factor of 1.25 - float slope = atan(1.25 * length(deriv) / scaleFactor); - float aspect = deriv.x != 0.0 ? atan2(deriv.y, -deriv.x) : M_PI_F / 2.0 * (deriv.y > 0.0 ? 1.0 : -1.0); - - float intensity = tileProps.light.x; - // We add PI to make this property match the global light object, which adds PI/2 to the light's azimuthal - // position property to account for 0deg corresponding to north/the top of the viewport in the style spec - // and the original shader was written to accept (-illuminationDirection - 90) as the azimuthal. - float azimuth = tileProps.light.y + M_PI_F; - - // We scale the slope exponentially based on intensity, using a calculation similar to - // the exponential interpolation function in the style spec: - // https://github.com/mapbox/mapbox-gl-js/blob/master/src/style-spec/expression/definitions/interpolate.js#L217-L228 - // so that higher intensity values create more opaque hillshading. - float base = 1.875 - intensity * 1.75; - float maxValue = 0.5 * M_PI_F; - float scaledSlope = intensity != 0.5 ? ((pow(base, slope) - 1.0) / (pow(base, maxValue) - 1.0)) * maxValue : slope; + // Scale the derivative based on the mercator distortion at this latitude + float scaleFactor = cos(radians((tileProps.latrange.x - tileProps.latrange.y) * (1.0 - in.pos.y) + tileProps.latrange.y)); + + // The derivative is scaled back from [0, 1] texture range to world-space slope + // Texture range [0, 1] corresponds to slope range [-4, 4] + float2 deriv = ((pixel.rg * 8.0) - 4.0) / scaleFactor; - // The accent color is calculated with the cosine of the slope while the shade color is calculated with the sine - // so that the accent color's rate of change eases in while the shade color's eases out. - float accent = cos(scaledSlope); - // We multiply both the accent and shade color by a clamped intensity value - // so that intensities >= 0.5 do not additionally affect the color values - // while intensity values < 0.5 make the overall color more transparent. - float4 accent_color = (1.0 - accent) * props.accent * clamp(intensity * 2.0, 0.0, 1.0); - float shade = abs(glMod((aspect + azimuth) / M_PI_F + 0.5, 2.0) - 1.0); - float4 shade_color = mix(props.shadow, props.highlight, shade) * sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0); - float4 color = accent_color * (1.0 - shade_color.a) + shade_color; + // Dispatch to the selected hillshade method + if (tileProps.method == STANDARD) { + standard_hillshade(deriv, tileProps, props, fragColor); + } else if (tileProps.method == COMBINED) { + combined_hillshade(deriv, tileProps, props, fragColor); + } else if (tileProps.method == IGOR) { + igor_hillshade(deriv, tileProps, props, fragColor); + } else if (tileProps.method == MULTIDIRECTIONAL) { + multidirectional_hillshade(deriv, tileProps, props, fragColor); + } else { + // Default to BASIC + basic_hillshade(deriv, tileProps, props, fragColor); + } - return half4(color); + return fragColor; } )"; }; diff --git a/include/mbgl/shaders/mtl/hillshade_prepare.hpp b/include/mbgl/shaders/mtl/hillshade_prepare.hpp index 6d7ddcf01a7e..86c996ff93bf 100644 --- a/include/mbgl/shaders/mtl/hillshade_prepare.hpp +++ b/include/mbgl/shaders/mtl/hillshade_prepare.hpp @@ -75,7 +75,7 @@ float getElevation(float2 coord, float bias, texture2d im // Convert encoded elevation value to meters float4 data = image.sample(image_sampler, coord) * 255.0; data.a = -1.0; - return dot(data, unpack) / 4.0; + return dot(data, unpack); } half4 fragment fragmentMain(FragmentStage in [[stage_in]], @@ -87,8 +87,9 @@ half4 fragment fragmentMain(FragmentStage in [[stage_in]], #endif float2 epsilon = 1.0 / tileProps.dimension; + float tileSize = tileProps.dimension.x - 2.0; - // queried pixels: + // queried pixels (using Sobel operator kernel): // +-----------+ // | | | | // | a | b | c | @@ -102,7 +103,6 @@ half4 fragment fragmentMain(FragmentStage in [[stage_in]], // | g | h | i | // | | | | // +-----------+ - float a = getElevation(in.pos + float2(-epsilon.x, -epsilon.y), 0.0, image, image_sampler, tileProps.unpack); float b = getElevation(in.pos + float2(0, -epsilon.y), 0.0, image, image_sampler, tileProps.unpack); float c = getElevation(in.pos + float2(epsilon.x, -epsilon.y), 0.0, image, image_sampler, tileProps.unpack); @@ -113,27 +113,23 @@ half4 fragment fragmentMain(FragmentStage in [[stage_in]], float h = getElevation(in.pos + float2(0, epsilon.y), 0.0, image, image_sampler, tileProps.unpack); float i = getElevation(in.pos + float2(epsilon.x, epsilon.y), 0.0, image, image_sampler, tileProps.unpack); - // here we divide the x and y slopes by 8 * pixel size - // where pixel size (aka meters/pixel) is: - // circumference of the world / (pixels per tile * number of tiles) - // which is equivalent to: 8 * 40075016.6855785 / (512 * pow(2, u_zoom)) - // which can be reduced to: pow(2, 19.25619978527 - u_zoom) - // we want to vertically exaggerate the hillshading though, because otherwise - // it is barely noticeable at low zooms. to do this, we multiply this by some - // scale factor pow(2, (u_zoom - u_maxzoom) * a) where a is an arbitrary value - // Here we use a=0.3 which works out to the expression below. see - // nickidlugash's awesome breakdown for more info - // https://github.com/mapbox/mapbox-gl-js/pull/5286#discussion_r148419556 + // Convert the raw pixel-space derivative (slope) into world-space slope. + // The conversion factor is: tileSize / (8 * meters_per_pixel). + // meters_per_pixel is calculated as pow(2.0, 28.2562 - u_zoom). + // The exaggeration factor is applied to scale the effect at lower zooms. float exaggeration = tileProps.zoom < 2.0 ? 0.4 : tileProps.zoom < 4.5 ? 0.35 : 0.3; float2 deriv = float2( (c + f + f + i) - (a + d + d + g), (g + h + h + i) - (a + b + b + c) - ) / pow(2.0, (tileProps.zoom - tileProps.maxzoom) * exaggeration + 19.2562 - tileProps.zoom); + ) * tileSize / pow(2.0, (tileProps.zoom - tileProps.maxzoom) * exaggeration + 28.2562 - tileProps.zoom); + // Encode the derivative into the color channels (r and g) + // The derivative is scaled from world-space slope to the range [0, 1] for texture storage. + // The maximum possible world-space derivative is assumed to be 4 (hence division by 8.0). float4 color = clamp(float4( - deriv.x / 2.0 + 0.5, - deriv.y / 2.0 + 0.5, + deriv.x / 8.0 + 0.5, + deriv.y / 8.0 + 0.5, 1.0, 1.0), 0.0, 1.0); diff --git a/include/mbgl/shaders/shader_defines.hpp b/include/mbgl/shaders/shader_defines.hpp index daee8ca31a6b..884c3b8057df 100644 --- a/include/mbgl/shaders/shader_defines.hpp +++ b/include/mbgl/shaders/shader_defines.hpp @@ -78,6 +78,13 @@ enum { hillshadePrepareDrawableUBOCount }; +enum { + idColorReliefDrawableUBO = idDrawableReservedVertexOnlyUBO, + idColorReliefTilePropsUBO = drawableReservedUBOCount, + idColorReliefEvaluatedPropsUBO, + colorReliefDrawableUBOCount +}; + enum { idLineDrawableUBO = idDrawableReservedVertexOnlyUBO, idLineTilePropsUBO = idDrawableReservedFragmentOnlyUBO, @@ -120,6 +127,7 @@ static constexpr uint32_t layerUBOStartId = std::max({static_cast(back static_cast(heatmapTextureDrawableUBOCount), static_cast(hillshadeDrawableUBOCount), static_cast(hillshadePrepareDrawableUBOCount), + static_cast(colorReliefDrawableUBOCount), static_cast(lineDrawableUBOCount), static_cast(locationIndicatorDrawableUBOCount), static_cast(rasterDrawableUBOCount), @@ -195,6 +203,10 @@ enum { hillshadePrepareUBOCount = getLayerStartValue(hillshadePrepareDrawableUBOCount) }; +enum { + colorReliefUBOCount = getLayerStartValue(colorReliefDrawableUBOCount) +}; + enum { idLineEvaluatedPropsUBO = getLayerStartValue(lineDrawableUBOCount), idLineExpressionUBO, @@ -233,6 +245,7 @@ static constexpr uint32_t maxUBOCountPerShader = std::max({static_cast static_cast(heatmapTextureUBOCount), static_cast(hillshadeUBOCount), static_cast(hillshadePrepareUBOCount), + static_cast(colorReliefUBOCount), static_cast(lineUBOCount), static_cast(locationIndicatorUBOCount), static_cast(rasterUBOCount), @@ -296,6 +309,13 @@ enum { hillshadeTextureCount }; +enum { + idColorReliefImageTexture, + idColorReliefElevationStopsTexture, + idColorReliefColorStopsTexture, + colorReliefTextureCount +}; + enum { idLocationIndicatorTexture, locationIndicatorTextureCount @@ -329,6 +349,7 @@ static constexpr uint32_t maxTextureCountPerShader = std::max({static_cast(fillExtrusionTextureCount), static_cast(heatmapTextureCount), static_cast(hillshadeTextureCount), + static_cast(colorReliefTextureCount), static_cast(lineTextureCount), static_cast(locationIndicatorTextureCount), static_cast(rasterTextureCount), @@ -429,6 +450,12 @@ enum { hillshadeVertexAttributeCount }; +enum { + idColorReliefPosVertexAttribute, + idColorReliefTexturePosVertexAttribute, + colorReliefVertexAttributeCount +}; + enum { idLinePosNormalVertexAttribute, idLineDataVertexAttribute, @@ -505,6 +532,7 @@ static constexpr uint32_t maxVertexAttributeCountPerShader = std::max({ static_cast(fillExtrusionVertexAttributeCount), static_cast(heatmapVertexAttributeCount), static_cast(hillshadeVertexAttributeCount), + static_cast(colorReliefVertexAttributeCount), static_cast(lineVertexAttributeCount), static_cast(locationIndicatorVertexAttributeCount), static_cast(rasterVertexAttributeCount), diff --git a/include/mbgl/shaders/shader_manifest.hpp b/include/mbgl/shaders/shader_manifest.hpp index 3ff0c80423c2..715f61a63211 100644 --- a/include/mbgl/shaders/shader_manifest.hpp +++ b/include/mbgl/shaders/shader_manifest.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include diff --git a/include/mbgl/shaders/shader_source.hpp b/include/mbgl/shaders/shader_source.hpp index 6f3e3111b80f..339f0025fff9 100644 --- a/include/mbgl/shaders/shader_source.hpp +++ b/include/mbgl/shaders/shader_source.hpp @@ -29,6 +29,7 @@ enum class BuiltIn { HeatmapShader, HeatmapTextureShader, HillshadePrepareShader, + ColorReliefShader, HillshadeShader, LineShader, LineGradientShader, diff --git a/include/mbgl/shaders/vulkan/color_relief.hpp b/include/mbgl/shaders/vulkan/color_relief.hpp new file mode 100644 index 000000000000..a4ce8a85d0c0 --- /dev/null +++ b/include/mbgl/shaders/vulkan/color_relief.hpp @@ -0,0 +1,142 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace shaders { + +constexpr auto colorReliefShaderPrelude = R"( + +#define idColorReliefDrawableUBO idDrawableReservedVertexOnlyUBO +#define idColorReliefTilePropsUBO idDrawableReservedFragmentOnlyUBO +#define idColorReliefEvaluatedPropsUBO layerUBOStartId + +)"; + +template <> +struct ShaderSource { + static constexpr const char* name = "ColorReliefShader"; + + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto prelude = colorReliefShaderPrelude; + + static constexpr auto vertex = R"( + +layout(location = 0) in ivec2 in_position; +layout(location = 1) in ivec2 in_texture_position; + +layout(set = DRAWABLE_UBO_SET_INDEX, binding = idColorReliefDrawableUBO) uniform ColorReliefDrawableUBO { + mat4 matrix; +} drawable; + +layout(set = DRAWABLE_UBO_SET_INDEX, binding = idColorReliefTilePropsUBO) uniform ColorReliefTilePropsUBO { + vec4 unpack; + vec2 dimension; + int color_ramp_size; + float pad_tile0; +} tileProps; + +layout(location = 0) out vec2 frag_position; + +void main() { + + gl_Position = drawable.matrix * vec4(in_position, 0, 1); + applySurfaceTransform(); + + highp vec2 epsilon = 1.0 / tileProps.dimension; + float scale = (tileProps.dimension.x - 2.0) / tileProps.dimension.x; + frag_position = (vec2(in_position) / 8192.0) * scale + epsilon; + + // Handle poles (use in_position to match GLSL a_pos) + if (float(in_position.y) < -32767.5) frag_position.y = 0.0; + if (float(in_position.y) > 32766.5) frag_position.y = 1.0; +} +)"; + + static constexpr auto fragment = R"( + +layout(location = 0) in vec2 frag_position; +layout(location = 0) out vec4 out_color; + +layout(set = DRAWABLE_UBO_SET_INDEX, binding = idColorReliefTilePropsUBO) uniform ColorReliefTilePropsUBO { + vec4 unpack; + vec2 dimension; + int color_ramp_size; + float pad_tile0; +} tileProps; + +layout(set = LAYER_SET_INDEX, binding = idColorReliefEvaluatedPropsUBO) uniform ColorReliefEvaluatedPropsUBO { + float opacity; + float pad_eval0; + float pad_eval1; + float pad_eval2; +} props; + +layout(set = DRAWABLE_IMAGE_SET_INDEX, binding = 0) uniform sampler2D image_sampler; +layout(set = DRAWABLE_IMAGE_SET_INDEX, binding = 1) uniform sampler2D elevation_stops_sampler; +layout(set = DRAWABLE_IMAGE_SET_INDEX, binding = 2) uniform sampler2D color_stops_sampler; + +float getElevation(vec2 coord) { + // Convert encoded elevation value to meters + vec4 data = texture(image_sampler, coord) * 255.0; + data.a = -1.0; + return dot(data, tileProps.unpack); +} + +float getElevationStop(int stop) { + // Elevation stops are plain float values, not terrain-RGB encoded + float x = (float(stop) + 0.5) / float(tileProps.color_ramp_size); + return texture(elevation_stops_sampler, vec2(x, 0.0)).r; +} + +vec4 getColorStop(int stop) { + float x = (float(stop) + 0.5) / float(tileProps.color_ramp_size); + return texture(color_stops_sampler, vec2(x, 0.0)); +} + +void main() { + +#if defined(OVERDRAW_INSPECTOR) + out_color = vec4(1.0); + return; +#endif + + float el = getElevation(frag_position); + + // Binary search to find surrounding elevation stops (l and r indices) + int r = tileProps.color_ramp_size - 1; + int l = 0; + + // Perform binary search + while (r - l > 1) { + int m = (r + l) / 2; + float el_m = getElevationStop(m); + + if (el < el_m) { + r = m; + } else { + l = m; + } + } + + // Get elevation values and colors at the stops + float el_l = getElevationStop(l); + float el_r = getElevationStop(r); + + vec4 color_l = getColorStop(l); + vec4 color_r = getColorStop(r); + + // Interpolate color based on elevation + float t = clamp((el - el_l) / (el_r - el_l), 0.0, 1.0); + + out_color = props.opacity * mix(color_l, color_r, t); +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/vulkan/hillshade.hpp b/include/mbgl/shaders/vulkan/hillshade.hpp index 9306762608df..523b12a356ea 100644 --- a/include/mbgl/shaders/vulkan/hillshade.hpp +++ b/include/mbgl/shaders/vulkan/hillshade.hpp @@ -49,12 +49,19 @@ void main() { applySurfaceTransform(); frag_position = vec2(in_texture_position) / 8192.0; - frag_position.y = 1.0 - frag_position.y; // TODO check this. prepare should ignore the flip + frag_position.y = 1.0 - frag_position.y; } )"; static constexpr auto fragment = R"( +#define PI 3.141592653589793 +#define STANDARD 0 +#define COMBINED 1 +#define IGOR 2 +#define MULTIDIRECTIONAL 3 +#define BASIC 4 + layout(location = 0) in vec2 frag_position; layout(location = 0) out vec4 out_color; @@ -64,7 +71,12 @@ layout(push_constant) uniform Constants { struct HillshadeTilePropsUBO { vec2 latrange; - vec2 light; + float exaggeration; + int method; + int num_lights; + float pad0; + float pad1; + float pad2; }; layout(std140, set = LAYER_SET_INDEX, binding = idHillshadeTilePropsUBO) readonly buffer HillshadeTilePropsUBOVector { @@ -72,13 +84,140 @@ layout(std140, set = LAYER_SET_INDEX, binding = idHillshadeTilePropsUBO) readonl } tilePropsVector; layout(set = LAYER_SET_INDEX, binding = idHillshadeEvaluatedPropsUBO) uniform HillshadeEvaluatedPropsUBO { - vec4 highlight; - vec4 shadow; vec4 accent; + vec4 altitudes; + vec4 azimuths; + vec4 shadows[4]; + vec4 highlights[4]; } props; layout(set = DRAWABLE_IMAGE_SET_INDEX, binding = 0) uniform sampler2D image_sampler; +float get_aspect(vec2 deriv) { + return deriv.x != 0.0 ? atan(deriv.y, -deriv.x) : PI / 2.0 * (deriv.y > 0.0 ? 1.0 : -1.0); +} + +// MapLibre's legacy hillshade algorithm (Method 0: STANDARD) +void standard_hillshade(vec2 deriv, const HillshadeTilePropsUBO tileProps) { + float azimuth = props.azimuths.x + PI; + float slope = atan(0.625 * length(deriv)); + float aspect = get_aspect(deriv); + + float intensity = tileProps.exaggeration; + + // Scale the slope exponentially based on intensity + float base = 1.875 - intensity * 1.75; + float maxValue = 0.5 * PI; + float scaledSlope = intensity != 0.5 ? ((pow(base, slope) - 1.0) / (pow(base, maxValue) - 1.0)) * maxValue : slope; + + float accent = cos(scaledSlope); + vec4 accent_color = (1.0 - accent) * props.accent * clamp(intensity * 2.0, 0.0, 1.0); + + float shade = abs(mod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); + vec4 shade_color = mix(props.shadows[0], props.highlights[0], shade) * sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0); + + out_color = accent_color * (1.0 - shade_color.a) + shade_color; +} + +// Basic directional hillshade (Method 4: BASIC) +void basic_hillshade(vec2 deriv, const HillshadeTilePropsUBO tileProps) { + deriv = deriv * tileProps.exaggeration * 2.0; + float azimuth = props.azimuths.x + PI; + float cos_az = cos(azimuth); + float sin_az = sin(azimuth); + float cos_alt = cos(props.altitudes.x); + float sin_alt = sin(props.altitudes.x); + + // Calculate the cosine of the angle between the light vector and the surface normal + float cang = (sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / sqrt(1.0 + dot(deriv, deriv)); + float shade = clamp(cang, 0.0, 1.0); + + // Blend shadow and highlight based on intensity + if (shade > 0.5) { + out_color = props.highlights[0] * (2.0 * shade - 1.0); + } else { + out_color = props.shadows[0] * (1.0 - 2.0 * shade); + } +} + +// Multidirectional hillshade (Method 3: MULTIDIRECTIONAL) +void multidirectional_hillshade(vec2 deriv, const HillshadeTilePropsUBO tileProps) { + deriv = deriv * tileProps.exaggeration * 2.0; + vec4 total_color = vec4(0, 0, 0, 0); + + // Access altitude and azimuth from vec4 UBOs + float altitudes[4] = float[4](props.altitudes.x, props.altitudes.y, props.altitudes.z, props.altitudes.w); + float azimuths[4] = float[4](props.azimuths.x, props.azimuths.y, props.azimuths.z, props.azimuths.w); + + int num_lights = min(tileProps.num_lights, 4); + + // Iterate through all light sources + for (int i = 0; i < num_lights; i++) { + float altitude = altitudes[i]; + float azimuth = azimuths[i]; + + float cos_alt = cos(altitude); + float sin_alt = sin(altitude); + // Negate cos/sin azimuth for correct light direction + float cos_az = -cos(azimuth); + float sin_az = -sin(azimuth); + + // Calculate the cosine of the angle between the light vector and the surface normal + float cang = (sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / sqrt(1.0 + dot(deriv, deriv)); + float shade = clamp(cang, 0.0, 1.0); + + // Accumulate shadow/highlight contribution from each light + if (shade > 0.5) { + total_color += props.highlights[i] * (2.0 * shade - 1.0) / float(num_lights); + } else { + total_color += props.shadows[i] * (1.0 - 2.0 * shade) / float(num_lights); + } + } + + out_color = total_color; +} + +// Combined shadow and highlight method (Method 1: COMBINED) +void combined_hillshade(vec2 deriv, const HillshadeTilePropsUBO tileProps) { + // Only supports one light source (index 0) + deriv = deriv * tileProps.exaggeration * 2.0; + float azimuth = props.azimuths.x + PI; + float cos_az = cos(azimuth); + float sin_az = sin(azimuth); + float cos_alt = cos(props.altitudes.x); + float sin_alt = sin(props.altitudes.x); + + // Calculate the angle between the light vector and the surface normal + float cang = acos((sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / sqrt(1.0 + dot(deriv, deriv))); + + cang = clamp(cang, 0.0, PI / 2.0); + + // Calculate shade and highlight components from angle and slope magnitude + float shade = cang * atan(length(deriv)) * 4.0 / PI / PI; + float highlight = (PI / 2.0 - cang) * atan(length(deriv)) * 4.0 / PI / PI; + + out_color = props.shadows[0] * shade + props.highlights[0] * highlight; +} + +// Igor's shadow/highlight method (Method 2: IGOR) +void igor_hillshade(vec2 deriv, const HillshadeTilePropsUBO tileProps) { + // Only supports one light source (index 0) + deriv = deriv * tileProps.exaggeration * 2.0; + float aspect = get_aspect(deriv); + float azimuth = props.azimuths.x + PI; + + // Slope strength is magnitude of slope vector, normalized to [0, 1] + float slope_strength = atan(length(deriv)) * 2.0 / PI; + + // Aspect strength is difference between aspect and light azimuth, normalized to [0, 1] + float aspect_strength = 1.0 - abs(mod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); + + float shadow_strength = slope_strength * aspect_strength; + float highlight_strength = slope_strength * (1.0 - aspect_strength); + + out_color = props.shadows[0] * shadow_strength + props.highlights[0] * highlight_strength; +} + void main() { #if defined(OVERDRAW_INSPECTOR) @@ -90,41 +229,26 @@ void main() { vec4 pixel = texture(image_sampler, frag_position); - vec2 deriv = pixel.rg * 2.0 - 1.0; - - // We divide the slope by a scale factor based on the cosin of the pixel's approximate latitude - // to account for mercator projection distortion. see #4807 for details + // Scale the derivative based on the mercator distortion at this latitude float scaleFactor = cos(radians((tileProps.latrange[0] - tileProps.latrange[1]) * frag_position.y + tileProps.latrange[1])); - // We also multiply the slope by an arbitrary z-factor of 1.25 - float slope = atan(1.25 * length(deriv) / scaleFactor); - float aspect = deriv.x != 0.0 ? atan(deriv.y, -deriv.x) : M_PI / 2.0 * (deriv.y > 0.0 ? 1.0 : -1.0); - - float intensity = tileProps.light.x; - // We add PI to make this property match the global light object, which adds PI/2 to the light's azimuthal - // position property to account for 0deg corresponding to north/the top of the viewport in the style spec - // and the original shader was written to accept (-illuminationDirection - 90) as the azimuthal. - float azimuth = tileProps.light.y + M_PI; - - // We scale the slope exponentially based on intensity, using a calculation similar to - // the exponential interpolation function in the style spec: - // https://github.com/mapbox/mapbox-gl-js/blob/master/src/style-spec/expression/definitions/interpolate.js#L217-L228 - // so that higher intensity values create more opaque hillshading. - float base = 1.875 - intensity * 1.75; - float maxValue = 0.5 * M_PI; - float scaledSlope = intensity != 0.5 ? ((pow(base, slope) - 1.0) / (pow(base, maxValue) - 1.0)) * maxValue : slope; - - // The accent color is calculated with the cosine of the slope while the shade color is calculated with the sine - // so that the accent color's rate of change eases in while the shade color's eases out. - float accent = cos(scaledSlope); - // We multiply both the accent and shade color by a clamped intensity value - // so that intensities >= 0.5 do not additionally affect the color values - // while intensity values < 0.5 make the overall color more transparent. - vec4 accent_color = (1.0 - accent) * props.accent * clamp(intensity * 2.0, 0.0, 1.0); - float shade = abs(mod((aspect + azimuth) / M_PI + 0.5, 2.0) - 1.0); - vec4 shade_color = mix(props.shadow, props.highlight, shade) * sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0); - vec4 color = accent_color * (1.0 - shade_color.a) + shade_color; - - out_color = color; + + // The derivative is scaled back from [0, 1] texture range to world-space slope + // Texture range [0, 1] corresponds to slope range [-4, 4] + vec2 deriv = ((pixel.rg * 8.0) - 4.0) / scaleFactor; + + // Dispatch to the selected hillshade method + if (tileProps.method == BASIC) { + basic_hillshade(deriv, tileProps); + } else if (tileProps.method == COMBINED) { + combined_hillshade(deriv, tileProps); + } else if (tileProps.method == IGOR) { + igor_hillshade(deriv, tileProps); + } else if (tileProps.method == MULTIDIRECTIONAL) { + multidirectional_hillshade(deriv, tileProps); + } else { + // Default to STANDARD + standard_hillshade(deriv, tileProps); + } } )"; }; diff --git a/include/mbgl/shaders/vulkan/hillshade_prepare.hpp b/include/mbgl/shaders/vulkan/hillshade_prepare.hpp index a2aa699b421f..755ffe3915ae 100644 --- a/include/mbgl/shaders/vulkan/hillshade_prepare.hpp +++ b/include/mbgl/shaders/vulkan/hillshade_prepare.hpp @@ -69,7 +69,7 @@ float getElevation(vec2 coord, float bias, sampler2D image_sampler, vec4 unpack) // Convert encoded elevation value to meters vec4 data = texture(image_sampler, coord) * 255.0; data.a = -1.0; - return dot(data, unpack) / 4.0; + return dot(data, unpack); } void main() { @@ -80,8 +80,9 @@ void main() { #endif const vec2 epsilon = 1.0 / tileProps.dimension; + const float tileSize = tileProps.dimension.x - 2.0; - // queried pixels: + // queried pixels (using Sobel operator kernel): // +-----------+ // | | | | // | a | b | c | @@ -106,27 +107,23 @@ void main() { float h = getElevation(frag_position + vec2(0, epsilon.y), 0.0, image_sampler, tileProps.unpack); float i = getElevation(frag_position + vec2(epsilon.x, epsilon.y), 0.0, image_sampler, tileProps.unpack); - // here we divide the x and y slopes by 8 * pixel size - // where pixel size (aka meters/pixel) is: - // circumference of the world / (pixels per tile * number of tiles) - // which is equivalent to: 8 * 40075016.6855785 / (512 * pow(2, u_zoom)) - // which can be reduced to: pow(2, 19.25619978527 - u_zoom) - // we want to vertically exaggerate the hillshading though, because otherwise - // it is barely noticeable at low zooms. to do this, we multiply this by some - // scale factor pow(2, (u_zoom - u_maxzoom) * a) where a is an arbitrary value - // Here we use a=0.3 which works out to the expression below. see - // nickidlugash's awesome breakdown for more info - // https://github.com/mapbox/mapbox-gl-js/pull/5286#discussion_r148419556 + // Convert the raw pixel-space derivative (slope) into world-space slope. + // The conversion factor is: tileSize / (8 * meters_per_pixel). + // meters_per_pixel is calculated as pow(2.0, 28.2562 - u_zoom). + // The exaggeration factor is applied to scale the effect at lower zooms. float exaggeration = tileProps.zoom < 2.0 ? 0.4 : tileProps.zoom < 4.5 ? 0.35 : 0.3; vec2 deriv = vec2( (c + f + f + i) - (a + d + d + g), (g + h + h + i) - (a + b + b + c) - ) / pow(2.0, (tileProps.zoom - tileProps.maxzoom) * exaggeration + 19.2562 - tileProps.zoom); + ) * tileSize / pow(2.0, (tileProps.zoom - tileProps.maxzoom) * exaggeration + 28.2562 - tileProps.zoom); + // Encode the derivative into the color channels (r and g) + // The derivative is scaled from world-space slope to the range [0, 1] for texture storage. + // The maximum possible world-space derivative is assumed to be 4 (hence division by 8.0). out_color = clamp(vec4( - deriv.x / 2.0 + 0.5, - deriv.y / 2.0 + 0.5, + deriv.x / 8.0 + 0.5, + deriv.y / 8.0 + 0.5, 1.0, 1.0), 0.0, 1.0); } diff --git a/include/mbgl/shaders/webgpu/color_relief.hpp b/include/mbgl/shaders/webgpu/color_relief.hpp new file mode 100644 index 000000000000..8089f17a6881 --- /dev/null +++ b/include/mbgl/shaders/webgpu/color_relief.hpp @@ -0,0 +1,158 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "ColorReliefShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(5) position: vec2, + @location(6) texture_pos: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) frag_position: vec2, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +struct ColorReliefDrawableUBO { + matrix: mat4x4, +}; + +struct ColorReliefTilePropsUBO { + unpack: vec4, + dimension: vec2, + color_ramp_size: i32, + pad_tile0: f32, +}; + +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(3) var tilePropsVector: array; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let drawable = drawableVector[globalIndex.value]; + let tileProps = tilePropsVector[globalIndex.value]; + + out.position = drawable.matrix * vec4(f32(in.position.x), f32(in.position.y), 0.0, 1.0); + + let a_pos = vec2(f32(in.position.x), f32(in.position.y)); + let epsilon = vec2(1.0, 1.0) / tileProps.dimension; + let scale = (tileProps.dimension.x - 2.0) / tileProps.dimension.x; + out.frag_position = (a_pos / 8192.0) * scale + epsilon; + + // Handle poles + out.frag_position.y = select(out.frag_position.y, 0.0, a_pos.y < -32767.5); + out.frag_position.y = select(out.frag_position.y, 1.0, a_pos.y > 32766.5); + + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) frag_position: vec2, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +struct ColorReliefTilePropsUBO { + unpack: vec4, + dimension: vec2, + color_ramp_size: i32, + pad_tile0: f32, +}; + +struct ColorReliefEvaluatedPropsUBO { + opacity: f32, + pad_eval0: f32, + pad_eval1: f32, + pad_eval2: f32, +}; + +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(3) var tilePropsVector: array; +@group(0) @binding(4) var props: ColorReliefEvaluatedPropsUBO; +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var dem_texture: texture_2d; +@group(1) @binding(2) var elevation_stops_texture: texture_2d; +@group(1) @binding(3) var color_stops_texture: texture_2d; + +fn getElevation(coord: vec2, unpack: vec4) -> f32 { + var data = textureSample(dem_texture, texture_sampler, coord) * 255.0; + data.a = -1.0; + return dot(data, unpack); +} + +fn getElevationStop(stop: i32, color_ramp_size: i32) -> f32 { + let x = (f32(stop) + 0.5) / f32(color_ramp_size); + return textureSample(elevation_stops_texture, texture_sampler, vec2(x, 0.0)).r; +} + +fn getColorStop(stop: i32, color_ramp_size: i32) -> vec4 { + let x = (f32(stop) + 0.5) / f32(color_ramp_size); + return textureSample(color_stops_texture, texture_sampler, vec2(x, 0.0)); +} + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { +#ifdef OVERDRAW_INSPECTOR + return vec4(1.0, 1.0, 1.0, 1.0); +#endif + + let tileProps = tilePropsVector[globalIndex.value]; + let el = getElevation(in.frag_position, tileProps.unpack); + + // Binary search to find surrounding elevation stops + var r: i32 = tileProps.color_ramp_size - 1; + var l: i32 = 0; + + loop { + if (r - l <= 1) { break; } + let m = (r + l) / 2; + let el_m = getElevationStop(m, tileProps.color_ramp_size); + if (el < el_m) { + r = m; + } else { + l = m; + } + } + + // Get elevation values and colors at the stops + let el_l = getElevationStop(l, tileProps.color_ramp_size); + let el_r = getElevationStop(r, tileProps.color_ramp_size); + + let color_l = getColorStop(l, tileProps.color_ramp_size); + let color_r = getColorStop(r, tileProps.color_ramp_size); + + // Interpolate color based on elevation + let t = clamp((el - el_l) / (el_r - el_l), 0.0, 1.0); + let final_color = mix(color_l, color_r, t); + + return vec4(final_color.rgb, final_color.a * props.opacity); +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/hillshade.hpp b/include/mbgl/shaders/webgpu/hillshade.hpp index 340dee529be3..5fbf7c97fcd4 100644 --- a/include/mbgl/shaders/webgpu/hillshade.hpp +++ b/include/mbgl/shaders/webgpu/hillshade.hpp @@ -51,6 +51,13 @@ fn main(in: VertexInput) -> VertexOutput { )"; static constexpr auto fragment = R"( +const PI: f32 = 3.141592653589793; +const STANDARD: i32 = 0; +const COMBINED: i32 = 1; +const IGOR: i32 = 2; +const MULTIDIRECTIONAL: i32 = 3; +const BASIC: i32 = 4; + struct FragmentInput { @location(0) tex_coord: vec2, }; @@ -62,13 +69,20 @@ struct GlobalIndexUBO { struct HillshadeTilePropsUBO { latrange: vec2, - light: vec2, + exaggeration: f32, + method: i32, + num_lights: i32, + pad0: f32, + pad1: f32, + pad2: f32, }; struct HillshadeEvaluatedPropsUBO { - highlight: vec4, - shadow: vec4, accent: vec4, + altitudes: vec4, + azimuths: vec4, + shadows: array, 4>, + highlights: array, 4>, }; @group(0) @binding(1) var globalIndex: GlobalIndexUBO; @@ -77,6 +91,108 @@ struct HillshadeEvaluatedPropsUBO { @group(1) @binding(0) var texture_sampler: sampler; @group(1) @binding(1) var hillshade_texture: texture_2d; +fn get_aspect(deriv: vec2) -> f32 { + let aspectDefault = 0.5 * PI * select(-1.0, 1.0, deriv.y > 0.0); + return select(aspectDefault, atan2(deriv.y, -deriv.x), deriv.x != 0.0); +} + +fn standard_hillshade(deriv: vec2, tileProps: HillshadeTilePropsUBO) -> vec4 { + let azimuth = props.azimuths.x + PI; + let slope = atan(0.625 * length(deriv)); + let aspect = get_aspect(deriv); + + let intensity = tileProps.exaggeration; + + let base = 1.875 - intensity * 1.75; + let maxValue = 0.5 * PI; + let denom = pow(base, maxValue) - 1.0; + let useLinear = abs(intensity - 0.5) < 1e-6; + let scaledSlope = select(((pow(base, slope) - 1.0) / denom) * maxValue, slope, useLinear); + + let accent = cos(scaledSlope); + let accentColor = (1.0 - accent) * props.accent * clamp(intensity * 2.0, 0.0, 1.0); + + let shade = abs(glMod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); + let shadeColor = mix(props.shadows[0], props.highlights[0], shade) * sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0); + + return accentColor * (1.0 - shadeColor.a) + shadeColor; +} + +fn basic_hillshade(deriv: vec2, tileProps: HillshadeTilePropsUBO) -> vec4 { + let scaled_deriv = deriv * tileProps.exaggeration * 2.0; + let azimuth = props.azimuths.x + PI; + let cos_az = cos(azimuth); + let sin_az = sin(azimuth); + let cos_alt = cos(props.altitudes.x); + let sin_alt = sin(props.altitudes.x); + + let cang = (sin_alt - (scaled_deriv.y * cos_az * cos_alt - scaled_deriv.x * sin_az * cos_alt)) / sqrt(1.0 + dot(scaled_deriv, scaled_deriv)); + let shade = clamp(cang, 0.0, 1.0); + + return select(props.shadows[0] * (1.0 - 2.0 * shade), + props.highlights[0] * (2.0 * shade - 1.0), + shade > 0.5); +} + +fn multidirectional_hillshade(deriv: vec2, tileProps: HillshadeTilePropsUBO) -> vec4 { + let scaled_deriv = deriv * tileProps.exaggeration * 2.0; + var total_color = vec4(0.0, 0.0, 0.0, 0.0); + + let num_lights = min(tileProps.num_lights, 4); + + for (var i: i32 = 0; i < num_lights; i = i + 1) { + let altitude = select(select(select(props.altitudes.x, props.altitudes.y, i == 1), props.altitudes.z, i == 2), props.altitudes.w, i == 3); + let azimuth = select(select(select(props.azimuths.x, props.azimuths.y, i == 1), props.azimuths.z, i == 2), props.azimuths.w, i == 3); + + let cos_alt = cos(altitude); + let sin_alt = sin(altitude); + let cos_az = -cos(azimuth); + let sin_az = -sin(azimuth); + + let cang = (sin_alt - (scaled_deriv.y * cos_az * cos_alt - scaled_deriv.x * sin_az * cos_alt)) / sqrt(1.0 + dot(scaled_deriv, scaled_deriv)); + let shade = clamp(cang, 0.0, 1.0); + + if (shade > 0.5) { + total_color = total_color + props.highlights[i] * (2.0 * shade - 1.0) / f32(num_lights); + } else { + total_color = total_color + props.shadows[i] * (1.0 - 2.0 * shade) / f32(num_lights); + } + } + + return total_color; +} + +fn combined_hillshade(deriv: vec2, tileProps: HillshadeTilePropsUBO) -> vec4 { + let scaled_deriv = deriv * tileProps.exaggeration * 2.0; + let azimuth = props.azimuths.x + PI; + let cos_az = cos(azimuth); + let sin_az = sin(azimuth); + let cos_alt = cos(props.altitudes.x); + let sin_alt = sin(props.altitudes.x); + + let cang = acos((sin_alt - (scaled_deriv.y * cos_az * cos_alt - scaled_deriv.x * sin_az * cos_alt)) / sqrt(1.0 + dot(scaled_deriv, scaled_deriv))); + let clamped_cang = clamp(cang, 0.0, PI / 2.0); + + let shade = clamped_cang * atan(length(scaled_deriv)) * 4.0 / PI / PI; + let highlight = (PI / 2.0 - clamped_cang) * atan(length(scaled_deriv)) * 4.0 / PI / PI; + + return props.shadows[0] * shade + props.highlights[0] * highlight; +} + +fn igor_hillshade(deriv: vec2, tileProps: HillshadeTilePropsUBO) -> vec4 { + let scaled_deriv = deriv * tileProps.exaggeration * 2.0; + let aspect = get_aspect(scaled_deriv); + let azimuth = props.azimuths.x + PI; + + let slope_strength = atan(length(scaled_deriv)) * 2.0 / PI; + let aspect_strength = 1.0 - abs(glMod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); + + let shadow_strength = slope_strength * aspect_strength; + let highlight_strength = slope_strength * (1.0 - aspect_strength); + + return props.shadows[0] * shadow_strength + props.highlights[0] * highlight_strength; +} + @fragment fn main(in: FragmentInput) -> @location(0) vec4 { #ifdef OVERDRAW_INSPECTOR @@ -86,34 +202,23 @@ fn main(in: FragmentInput) -> @location(0) vec4 { let tileProps = tilePropsVector[globalIndex.value]; let pixel = textureSample(hillshade_texture, texture_sampler, in.tex_coord); - let deriv = pixel.rg * 2.0 - vec2(1.0, 1.0); - let latRange = tileProps.latrange; let latitude = (latRange.x - latRange.y) * in.tex_coord.y + latRange.y; let scaleFactor = cos(radians(latitude)); - let slope = atan(1.25 * length(deriv) / scaleFactor); - let aspectDefault = 0.5 * PI * select(-1.0, 1.0, deriv.y > 0.0); - let aspect = select(aspectDefault, atan2(deriv.y, -deriv.x), deriv.x != 0.0); - - let intensity = tileProps.light.x; - let azimuth = tileProps.light.y + PI; - - let base = 1.875 - intensity * 1.75; - let maxValue = 0.5 * PI; - let denom = pow(base, maxValue) - 1.0; - let useLinear = abs(intensity - 0.5) < 1e-6; - let scaledSlope = select(((pow(base, slope) - 1.0) / denom) * maxValue, - slope, - useLinear); - - let accent = cos(scaledSlope); - let accentColor = (1.0 - accent) * props.accent * clamp(intensity * 2.0, 0.0, 1.0); - - let shade = abs(glMod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); - let shadeColor = mix(props.shadow, props.highlight, shade) * sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0); - - let color = accentColor * (1.0 - shadeColor.a) + shadeColor; - return color; + + let deriv = ((pixel.rg * 8.0) - vec2(4.0, 4.0)) / scaleFactor; + + if (tileProps.method == BASIC) { + return basic_hillshade(deriv, tileProps); + } else if (tileProps.method == COMBINED) { + return combined_hillshade(deriv, tileProps); + } else if (tileProps.method == IGOR) { + return igor_hillshade(deriv, tileProps); + } else if (tileProps.method == MULTIDIRECTIONAL) { + return multidirectional_hillshade(deriv, tileProps); + } else { + return standard_hillshade(deriv, tileProps); + } } )"; }; diff --git a/include/mbgl/shaders/webgpu/hillshade_prepare.hpp b/include/mbgl/shaders/webgpu/hillshade_prepare.hpp index a9916b887777..b732f8aea7e9 100644 --- a/include/mbgl/shaders/webgpu/hillshade_prepare.hpp +++ b/include/mbgl/shaders/webgpu/hillshade_prepare.hpp @@ -70,7 +70,7 @@ struct HillshadePrepareTilePropsUBO { fn getElevation(coord: vec2) -> f32 { var data = textureSample(dem_texture, texture_sampler, coord) * 255.0; data.a = -1.0; - return dot(data, tileProps.unpack) / 4.0; + return dot(data, tileProps.unpack); } @fragment @@ -80,6 +80,7 @@ fn main(in: FragmentInput) -> @location(0) vec4 { #endif let epsilon = vec2(1.0, 1.0) / tileProps.dimension; + let tileSize = tileProps.dimension.x - 2.0; let a = getElevation(in.tex_coord + vec2(-epsilon.x, -epsilon.y)); let b = getElevation(in.tex_coord + vec2(0.0, -epsilon.y)); @@ -98,12 +99,12 @@ fn main(in: FragmentInput) -> @location(0) vec4 { exaggeration = 0.4; } - let denom = pow(2.0, (tileProps.zoom - tileProps.maxzoom) * exaggeration + 19.2562 - tileProps.zoom); + let denom = pow(2.0, (tileProps.zoom - tileProps.maxzoom) * exaggeration + 28.2562 - tileProps.zoom); let deriv = vec2((c + f + f + i) - (a + d + d + g), - (g + h + h + i) - (a + b + b + c)) / denom; + (g + h + h + i) - (a + b + b + c)) * tileSize / denom; - let color = clamp(vec4(deriv.x / 2.0 + 0.5, - deriv.y / 2.0 + 0.5, + let color = clamp(vec4(deriv.x / 8.0 + 0.5, + deriv.y / 8.0 + 0.5, 1.0, 1.0), vec4(0.0, 0.0, 0.0, 0.0), diff --git a/include/mbgl/style/conversion/constant.hpp b/include/mbgl/style/conversion/constant.hpp index a73f6f461b62..94da87970f72 100644 --- a/include/mbgl/style/conversion/constant.hpp +++ b/include/mbgl/style/conversion/constant.hpp @@ -46,6 +46,11 @@ struct Converter { std::optional operator()(const Convertible& value, Error& error) const; }; +template <> +struct Converter> { + std::optional> operator()(const Convertible& value, Error& error) const; +}; + template <> struct Converter { std::optional operator()(const Convertible& value, Error& error) const; diff --git a/include/mbgl/style/expression/expression.hpp b/include/mbgl/style/expression/expression.hpp index 3c8616e8202f..ae993e141464 100644 --- a/include/mbgl/style/expression/expression.hpp +++ b/include/mbgl/style/expression/expression.hpp @@ -51,6 +51,15 @@ class EvaluationContext { feature(feature_), colorRampParameter(std::move(colorRampParameter_)) {} + EvaluationContext(std::optional zoom_, + GeometryTileFeature const* feature_, + std::optional colorRampParameter_, + std::optional elevation_) noexcept + : zoom(std::move(zoom_)), + feature(feature_), + colorRampParameter(std::move(colorRampParameter_)), + elevation(std::move(elevation_)) {} + EvaluationContext& withFormattedSection(const Value* formattedSection_) noexcept { formattedSection = formattedSection_; return *this; @@ -71,10 +80,16 @@ class EvaluationContext { return *this; }; + EvaluationContext& withElevation(float elevation_) noexcept { + elevation = elevation_; + return *this; + }; + std::optional zoom; std::optional accumulated; GeometryTileFeature const* feature = nullptr; std::optional colorRampParameter; + std::optional elevation; // Contains formatted section object, std::unordered_map. const Value* formattedSection = nullptr; const FeatureState* featureState = nullptr; @@ -202,7 +217,8 @@ enum class Dependency : uint32_t { Bind = 1 << 4, // Create variable binding ("let") Var = 1 << 5, // Use variable binding Override = 1 << 6, // Property override - MaskCount = 7, + Elevation = 1 << 7, // Elevation from DEM + MaskCount = 8, All = (1 << MaskCount) - 1, }; diff --git a/include/mbgl/style/layers/color_relief_layer.hpp b/include/mbgl/style/layers/color_relief_layer.hpp new file mode 100644 index 000000000000..09c5df93e64a --- /dev/null +++ b/include/mbgl/style/layers/color_relief_layer.hpp @@ -0,0 +1,59 @@ +// clang-format off + +// This file is generated. Do not edit. + +#pragma once + +#include +#include +#include +#include +#include + +namespace mbgl { +namespace style { + +class TransitionOptions; + +class ColorReliefLayer final : public Layer { +public: + ColorReliefLayer(const std::string& layerID, const std::string& sourceID); + ~ColorReliefLayer() override; + + // Paint properties + + static ColorRampPropertyValue getDefaultColorReliefColor(); + const ColorRampPropertyValue& getColorReliefColor() const; + void setColorReliefColor(const ColorRampPropertyValue&); + void setColorReliefColorTransition(const TransitionOptions&); + TransitionOptions getColorReliefColorTransition() const; + + static PropertyValue getDefaultColorReliefOpacity(); + const PropertyValue& getColorReliefOpacity() const; + void setColorReliefOpacity(const PropertyValue&); + void setColorReliefOpacityTransition(const TransitionOptions&); + TransitionOptions getColorReliefOpacityTransition() const; + + // Private implementation + + class Impl; + const Impl& impl() const; + + Mutable mutableImpl() const; + ColorReliefLayer(Immutable); + std::unique_ptr cloneRef(const std::string& id) const final; + +protected: + // Dynamic properties + std::optional setPropertyInternal(const std::string& name, const conversion::Convertible& value) final; + + StyleProperty getProperty(const std::string& name) const final; + Value serialize() const final; + + Mutable mutableBaseImpl() const final; +}; + +} // namespace style +} // namespace mbgl + +// clang-format on diff --git a/include/mbgl/style/layers/hillshade_layer.hpp b/include/mbgl/style/layers/hillshade_layer.hpp index 22034737f48c..1a1e3a8b2a2a 100644 --- a/include/mbgl/style/layers/hillshade_layer.hpp +++ b/include/mbgl/style/layers/hillshade_layer.hpp @@ -33,27 +33,39 @@ class HillshadeLayer final : public Layer { void setHillshadeExaggerationTransition(const TransitionOptions&); TransitionOptions getHillshadeExaggerationTransition() const; - static PropertyValue getDefaultHillshadeHighlightColor(); - const PropertyValue& getHillshadeHighlightColor() const; - void setHillshadeHighlightColor(const PropertyValue&); + static PropertyValue> getDefaultHillshadeHighlightColor(); + const PropertyValue>& getHillshadeHighlightColor() const; + void setHillshadeHighlightColor(const PropertyValue>&); void setHillshadeHighlightColorTransition(const TransitionOptions&); TransitionOptions getHillshadeHighlightColorTransition() const; + static PropertyValue> getDefaultHillshadeIlluminationAltitude(); + const PropertyValue>& getHillshadeIlluminationAltitude() const; + void setHillshadeIlluminationAltitude(const PropertyValue>&); + void setHillshadeIlluminationAltitudeTransition(const TransitionOptions&); + TransitionOptions getHillshadeIlluminationAltitudeTransition() const; + static PropertyValue getDefaultHillshadeIlluminationAnchor(); const PropertyValue& getHillshadeIlluminationAnchor() const; void setHillshadeIlluminationAnchor(const PropertyValue&); void setHillshadeIlluminationAnchorTransition(const TransitionOptions&); TransitionOptions getHillshadeIlluminationAnchorTransition() const; - static PropertyValue getDefaultHillshadeIlluminationDirection(); - const PropertyValue& getHillshadeIlluminationDirection() const; - void setHillshadeIlluminationDirection(const PropertyValue&); + static PropertyValue> getDefaultHillshadeIlluminationDirection(); + const PropertyValue>& getHillshadeIlluminationDirection() const; + void setHillshadeIlluminationDirection(const PropertyValue>&); void setHillshadeIlluminationDirectionTransition(const TransitionOptions&); TransitionOptions getHillshadeIlluminationDirectionTransition() const; - static PropertyValue getDefaultHillshadeShadowColor(); - const PropertyValue& getHillshadeShadowColor() const; - void setHillshadeShadowColor(const PropertyValue&); + static PropertyValue getDefaultHillshadeMethod(); + const PropertyValue& getHillshadeMethod() const; + void setHillshadeMethod(const PropertyValue&); + void setHillshadeMethodTransition(const TransitionOptions&); + TransitionOptions getHillshadeMethodTransition() const; + + static PropertyValue> getDefaultHillshadeShadowColor(); + const PropertyValue>& getHillshadeShadowColor() const; + void setHillshadeShadowColor(const PropertyValue>&); void setHillshadeShadowColorTransition(const TransitionOptions&); TransitionOptions getHillshadeShadowColorTransition() const; diff --git a/include/mbgl/style/layers/layer.hpp.ejs b/include/mbgl/style/layers/layer.hpp.ejs index 4b2232fdc34a..46d6b22c7c57 100644 --- a/include/mbgl/style/layers/layer.hpp.ejs +++ b/include/mbgl/style/layers/layer.hpp.ejs @@ -9,7 +9,7 @@ #pragma once -<% if (type === 'heatmap' || type === 'line') { -%> +<% if (type === 'heatmap' || type === 'line' || type === 'color-relief') { -%> #include <% } -%> #include diff --git a/include/mbgl/style/types.hpp b/include/mbgl/style/types.hpp index 3d5780935a89..3955514e9805 100644 --- a/include/mbgl/style/types.hpp +++ b/include/mbgl/style/types.hpp @@ -48,6 +48,14 @@ enum class HillshadeIlluminationAnchorType : bool { Viewport }; +enum class HillshadeMethodType : uint8_t { + Standard, + Combined, + Igor, + Multidirectional, + Basic +}; + enum class TranslateAnchorType : bool { Map, Viewport diff --git a/metrics/integration/render-tests/color-relief/hillshade/expected.png b/metrics/integration/render-tests/color-relief/hillshade/expected.png new file mode 100644 index 000000000000..558998762c96 Binary files /dev/null and b/metrics/integration/render-tests/color-relief/hillshade/expected.png differ diff --git a/metrics/integration/render-tests/color-relief/hillshade/style.json b/metrics/integration/render-tests/color-relief/hillshade/style.json new file mode 100644 index 000000000000..5e7e4dde7d66 --- /dev/null +++ b/metrics/integration/render-tests/color-relief/hillshade/style.json @@ -0,0 +1,59 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "sources": { + "source": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "color-relief", + "type": "color-relief", + "source": "source", + "paint": { + "color-relief-opacity": 1, + "color-relief-color": [ + "interpolate", + ["linear"], + ["elevation"], + 400, "rgb(112, 209, 255)", + 494.1176471, "rgb(113, 211, 247)", + 588.2352941, "rgb(114, 212, 234)", + 682.3529412, "rgb(117, 213, 222)", + 776.4705882, "rgb(120, 214, 209)", + 870.5882353, "rgb(124, 215, 196)", + 964.7058824, "rgb(130, 215, 183)", + 1058.823529, "rgb(138, 215, 169)", + 1152.941176, "rgb(149, 214, 155)", + 1247.058824, "rgb(163, 212, 143)", + 1341.176471, "rgb(178, 209, 134)", + 1435.294118, "rgb(193, 205, 127)", + 1529.411765, "rgb(207, 202, 121)", + 1623.529412, "rgb(220, 197, 118)", + 1717.647059, "rgb(233, 193, 118)", + 1811.764706, "rgb(244, 188, 120)", + 1905.882353, "rgb(255, 183, 124)", + 2000, "rgb(255, 178, 129)" + ] + } + }, + { + "id": "hillshade", + "type": "hillshade", + "source": "source" + } + ] +} diff --git a/metrics/integration/render-tests/color-relief/opacity/expected.png b/metrics/integration/render-tests/color-relief/opacity/expected.png new file mode 100644 index 000000000000..79d561dac09f Binary files /dev/null and b/metrics/integration/render-tests/color-relief/opacity/expected.png differ diff --git a/metrics/integration/render-tests/color-relief/opacity/style.json b/metrics/integration/render-tests/color-relief/opacity/style.json new file mode 100644 index 000000000000..b4de87cacff9 --- /dev/null +++ b/metrics/integration/render-tests/color-relief/opacity/style.json @@ -0,0 +1,43 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "sources": { + "source": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "color-relief", + "type": "color-relief", + "source": "source", + "paint": { + "color-relief-opacity": 0.5, + "color-relief-color": [ + "interpolate", + ["linear"], + ["elevation"], + 400, "#F00", + 800, "#AA0", + 1000, "#AF0", + 1200, "#0F0", + 1400, "#0AA", + 1600, "#00F", + 2000, "#C0C" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/color-relief/rainbow/expected.png b/metrics/integration/render-tests/color-relief/rainbow/expected.png new file mode 100644 index 000000000000..2f28acbba81f Binary files /dev/null and b/metrics/integration/render-tests/color-relief/rainbow/expected.png differ diff --git a/metrics/integration/render-tests/color-relief/rainbow/style.json b/metrics/integration/render-tests/color-relief/rainbow/style.json new file mode 100644 index 000000000000..f9b8cf2adcee --- /dev/null +++ b/metrics/integration/render-tests/color-relief/rainbow/style.json @@ -0,0 +1,43 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "sources": { + "source": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "color-relief", + "type": "color-relief", + "source": "source", + "paint": { + "color-relief-opacity": 1, + "color-relief-color": [ + "interpolate", + ["linear"], + ["elevation"], + 400, "#F00", + 800, "#AA0", + 1000, "#AF0", + 1200, "#0F0", + 1400, "#0AA", + 1600, "#00F", + 2000, "#C0C" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/color-relief/transparency/expected.png b/metrics/integration/render-tests/color-relief/transparency/expected.png new file mode 100644 index 000000000000..c8aa0bc916c2 Binary files /dev/null and b/metrics/integration/render-tests/color-relief/transparency/expected.png differ diff --git a/metrics/integration/render-tests/color-relief/transparency/style.json b/metrics/integration/render-tests/color-relief/transparency/style.json new file mode 100644 index 000000000000..24dd5b1e62e9 --- /dev/null +++ b/metrics/integration/render-tests/color-relief/transparency/style.json @@ -0,0 +1,43 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "sources": { + "source": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "color-relief", + "type": "color-relief", + "source": "source", + "paint": { + "color-relief-opacity": 1, + "color-relief-color": [ + "interpolate", + ["linear"], + ["elevation"], + 400, "#F00C", + 800, "#AA0A", + 1000, "#AF09", + 1200, "#0F08", + 1400, "#0AA7", + 1600, "#00F6", + 2000, "#C0C4" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/combinations/background-opaque--hillshade-translucent/expected-ubuntu-22.png b/metrics/integration/render-tests/combinations/background-opaque--hillshade-translucent/expected-ubuntu-22.png new file mode 100644 index 000000000000..44f43698b591 Binary files /dev/null and b/metrics/integration/render-tests/combinations/background-opaque--hillshade-translucent/expected-ubuntu-22.png differ diff --git a/metrics/integration/render-tests/combinations/fill-opaque--hillshade-translucent/expected-ubuntu-22.png b/metrics/integration/render-tests/combinations/fill-opaque--hillshade-translucent/expected-ubuntu-22.png new file mode 100644 index 000000000000..44f43698b591 Binary files /dev/null and b/metrics/integration/render-tests/combinations/fill-opaque--hillshade-translucent/expected-ubuntu-22.png differ diff --git a/metrics/integration/render-tests/combinations/hillshade-translucent--fill-opaque/expected-ubuntu-22.png b/metrics/integration/render-tests/combinations/hillshade-translucent--fill-opaque/expected-ubuntu-22.png new file mode 100644 index 000000000000..1b8463f5e8a3 Binary files /dev/null and b/metrics/integration/render-tests/combinations/hillshade-translucent--fill-opaque/expected-ubuntu-22.png differ diff --git a/metrics/integration/render-tests/combinations/hillshade-translucent--fill-opaque/expected.png b/metrics/integration/render-tests/combinations/hillshade-translucent--fill-opaque/expected.png index 987ac0bdda68..9d13779ca850 100644 Binary files a/metrics/integration/render-tests/combinations/hillshade-translucent--fill-opaque/expected.png and b/metrics/integration/render-tests/combinations/hillshade-translucent--fill-opaque/expected.png differ diff --git a/metrics/integration/render-tests/combinations/hillshade-translucent--fill-translucent/expected-ubuntu-22.png b/metrics/integration/render-tests/combinations/hillshade-translucent--fill-translucent/expected-ubuntu-22.png new file mode 100644 index 000000000000..3432c1633f21 Binary files /dev/null and b/metrics/integration/render-tests/combinations/hillshade-translucent--fill-translucent/expected-ubuntu-22.png differ diff --git a/metrics/integration/render-tests/hillshade-alt/basic-0/expected.png b/metrics/integration/render-tests/hillshade-alt/basic-0/expected.png new file mode 100644 index 000000000000..30893b753be0 Binary files /dev/null and b/metrics/integration/render-tests/hillshade-alt/basic-0/expected.png differ diff --git a/metrics/integration/render-tests/hillshade-alt/basic-0/style.json b/metrics/integration/render-tests/hillshade-alt/basic-0/style.json new file mode 100644 index 000000000000..cd86fa555208 --- /dev/null +++ b/metrics/integration/render-tests/hillshade-alt/basic-0/style.json @@ -0,0 +1,39 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "sources": { + "source": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "gray" + } + }, + { + "id": "hillshade", + "type": "hillshade", + "source": "source", + "paint": { + "hillshade-method": "basic", + "hillshade-illumination-altitude": 0 + } + } + ] +} diff --git a/metrics/integration/render-tests/hillshade-alt/basic-90/expected.png b/metrics/integration/render-tests/hillshade-alt/basic-90/expected.png new file mode 100644 index 000000000000..2ff03d7c955f Binary files /dev/null and b/metrics/integration/render-tests/hillshade-alt/basic-90/expected.png differ diff --git a/metrics/integration/render-tests/hillshade-alt/basic-90/style.json b/metrics/integration/render-tests/hillshade-alt/basic-90/style.json new file mode 100644 index 000000000000..cfff20b4f8a2 --- /dev/null +++ b/metrics/integration/render-tests/hillshade-alt/basic-90/style.json @@ -0,0 +1,39 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "sources": { + "source": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "gray" + } + }, + { + "id": "hillshade", + "type": "hillshade", + "source": "source", + "paint": { + "hillshade-method": "basic", + "hillshade-illumination-altitude": 90 + } + } + ] +} diff --git a/metrics/integration/render-tests/hillshade-alt/combined-60/expected.png b/metrics/integration/render-tests/hillshade-alt/combined-60/expected.png new file mode 100644 index 000000000000..4073d6c8a220 Binary files /dev/null and b/metrics/integration/render-tests/hillshade-alt/combined-60/expected.png differ diff --git a/metrics/integration/render-tests/hillshade-alt/combined-60/style.json b/metrics/integration/render-tests/hillshade-alt/combined-60/style.json new file mode 100644 index 000000000000..9a3fc90817d6 --- /dev/null +++ b/metrics/integration/render-tests/hillshade-alt/combined-60/style.json @@ -0,0 +1,39 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "sources": { + "source": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "gray" + } + }, + { + "id": "hillshade", + "type": "hillshade", + "source": "source", + "paint": { + "hillshade-method": "combined", + "hillshade-illumination-altitude": 60 + } + } + ] +} diff --git a/metrics/integration/render-tests/hillshade-maxzoom/default/expected.png b/metrics/integration/render-tests/hillshade-maxzoom/default/expected.png new file mode 100644 index 000000000000..28a557c6bd50 Binary files /dev/null and b/metrics/integration/render-tests/hillshade-maxzoom/default/expected.png differ diff --git a/metrics/integration/render-tests/hillshade-maxzoom/default/style.json b/metrics/integration/render-tests/hillshade-maxzoom/default/style.json new file mode 100644 index 000000000000..5a1f13ac7d8b --- /dev/null +++ b/metrics/integration/render-tests/hillshade-maxzoom/default/style.json @@ -0,0 +1,36 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "description": "This test verifies that setting a lower maxzoom on the raster-dem source produces the same output as a higher maxzoom (hillshade-accent-color/default) when the map zoom level is lower than the maxzoom (i.e., maxzoom does not alter rendering at lower zooms)" + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "sources": { + "source": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "hillshade", + "type": "hillshade", + "source": "source" + } + ] +} diff --git a/metrics/integration/render-tests/hillshade-maxzoom/overzoom/expected.png b/metrics/integration/render-tests/hillshade-maxzoom/overzoom/expected.png new file mode 100644 index 000000000000..3144a09e4494 Binary files /dev/null and b/metrics/integration/render-tests/hillshade-maxzoom/overzoom/expected.png differ diff --git a/metrics/integration/render-tests/hillshade-maxzoom/overzoom/style.json b/metrics/integration/render-tests/hillshade-maxzoom/overzoom/style.json new file mode 100644 index 000000000000..5991188457fc --- /dev/null +++ b/metrics/integration/render-tests/hillshade-maxzoom/overzoom/style.json @@ -0,0 +1,36 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "description": "This test verifies that setting a maximum zoom on the raster-dem source lower than the layer results in overzooming the source." + } + }, + "center": [-113.26903, 35.9654], + "zoom": 13, + "sources": { + "source": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "hillshade", + "type": "hillshade", + "source": "source" + } + ] +} diff --git a/metrics/integration/render-tests/hillshade-methods/basic/expected.png b/metrics/integration/render-tests/hillshade-methods/basic/expected.png new file mode 100644 index 000000000000..f626de6a51d9 Binary files /dev/null and b/metrics/integration/render-tests/hillshade-methods/basic/expected.png differ diff --git a/metrics/integration/render-tests/hillshade-methods/basic/style.json b/metrics/integration/render-tests/hillshade-methods/basic/style.json new file mode 100644 index 000000000000..08512da40150 --- /dev/null +++ b/metrics/integration/render-tests/hillshade-methods/basic/style.json @@ -0,0 +1,38 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "sources": { + "source": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "gray" + } + }, + { + "id": "hillshade", + "type": "hillshade", + "source": "source", + "paint": { + "hillshade-method": "basic" + } + } + ] +} diff --git a/metrics/integration/render-tests/hillshade-methods/combined/expected.png b/metrics/integration/render-tests/hillshade-methods/combined/expected.png new file mode 100644 index 000000000000..b5f19d63e7f6 Binary files /dev/null and b/metrics/integration/render-tests/hillshade-methods/combined/expected.png differ diff --git a/metrics/integration/render-tests/hillshade-methods/combined/style.json b/metrics/integration/render-tests/hillshade-methods/combined/style.json new file mode 100644 index 000000000000..c2e2eaa60f9a --- /dev/null +++ b/metrics/integration/render-tests/hillshade-methods/combined/style.json @@ -0,0 +1,38 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "sources": { + "source": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "gray" + } + }, + { + "id": "hillshade", + "type": "hillshade", + "source": "source", + "paint": { + "hillshade-method": "combined" + } + } + ] +} diff --git a/metrics/integration/render-tests/hillshade-methods/igor/expected.png b/metrics/integration/render-tests/hillshade-methods/igor/expected.png new file mode 100644 index 000000000000..7ed9e5c798d9 Binary files /dev/null and b/metrics/integration/render-tests/hillshade-methods/igor/expected.png differ diff --git a/metrics/integration/render-tests/hillshade-methods/igor/style.json b/metrics/integration/render-tests/hillshade-methods/igor/style.json new file mode 100644 index 000000000000..e3c4f1fac700 --- /dev/null +++ b/metrics/integration/render-tests/hillshade-methods/igor/style.json @@ -0,0 +1,38 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "sources": { + "source": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "gray" + } + }, + { + "id": "hillshade", + "type": "hillshade", + "source": "source", + "paint": { + "hillshade-method": "igor" + } + } + ] +} diff --git a/metrics/integration/render-tests/hillshade-methods/multidirectional/expected.png b/metrics/integration/render-tests/hillshade-methods/multidirectional/expected.png new file mode 100644 index 000000000000..d2ef9a3e66ad Binary files /dev/null and b/metrics/integration/render-tests/hillshade-methods/multidirectional/expected.png differ diff --git a/metrics/integration/render-tests/hillshade-methods/multidirectional/style.json b/metrics/integration/render-tests/hillshade-methods/multidirectional/style.json new file mode 100644 index 000000000000..18553477dcbf --- /dev/null +++ b/metrics/integration/render-tests/hillshade-methods/multidirectional/style.json @@ -0,0 +1,41 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "sources": { + "source": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "gray" + } + }, + { + "id": "hillshade", + "type": "hillshade", + "source": "source", + "paint": { + "hillshade-method": "multidirectional", + "hillshade-highlight-color": ["#FF4000", "#FFFF00", "#40ff00", "#00FF80"], + "hillshade-shadow-color": ["#00bfff", "#0000ff", "#bf00ff", "#FF0080"], + "hillshade-illumination-direction": [270,315,0,45] + } + } + ] +} diff --git a/package-lock.json b/package-lock.json index ed621e3dc426..0756e7316c7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "BSD-2-Clause", "dependencies": { "@actions/core": "^1.11.1", - "@maplibre/maplibre-gl-style-spec": "^20.3.1", + "@maplibre/maplibre-gl-style-spec": "^24.3.1", "minimatch": "^9.0.4", "npm-run-all": "^4.1.5", "simple-git": "^3.25.0" @@ -1157,18 +1157,17 @@ } }, "node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "20.3.1", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.3.1.tgz", - "integrity": "sha512-5ueL4UDitzVtceQ8J4kY+Px3WK+eZTsmGwha3MBKHKqiHvKrjWWwBCIl1K8BuJSc5OFh83uI8IFNoFvQxX2uUw==", + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.3.1.tgz", + "integrity": "sha512-TUM5JD40H2mgtVXl5IwWz03BuQabw8oZQLJTmPpJA0YTYF+B+oZppy5lNMO6bMvHzB+/5mxqW9VLG3wFdeqtOw==", "license": "ISC", "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", - "quickselect": "^2.0.0", + "quickselect": "^3.0.0", "rw": "^1.3.3", - "sort-object": "^3.0.3", "tinyqueue": "^3.0.0" }, "bin": { @@ -2106,15 +2105,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", @@ -2157,15 +2147,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", @@ -2248,25 +2229,6 @@ "node": ">= 0.8" } }, - "node_modules/bytewise": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz", - "integrity": "sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==", - "license": "MIT", - "dependencies": { - "bytewise-core": "^1.2.2", - "typewise": "^1.0.3" - } - }, - "node_modules/bytewise-core": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bytewise-core/-/bytewise-core-1.2.3.tgz", - "integrity": "sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==", - "license": "MIT", - "dependencies": { - "typewise-core": "^1.2" - } - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -2747,18 +2709,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fast-xml-parser": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", @@ -2915,15 +2865,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/glob": { "version": "5.0.15", "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", @@ -3243,15 +3184,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -3277,18 +3209,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -3376,15 +3296,6 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/jake": { "version": "10.8.7", "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", @@ -3926,9 +3837,9 @@ } }, "node_modules/quickselect": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", - "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", "license": "ISC" }, "node_modules/range-parser": { @@ -4171,21 +4082,6 @@ "node": ">= 0.4" } }, - "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -4275,41 +4171,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "license": "MIT" }, - "node_modules/sort-asc": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz", - "integrity": "sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sort-desc": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.2.0.tgz", - "integrity": "sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sort-object": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-3.0.3.tgz", - "integrity": "sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==", - "license": "MIT", - "dependencies": { - "bytewise": "^1.1.0", - "get-value": "^2.0.2", - "is-extendable": "^0.1.1", - "sort-asc": "^0.2.0", - "sort-desc": "^0.2.0", - "union-value": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -4338,43 +4199,6 @@ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==" }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -4612,21 +4436,6 @@ "node": ">=14.17" } }, - "node_modules/typewise": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz", - "integrity": "sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==", - "license": "MIT", - "dependencies": { - "typewise-core": "^1.2.0" - } - }, - "node_modules/typewise-core": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz", - "integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==", - "license": "MIT" - }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -4660,21 +4469,6 @@ "dev": true, "license": "MIT" }, - "node_modules/union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/universal-user-agent": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", diff --git a/package.json b/package.json index eb097bb489be..500f241c5d00 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "license": "BSD-2-Clause", "dependencies": { "@actions/core": "^1.11.1", - "@maplibre/maplibre-gl-style-spec": "^20.3.1", + "@maplibre/maplibre-gl-style-spec": "^24.3.1", "minimatch": "^9.0.4", "npm-run-all": "^4.1.5", "simple-git": "^3.25.0" diff --git a/platform/android/scripts/generate-style-code.mjs b/platform/android/scripts/generate-style-code.mjs index 53b65babb732..401da28ef724 100644 --- a/platform/android/scripts/generate-style-code.mjs +++ b/platform/android/scripts/generate-style-code.mjs @@ -119,6 +119,10 @@ global.propertyType = function propertyType(property) { return 'String'; case 'color': return 'String'; + case 'numberArray': + return 'Float[]'; + case 'colorArray': + return 'String[]'; case 'padding': return 'Float[]'; case 'variableAnchorOffsetCollection': @@ -145,6 +149,10 @@ global.propertyJavaType = function propertyType(property) { return 'String'; case 'color': return 'String'; + case 'numberArray': + return 'float[]'; + case 'colorArray': + return 'String[]'; case 'array': return `${propertyJavaType({type:property.value})}[]`; default: @@ -164,6 +172,10 @@ global.propertyJNIType = function propertyJNIType(property) { return 'String'; case 'color': return 'String'; + case 'numberArray': + return 'jarray'; + case 'colorArray': + return 'jarray'; case 'array': return `jarray<${propertyType({type:property.value})}[]>`; default: @@ -197,6 +209,10 @@ global.propertyNativeType = function (property) { return `${camelize(property.name)}Type`; case 'color': return `Color`; + case 'numberArray': + return 'std::vector'; + case 'colorArray': + return 'std::vector'; case 'padding': return 'Padding'; case 'variableAnchorOffsetCollection': @@ -237,6 +253,10 @@ global.defaultExpressionJava = function(property) { return "string"; case 'color': return 'toColor'; + case 'numberArray': + return 'array'; + case 'colorArray': + return 'array'; case 'padding': return 'toPadding'; case 'array': @@ -271,6 +291,10 @@ global.defaultValueJava = function(property) { return snakeCaseUpper(property.name) + "_" + snakeCaseUpper(Object.keys(property.values)[0]); case 'color': return '"rgba(255,128,0,0.7)"'; + case 'numberArray': + return 'new Float[] {1.0f, 2.0f}'; + case 'colorArray': + return 'new String[] {"rgba(255,0,0,1)", "rgba(0,0,255,1)"}'; case 'padding': return '{2.0f, 2.0f, 2.0f, 2.0f}'; case 'variableAnchorOffsetCollection': @@ -409,6 +433,10 @@ global.evaluatedType = function (property) { return (isLightProperty(property) ? 'Light' : '') + `${camelize(property.name)}Type`; case 'color': return `Color`; + case 'numberArray': + return 'std::vector'; + case 'colorArray': + return 'std::vector'; case 'array': if (property.length) { return `std::array<${evaluatedType({type: property.value})}, ${property.length}>`; diff --git a/platform/darwin/scripts/generate-style-code.mjs b/platform/darwin/scripts/generate-style-code.mjs index 82c6bc4e50e0..98f0f5ec5f76 100644 --- a/platform/darwin/scripts/generate-style-code.mjs +++ b/platform/darwin/scripts/generate-style-code.mjs @@ -206,6 +206,10 @@ global.objCTestValue = function (property, layerType, arraysAsStructs, indent) { return `@"'${_.last(_.keys(property.values))}'"`; case 'color': return '@"%@", [MLNColor redColor]'; + case 'numberArray': + return '@"{1, 2}"'; + case 'colorArray': + return '@"%@", @[[MLNColor redColor], [MLNColor blueColor]]'; case 'padding': return paddingTestValue(); case 'variableAnchorOffsetCollection': @@ -271,6 +275,10 @@ global.mbglTestValue = function (property, layerType) { } case 'color': return '{ 1, 0, 0, 1 }'; + case 'numberArray': + return '{1, 2}'; + case 'colorArray': + return '{ { 1, 0, 0, 1 }, { 0, 0, 1, 1 } }'; case 'padding': return '{ 1, 1, 1, 1 }'; case 'array': @@ -348,6 +356,10 @@ global.testHelperMessage = function (property, layerType, isFunction) { return `testEnum${fnSuffix}:${objCEnum} type:@encode(${objCType})`; case 'color': return 'testColor' + fnSuffix; + case 'numberArray': + return 'testNumberArray' + fnSuffix; + case 'colorArray': + return 'testColorArray' + fnSuffix; case 'padding': return 'testPaddingType' + fnSuffix; case 'array': @@ -532,6 +544,10 @@ global.describeType = function (property) { return '`MLN' + camelize(property.name) + '`'; case 'color': return '`UIColor`'; + case 'numberArray': + return 'numeric array'; + case 'colorArray': + return '`UIColor` array'; case 'padding': return '`UIEdgeInsets`'; case 'array': @@ -630,6 +646,14 @@ global.describeValue = function (value, property, layerType) { } return 'a `UIColor`' + ` object whose RGB value is ${formatNumber(color.r)}, ${formatNumber(color.g)}, ${formatNumber(color.b)} and whose alpha value is ${formatNumber(color.a)}`; + case 'numberArray': + if (Array.isArray(value)) { + return 'an array containing the numeric values `' + value.map(formatNumber).join('`, `') + '`'; + } + return 'an array of numeric values'; + case 'colorArray': + return 'an array of `UIColor` objects'; + case 'padding': return describePadding(); @@ -690,6 +714,10 @@ global.propertyType = function (property) { return 'NSValue *'; case 'color': return 'MLNColor *'; + case 'numberArray': + return 'NSArray *'; + case 'colorArray': + return 'NSArray *'; case 'padding': return 'NSValue *'; case 'array': @@ -742,6 +770,10 @@ global.valueTransformerArguments = function (property) { return [mbglType(property), 'NSValue *', mbglType(property), `MLN${camelize(property.name)}`]; case 'color': return ['mbgl::Color', objCType]; + case 'numberArray': + return ['std::vector', objCType, 'float']; + case 'colorArray': + return ['std::vector', objCType, 'mbgl::Color']; case 'padding': return ['mbgl::Padding', objCType]; case 'variableAnchorOffsetCollection': @@ -801,6 +833,10 @@ global.mbglType = function(property) { } case 'color': return 'mbgl::Color'; + case 'numberArray': + return 'std::vector'; + case 'colorArray': + return 'std::vector'; case 'padding': return 'mbgl::Padding'; case 'variableAnchorOffsetCollection': @@ -896,7 +932,7 @@ const layers = _(spec.layer.type.values).map((value, layerType) => { }).sortBy(['type']).value(); function duplicatePlatformDecls(src) { - // Look for a documentation comment that contains “MLNColor” or “UIColor” + // Look for a documentation comment that contains "MLNColor" or "UIColor" // and the subsequent function, method, or property declaration. Try not to // match greedily. return src.replace(/(\/\*\*(?:\*[^\/]|[^*])*?\b(?:MLN|NS|UI)Color\b[\s\S]*?\*\/)(\s*.+?;)/g, diff --git a/platform/default/src/mbgl/layermanager/layer_manager.cpp b/platform/default/src/mbgl/layermanager/layer_manager.cpp index 988c105eba48..7949c169fe1c 100644 --- a/platform/default/src/mbgl/layermanager/layer_manager.cpp +++ b/platform/default/src/mbgl/layermanager/layer_manager.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -69,6 +70,9 @@ LayerManagerDefault::LayerManagerDefault() { #if !defined(MBGL_LAYER_HILLSHADE_DISABLE_ALL) addLayerType(std::make_unique()); #endif +#if !defined(MBGL_LAYER_COLOR_RELIEF_DISABLE_ALL) + addLayerType(std::make_unique()); +#endif #if !defined(MBGL_LAYER_FILL_EXTRUSION_DISABLE_ALL) addLayerType(std::make_unique()); #endif diff --git a/platform/node/src/node_conversion.hpp b/platform/node/src/node_conversion.hpp index 267b5baec006..358303d6f0cd 100644 --- a/platform/node/src/node_conversion.hpp +++ b/platform/node/src/node_conversion.hpp @@ -121,6 +121,18 @@ class ConversionTraits> { return {Nan::To(value).ToChecked()}; } else if (value->IsNumber()) { return {Nan::To(value).ToChecked()}; + } else if (value->IsArray()) { + v8::Local array = value.As(); + std::vector result; + result.reserve(array->Length()); + for (uint32_t i = 0; i < array->Length(); ++i) { + v8::Local item = Nan::Get(array, i).ToLocalChecked(); + auto converted = toValue(item); + if (converted) { + result.push_back(*converted); + } + } + return {result}; } else { return {}; } diff --git a/platform/node/src/node_expression.cpp b/platform/node/src/node_expression.cpp index b2ab3e8d16bb..15269c68218d 100644 --- a/platform/node/src/node_expression.cpp +++ b/platform/node/src/node_expression.cpp @@ -51,7 +51,9 @@ type::Type parseType(v8::Local type) { {"formatted", type::Formatted}, {"number-format", type::String}, {"resolvedImage", type::Image}, - {"variableAnchorOffsetCollection", type::VariableAnchorOffsetCollection}}; + {"variableAnchorOffsetCollection", type::VariableAnchorOffsetCollection}, + {"numberArray", type::Array(type::Number)}, + {"colorArray", type::Array(type::Color)}}; v8::Local v8kind = Nan::Get(type, Nan::New("kind").ToLocalChecked()).ToLocalChecked(); std::string kind(*v8::String::Utf8Value(v8::Isolate::GetCurrent(), v8kind)); diff --git a/scripts/generate-style-code.mjs b/scripts/generate-style-code.mjs index 9b96cd39e047..1f1eca7d4814 100644 --- a/scripts/generate-style-code.mjs +++ b/scripts/generate-style-code.mjs @@ -53,6 +53,10 @@ function expressionType(property) { case 'number': case 'enum': return 'NumberType'; + case 'numberArray': + return 'Array'; + case 'colorArray': + return 'Array'; case 'image': return 'ImageType'; case 'string': @@ -104,6 +108,10 @@ function evaluatedType(property) { return 'Rotation'; } return /location$/.test(property.name) ? 'double' : 'float'; + case 'numberArray': + return 'std::vector'; + case 'colorArray': + return 'std::vector'; case 'resolvedImage': return 'expression::Image'; case 'formatted': @@ -210,7 +218,7 @@ function propertyValueType(property) { * @returns {string} */ function formatNumber(property, num = 0) { - if (evaluatedType(property) === "float") { + if (evaluatedType(property) === "float" || property.type === "number") { const str = num.toString(); return str + (str.includes(".") ? "" : ".") + "f"; } @@ -237,6 +245,23 @@ function defaultValue(property) { switch (property.type) { case 'number': return formatNumber(property, property.default); + case 'numberArray': + // Default is a single number, wrap it in a vector + return `{${formatNumber({type: 'number'}, property.default)}}`; + case 'colorArray': + // Default is a single color string, parse it and wrap in a vector + const colorArray = parseCSSColor(property.default); + const colorStr = colorArray.join(', '); + switch (colorStr) { + case '0, 0, 0, 0': + return '{{}}'; + case '0, 0, 0, 1': + return '{Color::black()}'; + case '1, 1, 1, 1': + return '{Color::white()}'; + default: + return `{{Color{${colorStr}}}}`; + } case 'formatted': case 'string': case 'resolvedImage': diff --git a/scripts/style-spec-reference/v8.json b/scripts/style-spec-reference/v8.json index eb3867f32b8b..cf458b8160f3 100644 --- a/scripts/style-spec-reference/v8.json +++ b/scripts/style-spec-reference/v8.json @@ -34,6 +34,18 @@ 40.7736 ] }, + "centerAltitude": { + "type": "number", + "doc": "Default map center altitude in meters above sea level. The style center altitude defines the altitude where the camera is looking at and will be used only if the map has not been positioned by other means (e.g. map options or user interaction).", + "example": 123.4, + "sdk-support": { + "basic functionality": { + "js": "5.0.0", + "android": "https://github.com/maplibre/maplibre-native/issues/2980", + "ios": "https://github.com/maplibre/maplibre-native/issues/2980" + } + } + }, "zoom": { "type": "number", "doc": "Default zoom level. The style zoom will be used only if the map has not been positioned by other means (e.g. map options or user interaction).", @@ -52,7 +64,58 @@ "default": 0, "units": "degrees", "doc": "Default pitch, in degrees. Zero is perpendicular to the surface, for a look straight down at the map, while a greater value like 60 looks ahead towards the horizon. The style pitch will be used only if the map has not been positioned by other means (e.g. map options or user interaction).", - "example": 50 + "example": 50, + "sdk-support": { + "0-60 degrees": { + "js": "0.8.0", + "android": "1.0.0", + "ios": "1.0.0" + }, + "0-85 degrees": { + "js": "2.0.0", + "android": "https://github.com/maplibre/maplibre-native/issues/1909", + "ios": "https://github.com/maplibre/maplibre-native/issues/1909" + }, + "0-180 degrees": { + "js": "5.0.0", + "android": "https://github.com/maplibre/maplibre-native/issues/1909", + "ios": "https://github.com/maplibre/maplibre-native/issues/1909" + } + } + }, + "roll": { + "type": "number", + "default": 0, + "units": "degrees", + "doc": "Default roll, in degrees. The roll angle is measured counterclockwise about the camera boresight. The style roll will be used only if the map has not been positioned by other means (e.g. map options or user interaction).", + "example": 45, + "sdk-support": { + "basic functionality": { + "js": "5.0.0", + "android": "https://github.com/maplibre/maplibre-native/issues/2941", + "ios": "https://github.com/maplibre/maplibre-native/issues/2941" + } + } + }, + "state": { + "type": "state", + "default": {}, + "doc": "An object used to define default values when using the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression.", + "example": { + "chargerType": { + "default": ["CCS", "CHAdeMO", "Type2"] + }, + "minPreferredChargingSpeed": { + "default": 50 + } + }, + "sdk-support": { + "basic functionality": { + "js": "5.6.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" + } + } }, "light": { "type": "light", @@ -65,7 +128,7 @@ }, "sky": { "type": "sky", - "doc": "The map's sky configuration. **Note:** this definition is still experimental and is under develoment in maplibre-gl-js.", + "doc": "The map's sky configuration. **Note:** this definition is still experimental and is under development in maplibre-gl-js.", "example": { "sky-color": "#199EF3", "sky-horizon-blend": 0.5, @@ -84,9 +147,15 @@ }, "projection": { "type": "projection", - "doc": "The projection configuration. **Note:** this definition is still experimental and is under development in maplibre-gl-js.", + "doc": "The projection configuration", "example": { - "type": "globe" + "type": [ + "interpolate", + ["linear"], + ["zoom"], + 10, "vertical-perspective", + 12, "mercator" + ] } }, "terrain": { @@ -134,8 +203,51 @@ }, "glyphs": { "type": "string", - "doc": "A URL template for loading signed-distance-field glyph sets in PBF format. \nThe URL must include:\n\n - `{fontstack}` - When requesting glyphs, this token is replaced with a comma separated list of fonts from a font stack specified in the text-font property of a symbol layer. \n\n - `{range}` - When requesting glyphs, this token is replaced with a range of 256 Unicode code points. For example, to load glyphs for the Unicode Basic Latin and Basic Latin-1 Supplement blocks, the range would be 0-255. The actual ranges that are loaded are determined at runtime based on what text needs to be displayed.\n\nThis property is required if any layer uses the `text-field` layout property. The URL must be absolute, containing the [scheme, authority and path components](https://en.wikipedia.org/wiki/URL#Syntax).", - "example": "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf" + "doc": "A URL template for loading signed-distance-field glyph sets in PBF format.\n\nIf this property is set, any text in the `text-field` layout property is displayed in the font stack named by the `text-font` layout property based on glyphs located at the URL specified by this property. Otherwise, font faces will be determined by the `text-font` property based on the local environment.\n\nThe URL must include:\n\n - `{fontstack}` - When requesting glyphs, this token is replaced with a comma separated list of fonts from a font stack specified in the `text-font` property of a symbol layer. \n\n - `{range}` - When requesting glyphs, this token is replaced with a range of 256 Unicode code points. For example, to load glyphs for the Unicode Basic Latin and Basic Latin-1 Supplement blocks, the range would be 0-255. The actual ranges that are loaded are determined at runtime based on what text needs to be displayed.\n\nThe URL must be absolute, containing the [scheme, authority and path components](https://en.wikipedia.org/wiki/URL#Syntax).", + "example": "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf", + "sdk-support": { + "basic functionality": { + "js": "0.0.16", + "android": "0.1.1", + "ios": "0.1.0" + }, + "omit to use local fonts": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/3302", + "android": "https://github.com/maplibre/maplibre-native/issues/165", + "ios": "https://github.com/maplibre/maplibre-native/issues/165" + } + } + }, + "font-faces": { + "type": "array", + "value": "fontFaces", + "doc": "The `font-faces` property can be used to specify what font files to use for rendering text. Font faces contain information needed to render complex texts such as [Devanagari](https://en.wikipedia.org/wiki/Devanagari), [Khmer](https://en.wikipedia.org/wiki/Khmer_script) among many others.

Unicode range

The optional `unicode-range` property can be used to only use a particular font file for characters within the specified unicode range(s). Its value should be an array of strings, each indicating a start and end of a unicode range, similar to the [CSS descriptor with the same name](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range). This allows specifying multiple non-consecutive unicode ranges. When not specified, the default value is `U+0-10FFFF`, meaning the font file will be used for all unicode characters.\n\nRefer to the [Unicode Character Code Charts](https://www.unicode.org/charts/) to see ranges for scripts supported by Unicode. To see what unicode code-points are available in a font, use a tool like [FontDrop](https://fontdrop.info/).\n\n

Font Resolution

For every name in a symbol layer’s [`text-font`](./layers.md/#text-font) array, characters are matched if they are covered one of the by the font files in the corresponding entry of the `font-faces` map. Any still-unmatched characters then fall back to the [`glyphs`](./glyphs.md) URL if provided.\n\n

Supported Fonts

What type of fonts are supported is implementation-defined. Unsupported fonts are ignored.", + "example": { + "Noto Sans Regular": [{ + "url": "https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansKhmer/hinted/ttf/NotoSansKhmer-Regular.ttf", + "unicode-range": ["U+1780-17FF"] + }, + { + "url": "https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansDevanagari/hinted/ttf/NotoSansDevanagari-Regular.ttf", + "unicode-range": ["U+0900-097F"] + }, + { + "url": "https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansMyanmar/hinted/ttf/NotoSansMyanmar-Regular.ttf", + "unicode-range": ["U+1000-109F"] + }, + { + "url": "https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansEthiopic/hinted/ttf/NotoSansEthiopic-Regular.ttf", + "unicode-range": ["U+1200-137F"] + }], + "Unifont": "https://ftp.gnu.org/gnu/unifont/unifont-15.0.01/unifont-15.0.01.ttf" + }, + "sdk-support": { + "basic functionality": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/50", + "android": "11.13.0", + "ios": "6.18.0" + } + } }, "transition": { "type": "transition", @@ -149,12 +261,12 @@ "required": true, "type": "array", "value": "layer", - "doc": "A style's `layers` property lists all the layers available in that style. The type of layer is specified by the `type` property, and must be one of `background`, `fill`, `line`, `symbol`, `raster`, `circle`, `fill-extrusion`, `heatmap`, `hillshade`.\n\nExcept for layers of the `background` type, each layer needs to refer to a source. Layers take the data that they get from a source, optionally filter features, and then define how those features are styled.", + "doc": "A style's `layers` property lists all the layers available in that style. The type of layer is specified by the `type` property, and must be one of `background`, `fill`, `line`, `symbol`, `raster`, `circle`, `fill-extrusion`, `heatmap`, `hillshade`, `color-relief`.\n\nExcept for layers of the `background` type, each layer needs to refer to a source. Layers take the data that they get from a source, optionally filter features, and then define how those features are styled.", "example": [ { "id": "coastline", "source": "maplibre", - "source-layer": "contries", + "source-layer": "countries", "type": "line", "paint": { "line-color": "#198EC8" @@ -247,7 +359,33 @@ "sdk-support": { "basic functionality": { "android": "9.3.0", - "ios": "5.10.0" + "ios": "5.10.0", + "js": "wontfix" + } + } + }, + "encoding": { + "type": "enum", + "values": { + "mvt": { + "doc": "Mapbox Vector Tiles. See http://github.com/mapbox/vector-tile-spec for more info." + }, + "mlt": { + "doc": "MapLibre Vector Tiles. See https://github.com/maplibre/maplibre-tile-spec for more info." + } + }, + "default": "mvt", + "doc": "The encoding used by this source. Mapbox Vector Tiles encoding is used by default.", + "sdk-support": { + "mvt": { + "android": "supported", + "ios": "supported", + "js": "supported" + }, + "mlt": { + "android": "https://github.com/maplibre/maplibre-native/issues/3721", + "ios": "https://github.com/maplibre/maplibre-native/issues/3721", + "js": "https://github.com/maplibre/maplibre-gl-js/issues/6258" } } }, @@ -328,7 +466,8 @@ "sdk-support": { "basic functionality": { "android": "9.3.0", - "ios": "5.10.0" + "ios": "5.10.0", + "js": "wontfix" } } }, @@ -403,7 +542,19 @@ } }, "default": "mapbox", - "doc": "The encoding used by this source. Mapbox Terrain RGB is used by default." + "doc": "The encoding used by this source. Mapbox Terrain RGB is used by default.", + "sdk-support": { + "mapbox, terrarium": { + "js": "0.43.0", + "ios": "6.0.0", + "android": "6.0.0" + }, + "custom": { + "js": "3.4.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2783", + "android": "https://github.com/maplibre/maplibre-native/issues/2783" + } + } }, "redFactor": { "type": "number", @@ -411,7 +562,9 @@ "doc": "Value that will be multiplied by the red channel value when decoding. Only used on custom encodings.", "sdk-support": { "basic functionality": { - "js": "3.4" + "js": "3.4.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2783", + "android": "https://github.com/maplibre/maplibre-native/issues/2783" } } }, @@ -421,7 +574,9 @@ "doc": "Value that will be multiplied by the blue channel value when decoding. Only used on custom encodings.", "sdk-support": { "basic functionality": { - "js": "3.4" + "js": "3.4.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2783", + "android": "https://github.com/maplibre/maplibre-native/issues/2783" } } }, @@ -431,7 +586,9 @@ "doc": "Value that will be multiplied by the green channel value when decoding. Only used on custom encodings.", "sdk-support": { "basic functionality": { - "js": "3.4" + "js": "3.4.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2358", + "android": "https://github.com/maplibre/maplibre-native/issues/2783" } } }, @@ -441,7 +598,9 @@ "doc": "Value that will be added to the encoding mix when decoding. Only used on custom encodings.", "sdk-support": { "basic functionality": { - "js": "3.4" + "js": "3.4.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2783", + "android": "https://github.com/maplibre/maplibre-native/issues/2783" } } }, @@ -452,7 +611,8 @@ "sdk-support": { "basic functionality": { "android": "9.3.0", - "ios": "5.10.0" + "ios": "5.10.0", + "js": "wontfix" } } }, @@ -505,7 +665,7 @@ "cluster": { "type": "boolean", "default": false, - "doc": "If the data is a collection of point features, setting this to true clusters the points by radius into groups. Cluster groups become new `Point` features in the source with additional properties:\n * `cluster` Is `true` if the point is a cluster \n * `cluster_id` A unqiue id for the cluster to be used in conjunction with the [cluster inspection methods](https://maplibre.org/maplibre-gl-js/docs/API/classes/GeoJSONSource/#getclusterexpansionzoom)\n * `point_count` Number of original points grouped into this cluster\n * `point_count_abbreviated` An abbreviated point count" + "doc": "If the data is a collection of point features, setting this to true clusters the points by radius into groups. Cluster groups become new `Point` features in the source with additional properties:\n\n * `cluster` Is `true` if the point is a cluster \n\n * `cluster_id` A unique id for the cluster to be used in conjunction with the [cluster inspection methods](https://maplibre.org/maplibre-gl-js/docs/API/classes/GeoJSONSource/#getclusterexpansionzoom)\n\n * `point_count` Number of original points grouped into this cluster\n\n * `point_count_abbreviated` An abbreviated point count" }, "clusterRadius": { "type": "number", @@ -523,7 +683,7 @@ }, "clusterProperties": { "type": "*", - "doc": "An object defining custom properties on the generated clusters if clustering is enabled, aggregating values from clustered points. Has the form `{\"property_name\": [operator, map_expression]}`. `operator` is any expression function that accepts at least 2 operands (e.g. `\"+\"` or `\"max\"`) — it accumulates the property value from clusters/points the cluster contains; `map_expression` produces the value of a single point.\n\nExample: `{\"sum\": [\"+\", [\"get\", \"scalerank\"]]}`.\n\nFor more advanced use cases, in place of `operator`, you can use a custom reduce expression that references a special `[\"accumulated\"]` value, e.g.:\n`{\"sum\": [[\"+\", [\"accumulated\"], [\"get\", \"sum\"]], [\"get\", \"scalerank\"]]}`" + "doc": "An object defining custom properties on the generated clusters if clustering is enabled, aggregating values from clustered points. Has the form `{\"property_name\": [operator, map_expression]}`. `operator` is any expression function that accepts at least 2 operands (e.g. `\"+\"` or `\"max\"`) — it accumulates the property value from clusters/points the cluster contains; `map_expression` produces the value of a single point.\n\nExample: `{\"sum\": [\"+\", [\"get\", \"scalerank\"]]}`.\n\nFor more advanced use cases, in place of `operator`, you can use a custom reduce expression that references a special `[\"accumulated\"]` value, e.g.:\n\n`{\"sum\": [[\"+\", [\"accumulated\"], [\"get\", \"sum\"]], [\"get\", \"scalerank\"]]}`" }, "lineMetrics": { "type": "boolean", @@ -685,6 +845,21 @@ "js": "0.43.0", "android": "6.0.0", "ios": "4.0.0" + }, + "additional methods": { + "js": "5.5.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3396", + "ios": "https://github.com/maplibre/maplibre-native/issues/3396" + } + } + }, + "color-relief": { + "doc": "Client-side elevation coloring based on DEM data. The implementation supports Mapbox Terrain RGB, Mapzen Terrarium tiles and custom encodings.", + "sdk-support": { + "basic functionality": { + "js": "5.6.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3408", + "ios": "https://github.com/maplibre/maplibre-native/issues/3408" } } }, @@ -751,6 +926,7 @@ "layout_symbol", "layout_raster", "layout_hillshade", + "layout_color-relief", "layout_background" ], "layout_background": { @@ -1532,7 +1708,9 @@ "ios": "2.0.0" }, "data-driven styling": { - "js": "2.2.0" + "js": "2.2.0", + "android": "https://github.com/maplibre/maplibre-native/issues/2754", + "ios": "https://github.com/maplibre/maplibre-native/issues/2754" } }, "expression": { @@ -1769,7 +1947,9 @@ "ios": "3.4.0" }, "`viewport-glyph` value": { - "js": "2.1.8" + "js": "2.1.8", + "android": "https://github.com/maplibre/maplibre-native/issues/250", + "ios": "https://github.com/maplibre/maplibre-native/issues/250" } }, "expression": { @@ -1813,7 +1993,7 @@ "Open Sans Regular", "Arial Unicode MS Regular" ], - "doc": "Font stack to use for displaying text.", + "doc": "Fonts to use for displaying text. If the `glyphs` root property is specified, this array is joined together and interpreted as a font stack name. Otherwise, it is interpreted as a cascading fallback list of local font names.", "requires": [ "text-field" ], @@ -1827,6 +2007,11 @@ "js": "0.43.0", "android": "6.0.0", "ios": "4.0.0" + }, + "local fonts": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/3302", + "android": "https://github.com/maplibre/maplibre-native/issues/165", + "ios": "https://github.com/maplibre/maplibre-native/issues/165" } }, "expression": { @@ -2092,12 +2277,12 @@ ] } ], - "doc": "To increase the chance of placing high-priority labels on the map, you can provide an array of `text-anchor` locations, each paired with an offset value. The renderer will attempt to place the label at each location, in order, before moving on to the next location+offset. Use `text-justify: auto` to choose justification based on anchor position. \n\n The length of the array must be even, and must alternate between enum and point entries. i.e., each anchor location must be accompanied by a point, and that point defines the offset when the corresponding anchor location is used. Positive offset values indicate right and down, while negative values indicate left and up. Anchor locations may repeat, allowing the renderer to try multiple offsets to try and place a label using the same anchor. \n\n When present, this property takes precedence over `text-anchor`, `text-variable-anchor`, `text-offset`, and `text-radial-offset`. \n\n ```json \n { \"text-variable-anchor-offset\": [\"top\", [0, 4], \"left\", [3,0], \"bottom\", [1, 1]] } \n ``` \n\n When the renderer chooses the `top` anchor, `[0, 4]` will be used for `text-offset`; the text will be shifted down by 4 ems. \n\n When the renderer chooses the `left` anchor, `[3, 0]` will be used for `text-offset`; the text will be shifted right by 3 ems.", + "doc": "To increase the chance of placing high-priority labels on the map, you can provide an array of `text-anchor` locations, each paired with an offset value. The renderer will attempt to place the label at each location, in order, before moving on to the next location+offset. Use `text-justify: auto` to choose justification based on anchor position. \n\n The length of the array must be even, and must alternate between enum and point entries. i.e., each anchor location must be accompanied by a point, and that point defines the offset when the corresponding anchor location is used. Positive offset values indicate right and down, while negative values indicate left and up. Anchor locations may repeat, allowing the renderer to try multiple offsets to try and place a label using the same anchor. \n\n When present, this property takes precedence over `text-anchor`, `text-variable-anchor`, `text-offset`, and `text-radial-offset`. \n\n ```json \n\n { \"text-variable-anchor-offset\": [\"top\", [0, 4], \"left\", [3,0], \"bottom\", [1, 1]] } \n\n ``` \n\n When the renderer chooses the `top` anchor, `[0, 4]` will be used for `text-offset`; the text will be shifted down by 4 ems. \n\n When the renderer chooses the `left` anchor, `[3, 0]` will be used for `text-offset`; the text will be shifted right by 3 ems.", "sdk-support": { "basic functionality": { "js": "3.3.0", - "ios": "https://github.com/maplibre/maplibre-native/issues/2358", - "android": "https://github.com/maplibre/maplibre-native/issues/2358" + "ios": "6.8.0", + "android": "11.6.0" }, "data-driven styling": { "js": "3.3.0", @@ -2569,6 +2754,26 @@ "property-type": "constant" } }, + "layout_color-relief": { + "visibility": { + "type": "enum", + "values": { + "visible": { + "doc": "The layer is shown." + }, + "none": { + "doc": "The layer is not shown." + } + }, + "default": "visible", + "doc": "Whether this layer is displayed.", + "sdk-support": { + "basic functionality": { + } + }, + "property-type": "constant" + } + }, "filter": { "type": "array", "value": "*", @@ -2693,7 +2898,7 @@ "default": { "type": "*", "required": false, - "doc": "A value to serve as a fallback function result when a value isn't otherwise available. It is used in the following circumstances:\n* In categorical functions, when the feature value does not match any of the stop domain values.\n* In property and zoom-and-property functions, when a feature does not contain a value for the specified property.\n* In identity functions, when the feature value is not valid for the style property (for example, if the function is being used for a `circle-color` property but the feature property value is not a string or not a valid color).\n* In interval or exponential property and zoom-and-property functions, when the feature value is not numeric.\nIf no default is provided, the style property's default is used in these circumstances." + "doc": "A value to serve as a fallback function result when a value isn't otherwise available. It is used in the following circumstances:\n\n* In categorical functions, when the feature value does not match any of the stop domain values.\n\n* In property and zoom-and-property functions, when a feature does not contain a value for the specified property.\n\n* In identity functions, when the feature value is not valid for the style property (for example, if the function is being used for a `circle-color` property but the feature property value is not a string or not a valid color).\n\n* In interval or exponential property and zoom-and-property functions, when the feature value is not numeric.\n\nIf no default is provided, the style property's default is used in these circumstances." } }, "function_stop": { @@ -2719,13 +2924,32 @@ "values": { "let": { "doc": "Binds expressions to named variables, which can then be referenced in the result expression using `[\"var\", \"variable_name\"]`.\n\n - [Visualize population density](https://maplibre.org/maplibre-gl-js/docs/examples/visualize-population-density/)", - "example": { - "syntax": { - "method": ["string", "value", "expression"], - "result": "value" - }, - "value": ["let", "someNumber", 500, ["interpolate", ["linear"], ["var", "someNumber"], 274, "#edf8e9", 1551, "#006d2c"]] + "syntax": { + "overloads": [ + { + "parameters": ["var_1_name", "var_1_value", "...", "var_n_name", "var_n_value", "expression"], + "output-type": "any" + } + ], + "parameters": [ + { + "name": "var_i_name", + "type": "string literal", + "description": "The name of the i-th variable." + }, + { + "name": "var_i_value", + "type": "any", + "description": "The value of the i-th variable." + }, + { + "name": "expression", + "type": "any", + "description": "The expression within which the named variables can be referenced." + } + ] }, + "example": ["let", "someNumber", 500, ["interpolate", ["linear"], ["var", "someNumber"], 274, "#edf8e9", 1551, "#006d2c"]], "group": "Variable binding", "sdk-support": { "basic functionality": { @@ -2737,13 +2961,22 @@ }, "var": { "doc": "References variable bound using `let`.\n\n - [Visualize population density](https://maplibre.org/maplibre-gl-js/docs/examples/visualize-population-density/)", - "example": { - "syntax": { - "method": ["string"], - "result": "value" - }, - "value": ["var", "density"] + "syntax": { + "overloads": [ + { + "parameters": ["var_name"], + "output-type": "any" + } + ], + "parameters": [ + { + "name": "var_name", + "type": "string literal", + "description": "The name of the variable bound using `let`." + } + ] }, + "example": ["var", "density"], "group": "Variable binding", "sdk-support": { "basic functionality": { @@ -2755,13 +2988,29 @@ }, "literal": { "doc": "Provides a literal array or object value.\n\n - [Display and style rich text labels](https://maplibre.org/maplibre-gl-js/docs/examples/display-and-style-rich-text-labels/)", - "example": { - "syntax": { - "method": ["JSON object or array"], - "result": "array | object" - }, - "value": ["literal",["DIN Offc Pro Italic", "Arial Unicode MS Regular"]] + "syntax": { + "overloads": [ + { + "parameters": ["json_object"], + "output-type": "object" + }, + { + "parameters": ["json_array"], + "output-type": "array" + } + ], + "parameters": [ + { + "name": "json_object", + "type": "JSON object" + }, + { + "name": "json_array", + "type": "JSON array" + } + ] }, + "example": ["literal", ["DIN Offc Pro Italic", "Arial Unicode MS Regular"]], "group": "Types", "sdk-support": { "basic functionality": { @@ -2772,14 +3021,40 @@ } }, "array": { - "doc": "Asserts that the input is an array (optionally with a specific item type and length). If, when the input expression is evaluated, it is not of the asserted type, then this assertion will cause the whole expression to be aborted.", - "example": { - "syntax": { - "method": ["value", "string?", "number?"], - "result": "array" - }, - "value": ["array", ["literal", ["a", "b", "c"]], "string", 3] + "doc": "Asserts that the input is an array (optionally with a specific item type and length). If, when the input expression is evaluated, it is not of the asserted type or length, then this assertion will cause the whole expression to be aborted.", + "syntax": { + "overloads": [ + { + "parameters": ["value"], + "output-type": "array" + }, + { + "parameters": ["type", "value"], + "output-type": "array" + }, + { + "parameters": ["type", "length", "value"], + "output-type": "array" + } + ], + "parameters": [ + { + "name": "value", + "type": "any" + }, + { + "name": "type", + "type": "\"string\" | \"number\" | \"boolean\"", + "description": "The asserted type of the input array." + }, + { + "name": "length", + "type": "number literal", + "description": "The asserted length of the input array." + } + ] }, + "example": ["array", "string", 3, ["literal", ["a", "b", "c"]]], "group": "Types", "sdk-support": { "basic functionality": { @@ -2791,13 +3066,27 @@ }, "at": { "doc": "Retrieves an item from an array.", - "example": { - "syntax": { - "method": ["value", "number"], - "result": "value" - }, - "value": ["at", ["literal", ["a", "b", "c"]], 1] + "syntax": { + "overloads": [ + { + "parameters": ["index", "array"], + "output-type": "T" + } + ], + "parameters": [ + { + "name": "index", + "type": "number", + "description": "The index into `array`." + }, + { + "name": "array", + "type": "array", + "description": "The array of items to retrieve the specified item from." + } + ] }, + "example": ["at", 1, ["literal", ["a", "b", "c"]]], "group": "Lookup", "sdk-support": { "basic functionality": { @@ -2808,14 +3097,42 @@ } }, "in": { - "doc": "Determines whether an item exists in an array or a substring exists in a string.\n\n - [Measure distances](https://maplibre.org/maplibre-gl-js/docs/examples/measure/)", - "example": { - "syntax": { - "method": ["value", "value"], - "result": "boolean" - }, - "value": ["in", "$type", "Point"] + "doc": "Determines whether an item exists in an array or a substring exists in a string.\n\n - [Measure distances](https://maplibre.org/maplibre-gl-js/docs/examples/measure-distances/)", + "syntax": { + "overloads": [ + { + "parameters": ["item", "array"], + "output-type": "boolean" + }, + { + "parameters": ["substring", "string"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "item", + "type": "T", + "description": "The needle to search for within `array`." + }, + { + "name": "array", + "type": "array", + "description": "The haystack through which to search for `item`." + }, + { + "name": "substring", + "type": "string", + "description": "The needle to search for within `string`." + }, + { + "name": "string", + "type": "string", + "description": "The haystack through which to search for `substring`." + } + ] }, + "example": ["in", "$type", "Point"], "group": "Lookup", "sdk-support": { "basic functionality": { @@ -2827,13 +3144,46 @@ }, "index-of": { "doc": "Returns the first position at which an item can be found in an array or a substring can be found in a string, or `-1` if the input cannot be found. Accepts an optional index from where to begin the search. In a string, a UTF-16 surrogate pair counts as a single position.", - "example": { - "syntax": { - "method": ["value", "value", "number?"], - "result": "number" - }, - "value": ["index-of", "foo", ["baz", "bar", "hello", "foo", "world"]] + "syntax": { + "overloads": [ + { + "parameters": ["item", "array", "from_index?"], + "output-type": "number" + }, + { + "parameters": ["substring", "string", "from_index?"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "item", + "type": "T", + "description": "The needle to search for within `array`." + }, + { + "name": "array", + "type": "array", + "description": "The haystack through which to search for `item`." + }, + { + "name": "substring", + "type": "string", + "description": "The needle to search for within `string`." + }, + { + "name": "string", + "type": "string", + "description": "The haystack through which to search for `substring`." + }, + { + "name": "from_index", + "type": "number", + "description": "The index from where to begin the search." + } + ] }, + "example": ["index-of", "foo", ["baz", "bar", "hello", "foo", "world"]], "group": "Lookup", "sdk-support": { "basic functionality": { @@ -2844,14 +3194,42 @@ } }, "slice": { - "doc": "Returns an item from an array or a substring from a string from a specified start index, or between a start index and an end index if set. The return value is inclusive of the start index but not of the end index. In a string, a UTF-16 surrogate pair counts as a single position.", - "example": { - "syntax": { - "method": ["value", "number", "number?"], - "result": "value" - }, - "value": ["slice", ["get", "name"], 0, 3] + "doc": "Returns a subarray from an array or a substring from a string from a specified start index, or between a start index and an end index if set. The return value is inclusive of the start index but not of the end index. In a string, a UTF-16 surrogate pair counts as a single position.", + "syntax": { + "overloads": [ + { + "parameters": ["array", "start_index", "end_index?"], + "output-type": "array" + }, + { + "parameters": ["string", "start_index", "end_index?"], + "output-type": "string" + } + ], + "parameters": [ + { + "name": "array", + "type": "array", + "description": "The original array from which to extract the subarray." + }, + { + "name": "string", + "type": "string", + "description": "The original string from which to extract the substring." + }, + { + "name": "start_index", + "type": "number", + "description": "The inclusive index from which `slice` extracts items or characters from the subarray or substring." + }, + { + "name": "end_index", + "type": "number", + "description": "The non-inclusive index up to which `slice` extracts items or characters from the subarray or substring." + } + ] }, + "example": ["slice", ["get", "name"], 0, 3], "group": "Lookup", "sdk-support": { "basic functionality": { @@ -2862,14 +3240,31 @@ } }, "case": { - "doc": "Selects the first output whose corresponding test condition evaluates to true, or the fallback value otherwise.\n\n - [Create a hover effect](https://maplibre.org/maplibre-gl-js/docs/examples/hover-styles/)\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/cluster-html/)", - "example": { - "syntax": { - "method": ["value", "value", "...", "fallback: value"], - "result": "value" - }, - "value": ["case", ["boolean", ["feature-state", "hover"], false], 1, 0.5 ] + "doc": "Selects the first output whose corresponding test condition evaluates to true, or the fallback value otherwise.\n\n - [Create a hover effect](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-hover-effect/)\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/display-html-clusters-with-custom-properties/)", + "syntax": { + "overloads": [ + { + "parameters": ["condition_1", "output_1", "...", "condition_n", "output_n", "fallback"], + "output-type": "any" + } + ], + "parameters": [ + { + "name": "condition_i", + "type": "boolean" + }, + { + "name": "output_i", + "type": "any" + }, + { + "name": "fallback", + "type": "any", + "description": "The result when no condition evaluates to true." + } + ] }, + "example": ["case", ["boolean", ["feature-state", "hover"], false], 1, 0.5], "group": "Decision", "sdk-support": { "basic functionality": { @@ -2880,14 +3275,38 @@ } }, "match": { - "doc": "Selects the output whose label value matches the input value, or the fallback value if no match is found. The input can be any expression (e.g. `[\"get\", \"building_type\"]`). Each label must be either:\n - a single literal value; or\n - an array of literal values, whose values must be all strings or all numbers (e.g. `[100, 101]` or `[\"c\", \"b\"]`). The input matches if any of the values in the array matches, similar to the `\"in\"` operator.\nEach label must be unique. If the input type does not match the type of the labels, the result will be the fallback value.", - "example": { - "syntax": { - "method": ["value", "value", "...", "fallback: value"], - "result": "value" - }, - "value": ["match", ["get", "building_type"], "residential", "#f00", "commercial", "#0f0", "#000"] + "doc": "Selects the output whose label value matches the input value, or the fallback value if no match is found. The input can be any expression (e.g. `[\"get\", \"building_type\"]`). Each label must be either:\n\n - a single literal value; or\n\n - an array of literal values, whose values must be all strings or all numbers (e.g. `[100, 101]` or `[\"c\", \"b\"]`). The input matches if any of the values in the array matches, similar to the `\"in\"` operator.\n\nEach label must be unique. If the input type does not match the type of the labels, the result will be the fallback value.", + "syntax": { + "overloads": [ + { + "parameters": ["input", "label_1", "output_1", "...", "label_n", "output_n", "fallback"], + "output-type": "any" + } + ], + "parameters": [ + { + "name": "input", + "type": "string | number", + "description": "Any expression." + }, + { + "name": "label_i", + "type": "string literal | number literal | array | array", + "description": "The i-th literal value or array of literal values to match the input against." + }, + { + "name": "output_i", + "type": "any", + "description": "The result when the i-th label is the first label to match the input." + }, + { + "name": "fallback", + "type": "any", + "description": "The result when no label matches the input." + } + ] }, + "example": ["match", ["get", "building_type"], "residential", "#f00", "commercial", "#0f0", "#000"], "group": "Decision", "sdk-support": { "basic functionality": { @@ -2898,14 +3317,22 @@ } }, "coalesce": { - "doc": "Evaluates each expression in turn until the first non-null value is obtained, and returns that value.\n\n - [Use a fallback image](https://maplibre.org/maplibre-gl-js/docs/examples/fallback-image/)", - "example": { - "syntax": { - "method": ["coalesce", "value", "fallback"], - "result": "value" - }, - "value": ["coalesce", ["image", ["concat", ["get", "icon"], "_15"]], ["image", "marker_15"] ] + "doc": "Evaluates each expression in turn until the first non-null value is obtained, and returns that value.\n\n - [Use a fallback image](https://maplibre.org/maplibre-gl-js/docs/examples/use-a-fallback-image/)", + "syntax": { + "overloads": [ + { + "parameters": ["expression_1", "...", "expression_n"], + "output-type": "any" + } + ], + "parameters": [ + { + "name": "expression_i", + "type": "any" + } + ] }, + "example": ["coalesce", ["image", ["concat", ["get", "icon"], "_15"]], ["image", "marker_15"]], "group": "Decision", "sdk-support": { "basic functionality": { @@ -2916,14 +3343,38 @@ } }, "step": { - "doc": "Produces discrete, stepped results by evaluating a piecewise-constant function defined by pairs of input and output values (\"stops\"). The `input` may be any numeric expression (e.g., `[\"get\", \"population\"]`). Stop inputs must be numeric literals in strictly ascending order. Returns the output value of the stop just less than the input, or the first output if the input is less than the first stop.\n\n - [Create and style clusters](https://maplibre.org/maplibre-gl-js/docs/examples/cluster/)", - "example": { - "syntax": { - "method": ["step", "number", "number", "number", "..."], - "result": "number" - }, - "value": [ "step", ["get", "point_count"], 20, 100, 30, 750, 40] + "doc": "Produces discrete, stepped results by evaluating a piecewise-constant function defined by pairs of input and output values (\"stops\"). The `input` may be any numeric expression (e.g., `[\"get\", \"population\"]`). Stop inputs must be numeric literals in strictly ascending order.\n\nReturns the output value of the stop just less than the input, or the first output if the input is less than the first stop.\n\n - [Create and style clusters](https://maplibre.org/maplibre-gl-js/docs/examples/create-and-style-clusters/)", + "syntax": { + "overloads": [ + { + "parameters": ["input", "output_0", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], + "output-type": "any" + } + ], + "parameters": [ + { + "name": "input", + "type": "number", + "description": "Any numeric expression." + }, + { + "name": "output_0", + "type": "any", + "description": "The result when the `input` is less than the first stop." + }, + { + "name": "stop_i_input", + "type": "number literal", + "description": "The value of the i-th stop against which the `input` is compared." + }, + { + "name": "stop_i_output", + "type": "any", + "description": "The result when the i-th stop is the last stop less than the `input`." + } + ] }, + "example": ["step", ["get", "point_count"], 20, 100, 30, 750, 40], "group": "Ramps, scales, curves", "sdk-support": { "basic functionality": { @@ -2934,14 +3385,38 @@ } }, "interpolate": { - "doc": "Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). The `input` may be any numeric expression (e.g., `[\"get\", \"population\"]`). Stop inputs must be numeric literals in strictly ascending order. The output type must be `number`, `array`, or `color`.\n\nInterpolation types:\n\n- `[\"linear\"]`, or an expression returning one of those types: Interpolates linearly between the pair of stops just less than and just greater than the input.\n- `[\"exponential\", base]`: Interpolates exponentially between the stops just less than and just greater than the input. `base` controls the rate at which the output increases: higher values make the output increase more towards the high end of the range. With values close to 1 the output increases linearly.\n- `[\"cubic-bezier\", x1, y1, x2, y2]`: Interpolates using the cubic bezier curve defined by the given control points.\n\n - [Animate map camera around a point](https://maplibre.org/maplibre-gl-js/docs/examples/animate-camera-around-point/)\n - [Change building color based on zoom level](https://maplibre.org/maplibre-gl-js/docs/examples/change-building-color-based-on-zoom-level/)\n - [Create a heatmap layer](https://maplibre.org/maplibre-gl-js/docs/examples/heatmap-layer/)\n - [Visualize population density](https://maplibre.org/maplibre-gl-js/docs/examples/visualize-population-density/)", - "example": { - "syntax": { - "method": ["[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]", "number", "number", "number", "..."], - "result": "number | array | color" - }, - "value": ["interpolate", ["linear"], ["zoom"], 15, 0, 15.05, ["get", "height"]] + "doc": "Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). The `input` may be any numeric expression (e.g., `[\"get\", \"population\"]`). Stop inputs must be numeric literals in strictly ascending order. The output type must be `number`, `array`, `color`, `array`, or `projection`.\n\nInterpolation types:\n\n- `[\"linear\"]`, or an expression returning one of those types: Interpolates linearly between the pair of stops just less than and just greater than the input.\n\n- `[\"exponential\", base]`: Interpolates exponentially between the stops just less than and just greater than the input. `base` controls the rate at which the output increases: higher values make the output increase more towards the high end of the range. With values close to 1 the output increases linearly.\n\n- `[\"cubic-bezier\", x1, y1, x2, y2]`: Interpolates using the cubic bezier curve defined by the given control points.\n\n - [Animate map camera around a point](https://maplibre.org/maplibre-gl-js/docs/examples/animate-camera-around-point/)\n\n - [Change building color based on zoom level](https://maplibre.org/maplibre-gl-js/docs/examples/change-building-color-based-on-zoom-level/)\n\n - [Create a heatmap layer](https://maplibre.org/maplibre-gl-js/docs/examples/heatmap-layer/)\n\n - [Visualize population density](https://maplibre.org/maplibre-gl-js/docs/examples/visualize-population-density/)", + "syntax": { + "overloads": [ + { + "parameters": ["interpolation_type", "input", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], + "output-type": "number | array | color | array | projection" + } + ], + "parameters": [ + { + "name": "interpolation_type", + "type": "[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]", + "description": "The interpolation type." + }, + { + "name": "input", + "type": "number", + "description": "Any numeric expression." + }, + { + "name": "stop_i_input", + "type": "number literal", + "description": "The value of the i-th stop against which the `input` is compared." + }, + { + "name": "stop_i_output", + "type": "number | array | color | array | projection", + "description": "The output value corresponding to the i-th stop." + } + ] }, + "example": ["interpolate", ["linear"], ["zoom"], 15, 0, 15.05, ["get", "height"]], "group": "Ramps, scales, curves", "sdk-support": { "basic functionality": { @@ -2952,46 +3427,92 @@ } }, "interpolate-hcl": { - "doc": "Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). Works like `interpolate`, but the output type must be `color`, and the interpolation is performed in the Hue-Chroma-Luminance color space.", - "example": { - "syntax": { - "method": ["[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]", "number", "number", "number", "..."], - "result": "color" - }, - "value": ["interpolate-hcl", ["linear"], ["zoom"], 15, "#f00", 15.05, "#00f"] + "doc": "Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). Works like `interpolate`, but the output type must be `color` or `array`, and the interpolation is performed in the Hue-Chroma-Luminance color space.", + "syntax": { + "overloads": [ + { + "parameters": ["interpolation_type", "input", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], + "output-type": "color | array" + } + ], + "parameters": [ + { + "name": "interpolation_type", + "type": "[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]" + }, + { + "name": "input", + "type": "number" + }, + { + "name": "stop_i_input", + "type": "number literal" + }, + { + "name": "stop_i_output", + "type": "color | array" + } + ] }, + "example": ["interpolate-hcl", ["linear"], ["zoom"], 15, "#f00", 15.05, "#00f"], "group": "Ramps, scales, curves", "sdk-support": { "basic functionality": { - "js": "0.49.0" + "js": "0.49.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2784", + "android": "https://github.com/maplibre/maplibre-native/issues/2784" } } }, "interpolate-lab": { - "doc": "Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). Works like `interpolate`, but the output type must be `color`, and the interpolation is performed in the CIELAB color space.", - "example": { - "syntax": { - "method": ["[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]", "number", "number", "number", "..."], - "result": "color" - }, - "value": ["interpolate-lab", ["linear"], ["zoom"], 15, "#f00", 15.05, "#00f"] + "doc": "Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). Works like `interpolate`, but the output type must be `color` or `array`, and the interpolation is performed in the CIELAB color space.", + "syntax": { + "overloads": [ + { + "parameters": ["interpolation_type", "input", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], + "output-type": "color | array" + } + ], + "parameters": [ + { + "name": "interpolation_type", + "type": "[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]" + }, + { + "name": "input", + "type": "number" + }, + { + "name": "stop_i_input", + "type": "number literal" + }, + { + "name": "stop_i_output", + "type": "color | array" + } + ] }, + "example": ["interpolate-lab", ["linear"], ["zoom"], 15, "#f00", 15.05, "#00f"], "group": "Ramps, scales, curves", "sdk-support": { "basic functionality": { - "js": "0.49.0" + "js": "0.49.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2784", + "android": "https://github.com/maplibre/maplibre-native/issues/2784" } } }, "ln2": { - "doc": "Returns mathematical constant ln(2).", - "example": { - "syntax": { - "method": [], - "result": "number" - }, - "value": ["ln2"] + "doc": "Returns the mathematical constant ln(2).", + "syntax": { + "overloads": [ + { + "parameters": [], + "output-type": "number" + } + ] }, + "example": ["ln2"], "group": "Math", "sdk-support": { "basic functionality": { @@ -3003,13 +3524,15 @@ }, "pi": { "doc": "Returns the mathematical constant pi.", - "example": { - "syntax": { - "method": [], - "result": "number" - }, - "value": ["pi"] + "syntax": { + "overloads": [ + { + "parameters": [], + "output-type": "number" + } + ] }, + "example": ["pi"], "group": "Math", "sdk-support": { "basic functionality": { @@ -3021,13 +3544,15 @@ }, "e": { "doc": "Returns the mathematical constant e.", - "example": { - "syntax": { - "method": [], - "result": "number" - }, - "value": ["e"] + "syntax": { + "overloads": [ + { + "parameters": [], + "output-type": "number" + } + ] }, + "example": ["e"], "group": "Math", "sdk-support": { "basic functionality": { @@ -3039,13 +3564,21 @@ }, "typeof": { "doc": "Returns a string describing the type of the given value.", - "example": { - "syntax": { - "method": ["value"], - "result": "string" - }, - "value": ["typeof", ["get", "name"]] + "syntax": { + "overloads": [ + { + "parameters": ["value"], + "output-type": "string" + } + ], + "parameters": [ + { + "name": "value", + "type": "any" + } + ] }, + "example": ["typeof", ["get", "name"]], "group": "Types", "sdk-support": { "basic functionality": { @@ -3057,13 +3590,21 @@ }, "string": { "doc": "Asserts that the input value is a string. If multiple values are provided, each one is evaluated in order until a string is obtained. If none of the inputs are strings, the expression is an error.", - "example": { - "syntax": { - "method": ["value", "fallback: value", "fallback: value", "..."], - "result": "string" - }, - "value": ["string", ["get", "name"]] + "syntax": { + "overloads": [ + { + "parameters": ["value_1", "...", "value_n"], + "output-type": "string" + } + ], + "parameters": [ + { + "name": "value_i", + "type": "any" + } + ] }, + "example": ["string", ["get", "name"]], "group": "Types", "sdk-support": { "basic functionality": { @@ -3075,13 +3616,21 @@ }, "number": { "doc": "Asserts that the input value is a number. If multiple values are provided, each one is evaluated in order until a number is obtained. If none of the inputs are numbers, the expression is an error.", - "example": { - "syntax": { - "method": ["value", "fallback: value", "fallback: value", "..."], - "result": "number" - }, - "value": ["number", ["get", "population"]] + "syntax": { + "overloads": [ + { + "parameters": ["value_1", "...", "value_n"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "value_i", + "type": "any" + } + ] }, + "example": ["number", ["get", "population"]], "group": "Types", "sdk-support": { "basic functionality": { @@ -3092,14 +3641,22 @@ } }, "boolean": { - "doc": "Asserts that the input value is a boolean. If multiple values are provided, each one is evaluated in order until a boolean is obtained. If none of the inputs are booleans, the expression is an error.\n\n - [Create a hover effect](https://maplibre.org/maplibre-gl-js/docs/examples/hover-styles/)", - "example": { - "syntax": { - "method": ["value", "fallback: value", "fallback: value", "..."], - "result": "boolean" - }, - "value": ["boolean", ["feature-state", "hover"], false] + "doc": "Asserts that the input value is a boolean. If multiple values are provided, each one is evaluated in order until a boolean is obtained. If none of the inputs are booleans, the expression is an error.\n\n - [Create a hover effect](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-hover-effect/)", + "syntax": { + "overloads": [ + { + "parameters": ["value_1", "...", "value_n"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "value_i", + "type": "any" + } + ] }, + "example": ["boolean", ["feature-state", "hover"], false], "group": "Types", "sdk-support": { "basic functionality": { @@ -3111,13 +3668,21 @@ }, "object": { "doc": "Asserts that the input value is an object. If multiple values are provided, each one is evaluated in order until an object is obtained. If none of the inputs are objects, the expression is an error.", - "example": { - "syntax": { - "method": ["value", "fallback: value", "fallback: value", "..."], - "result": "object" - }, - "value": ["object", ["get", "some-property"]] + "syntax": { + "overloads": [ + { + "parameters": ["value_1", "...", "value_n"], + "output-type": "object" + } + ], + "parameters": [ + { + "name": "value_i", + "type": "any" + } + ] }, + "example": ["object", ["get", "some-property"]], "group": "Types", "sdk-support": { "basic functionality": { @@ -3129,13 +3694,21 @@ }, "collator": { "doc": "Returns a `collator` for use in locale-dependent comparison operations. The `case-sensitive` and `diacritic-sensitive` options default to `false`. The `locale` argument specifies the IETF language tag of the locale to use. If none is provided, the default locale is used. If the requested locale is not available, the `collator` will use a system-defined fallback locale. Use `resolved-locale` to test the results of locale fallback behavior.", - "example": { - "syntax": { - "method": ["collator", "{ \"case-sensitive\": boolean, \"diacritic-sensitive\": boolean, \"locale\": string }"], - "result": "collator" - }, - "value": ["collator", {"case-sensitive": true, "diacritic-sensitive": true, "locale": "fr"}] + "syntax": { + "overloads": [ + { + "parameters": ["options"], + "output-type": "collator" + } + ], + "parameters": [ + { + "name": "options", + "type": "{ \"case-sensitive\"?: boolean, \"diacritic-sensitive\"?: boolean, \"locale\"?: string }" + } + ] }, + "example": ["collator", {"case-sensitive": true, "diacritic-sensitive": true, "locale": "fr"}], "group": "Types", "sdk-support": { "basic functionality": { @@ -3146,14 +3719,26 @@ } }, "format": { - "doc": "Returns a `formatted` string for displaying mixed-format text in the `text-field` property. The input may contain a string literal or expression, including an [`'image'`](#image) expression. Strings may be followed by a style override object that supports the following properties:\n- `\"text-font\"`: Overrides the font stack specified by the root layout property.\n- `\"text-color\"`: Overrides the color specified by the root paint property.\n- `\"font-scale\"`: Applies a scaling factor on `text-size` as specified by the root layout property.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)\n - [Display and style rich text labels](https://maplibre.org/maplibre-gl-js/docs/examples/display-and-style-rich-text-labels/)", - "example": { - "syntax": { - "method": ["value", "{ \"text-font\": string, \"text-color\": color, \"font-scale\": number }", "..."], - "result": "formatted" - }, - "value": ["format", ["upcase", ["get", "FacilityName"]], {"font-scale": 0.8}, "\n", {}, ["downcase", ["get", "Comments"]], {"font-scale": 0.6}] + "doc": "Returns a `formatted` string for displaying mixed-format text in the `text-field` property. The input may contain a string literal or expression, including an [`'image'`](#image) expression. Strings may be followed by a style override object that supports the following properties:\n\n- `\"text-font\"`: Overrides the font stack specified by the root layout property.\n\n- `\"text-color\"`: Overrides the color specified by the root paint property.\n\n- `\"font-scale\"`: Applies a scaling factor on `text-size` as specified by the root layout property.\n\n- `\"vertical-align\"`: Aligns vertically text section or image in relation to the row it belongs to. Possible values are: \n\t- `\"bottom\"` *default*: align the bottom of this section with the bottom of other sections.\n\"Visual\n\t- `\"center\"`: align the center of this section with the center of other sections.\n\"Visual\n\t- `\"top\"`: align the top of this section with the top of other sections.\n\"Visual\n\t- Refer to [the design proposal](https://github.com/maplibre/maplibre-style-spec/issues/832) for more details.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)\n\n - [Display and style rich text labels](https://maplibre.org/maplibre-gl-js/docs/examples/display-and-style-rich-text-labels/)", + "syntax": { + "overloads": [ + { + "parameters": ["input_1", "style_overrides_1?", "...", "input_n", "style_overrides_n?"], + "output-type": "formatted" + } + ], + "parameters": [ + { + "name": "input_i", + "type": "string | image" + }, + { + "name": "style_overrides_i", + "type": "{ \"text-font\"?: string, \"text-color\"?: color, \"font-scale\"?: number, \"vertical-align\"?: \"bottom\" | \"center\" | \"top\" }" + } + ] }, + "example": ["format", ["upcase", ["get", "FacilityName"]], {"font-scale": 0.8}, "\n\n", {}, ["downcase", ["get", "Comments"]], {"font-scale": 0.6, "vertical-align": "center"}], "group": "Types", "sdk-support": { "basic functionality": { @@ -3176,6 +3761,11 @@ "android": "7.3.0", "ios": "4.10.0" }, + "vertical-align": { + "js": "5.1.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3055", + "ios": "https://github.com/maplibre/maplibre-native/issues/3055" + }, "image": { "js": "1.6.0", "android": "8.6.0", @@ -3184,14 +3774,22 @@ } }, "image": { - "doc": "Returns an `image` type for use in `icon-image`, `*-pattern` entries and as a section in the `format` expression. If set, the `image` argument will check that the requested image exists in the style and will return either the resolved image name or `null`, depending on whether or not the image is currently in the style. This validation process is synchronous and requires the image to have been added to the style before requesting it in the `image` argument.\n\n - [Use a fallback image](https://maplibre.org/maplibre-gl-js/docs/examples/fallback-image/)", - "example": { - "syntax": { - "method": ["value"], - "result": "image" - }, - "value": ["image", "marker_15"] + "doc": "Returns an `image` type for use in `icon-image`, `*-pattern` entries and as a section in the `format` expression. If set, the `image` argument will check that the requested image exists in the style and will return either the resolved image name or `null`, depending on whether or not the image is currently in the style. This validation process is synchronous and requires the image to have been added to the style before requesting it in the `image` argument.\n\n - [Use a fallback image](https://maplibre.org/maplibre-gl-js/docs/examples/use-a-fallback-image/)", + "syntax": { + "overloads": [ + { + "parameters": ["image_name"], + "output-type": "image" + } + ], + "parameters": [ + { + "name": "image_name", + "type": "string" + } + ] }, + "example": ["image", "marker_15"], "group": "Types", "sdk-support": { "basic functionality": { @@ -3201,31 +3799,80 @@ } } }, + "global-state": { + "doc": "Retrieves a property value from global state that can be set with platform-specific APIs. Defaults can be provided using the [`state`](https://maplibre.org/maplibre-style-spec/root/#state) root property. Returns `null` if no value nor default value is set for the retrieved property.", + "group": "Lookup", + "syntax": { + "overloads": [ + { + "parameters": ["property_name"], + "output-type": "any" + } + ], + "parameters": [ + { + "name": "property_name", + "type": "string literal", + "description": "The name of the global state property to retrieve." + } + ] + }, + "example": ["global-state", "someProperty"], + "sdk-support": { + "basic functionality": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/4964", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" + } + } + }, "number-format": { - "doc": "Converts the input number into a string representation using the providing formatting rules. If set, the `locale` argument specifies the locale to use, as a BCP 47 language tag. If set, the `currency` argument specifies an ISO 4217 code to use for currency-style formatting. If set, the `min-fraction-digits` and `max-fraction-digits` arguments specify the minimum and maximum number of fractional digits to include.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/cluster-html/)", - "example": { - "syntax": { - "method": ["number", "{ \"locale\": string, \"currency\": string, \"min-fraction-digits\": number, \"max-fraction-digits\": number }"], - "result": "string" - }, - "value": ["number-format", ["get", "mag"], {"min-fraction-digits": 1, "max-fraction-digits": 1}] + "doc": "Converts the input number into a string representation using the providing formatting rules. If set, the `locale` argument specifies the locale to use, as a BCP 47 language tag. If set, the `currency` argument specifies an ISO 4217 code to use for currency-style formatting. If set, the `min-fraction-digits` and `max-fraction-digits` arguments specify the minimum and maximum number of fractional digits to include.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/display-html-clusters-with-custom-properties/)", + "syntax": { + "overloads": [ + { + "parameters": ["input", "format_options"], + "output-type": "string" + } + ], + "parameters": [ + { + "name": "input", + "type": "number" + }, + { + "name": "format_options", + "type": "{ \"locale\"?: string, \"currency\"?: string, \"min-fraction-digits\"?: number, \"max-fraction-digits\"?: number }" + } + ] }, + "example": ["number-format", ["get", "mag"], {"min-fraction-digits": 1, "max-fraction-digits": 1}], "group": "Types", "sdk-support": { "basic functionality": { - "js": "0.54.0" + "js": "0.54.0", + "android": "8.4.0", + "ios": "supported" } } }, "to-string": { - "doc": "Converts the input value to a string. If the input is `null`, the result is `\"\"`. If the input is a boolean, the result is `\"true\"` or `\"false\"`. If the input is a number, it is converted to a string as specified by the [\"NumberToString\" algorithm](https://tc39.github.io/ecma262/#sec-tostring-applied-to-the-number-type) of the ECMAScript Language Specification. If the input is a color, it is converted to a string of the form `\"rgba(r,g,b,a)\"`, where `r`, `g`, and `b` are numerals ranging from 0 to 255, and `a` ranges from 0 to 1. Otherwise, the input is converted to a string in the format specified by the [`JSON.stringify`](https://tc39.github.io/ecma262/#sec-json.stringify) function of the ECMAScript Language Specification.\n\n - [Create a time slider](https://maplibre.org/maplibre-gl-js/docs/examples/timeline-animation/)", - "example": { - "syntax": { - "method": ["value"], - "result": "string" - }, - "value": ["to-string", ["get", "mag"]] + "doc": "Converts the input value to a string. If the input is `null`, the result is `\"\"`. If the input is a boolean, the result is `\"true\"` or `\"false\"`. If the input is a number, it is converted to a string as specified by the [\"NumberToString\" algorithm](https://tc39.github.io/ecma262/#sec-tostring-applied-to-the-number-type) of the ECMAScript Language Specification. If the input is a color, it is converted to a string of the form `\"rgba(r,g,b,a)\"`, where `r`, `g`, and `b` are numerals ranging from 0 to 255, and `a` ranges from 0 to 1. Otherwise, the input is converted to a string in the format specified by the [`JSON.stringify`](https://tc39.github.io/ecma262/#sec-json.stringify) function of the ECMAScript Language Specification.\n\n - [Create a time slider](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-time-slider/)", + "syntax": { + "overloads": [ + { + "parameters": ["value"], + "output-type": "string" + } + ], + "parameters": [ + { + "name": "value", + "type": "any" + } + ] }, + "example": ["to-string", ["get", "mag"]], "group": "Types", "sdk-support": { "basic functionality": { @@ -3237,13 +3884,21 @@ }, "to-number": { "doc": "Converts the input value to a number, if possible. If the input is `null` or `false`, the result is 0. If the input is `true`, the result is 1. If the input is a string, it is converted to a number as specified by the [\"ToNumber Applied to the String Type\" algorithm](https://tc39.github.io/ecma262/#sec-tonumber-applied-to-the-string-type) of the ECMAScript Language Specification. If multiple values are provided, each one is evaluated in order until the first successful conversion is obtained. If none of the inputs can be converted, the expression is an error.", - "example": { - "syntax": { - "method": ["value", "fallback: value", "fallback: value", "..."], - "result": "number" - }, - "value": ["to-number", "someProperty"] + "syntax": { + "overloads": [ + { + "parameters": ["value_1", "...", "value_n"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "value_i", + "type": "any" + } + ] }, + "example": ["to-number", "someProperty"], "group": "Types", "sdk-support": { "basic functionality": { @@ -3254,14 +3909,22 @@ } }, "to-boolean": { - "doc": "Converts the input value to a boolean. The result is `false` when then input is an empty string, 0, `false`, `null`, or `NaN`; otherwise it is `true`.", - "example": { - "syntax": { - "method": ["value"], - "result": "boolean" - }, - "value": ["to-boolean", "someProperty"] + "doc": "Converts the input value to a boolean. The result is `false` when the input is an empty string, 0, `false`, `null`, or `NaN`; otherwise it is `true`.", + "syntax": { + "overloads": [ + { + "parameters": ["value"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "value", + "type": "any" + } + ] }, + "example": ["to-boolean", "someProperty"], "group": "Types", "sdk-support": { "basic functionality": { @@ -3273,13 +3936,21 @@ }, "to-rgba": { "doc": "Returns a four-element array containing the input color's red, green, blue, and alpha components, in that order.", - "example": { - "syntax": { - "method": ["color"], - "result": "array" - }, - "value": ["to-rgba", "#ff0000"] + "syntax": { + "overloads": [ + { + "parameters": ["color"], + "output-type": "array" + } + ], + "parameters": [ + { + "name": "color", + "type": "color" + } + ] }, + "example": ["to-rgba", "#ff0000"], "group": "Color", "sdk-support": { "basic functionality": { @@ -3291,13 +3962,21 @@ }, "to-color": { "doc": "Converts the input value to a color. If multiple values are provided, each one is evaluated in order until the first successful conversion is obtained. If none of the inputs can be converted, the expression is an error.\n\n - [Visualize population density](https://maplibre.org/maplibre-gl-js/docs/examples/visualize-population-density/)", - "example": { - "syntax": { - "method": ["value", "fallback: value", "fallback: value", "..."], - "result": "color" - }, - "value": ["to-color", "#edf8e9"] + "syntax": { + "overloads": [ + { + "parameters": ["value_1", "...", "value_n"], + "output-type": "color" + } + ], + "parameters": [ + { + "name": "value_i", + "type": "any" + } + ] }, + "example": ["to-color", "#edf8e9"], "group": "Types", "sdk-support": { "basic functionality": { @@ -3309,13 +3988,29 @@ }, "rgb": { "doc": "Creates a color value from red, green, and blue components, which must range between 0 and 255, and an alpha component of 1. If any component is out of range, the expression is an error.", - "example": { - "syntax": { - "method": ["number", "number", "number"], - "result": "color" - }, - "value": ["rgb", 255, 0, 0] + "syntax": { + "overloads": [ + { + "parameters": ["red", "green", "blue"], + "output-type": "color" + } + ], + "parameters": [ + { + "name": "red", + "type": "number" + }, + { + "name": "green", + "type": "number" + }, + { + "name": "blue", + "type": "number" + } + ] }, + "example": ["rgb", 255, 0, 0], "group": "Color", "sdk-support": { "basic functionality": { @@ -3327,13 +4022,33 @@ }, "rgba": { "doc": "Creates a color value from red, green, blue components, which must range between 0 and 255, and an alpha component which must range between 0 and 1. If any component is out of range, the expression is an error.", - "example": { - "syntax": { - "method": ["number", "number", "number", "number"], - "result": "color" - }, - "value": ["rgba", 255, 0, 0, 1] + "syntax": { + "overloads": [ + { + "parameters": ["red", "green", "blue", "alpha"], + "output-type": "color" + } + ], + "parameters": [ + { + "name": "red", + "type": "number" + }, + { + "name": "green", + "type": "number" + }, + { + "name": "blue", + "type": "number" + }, + { + "name": "alpha", + "type": "number" + } + ] }, + "example": ["rgba", 255, 0, 0, 1], "group": "Color", "sdk-support": { "basic functionality": { @@ -3344,14 +4059,28 @@ } }, "get": { - "doc": "Retrieves a property value from the current feature's properties, or from another object if a second argument is provided. Returns null if the requested property is missing.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/cluster-html/)\n - [Extrude polygons for 3D indoor mapping](https://maplibre.org/maplibre-gl-js/docs/examples/3d-extrusion-floorplan/)", - "example": { - "syntax": { - "method": ["string"], - "result": "value" - }, - "value": ["get", "someProperty"] + "doc": "Retrieves a property value from the current feature's properties, or from another object if a second argument is provided. Returns null if the requested property is missing.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/display-html-clusters-with-custom-properties/)\n\n - [Extrude polygons for 3D indoor mapping](https://maplibre.org/maplibre-gl-js/docs/examples/extrude-polygons-for-3d-indoor-mapping/)", + "syntax": { + "overloads": [ + { + "parameters": ["property_name", "object?"], + "output-type": "any" + } + ], + "parameters": [ + { + "name": "property_name", + "type": "string", + "description": "The name of the property to retrieve the value of." + }, + { + "name": "object", + "type": "object", + "description": "The object to retrieve the value from." + } + ] }, + "example": ["get", "someProperty"], "group": "Lookup", "sdk-support": { "basic functionality": { @@ -3362,14 +4091,28 @@ } }, "has": { - "doc": "Tests for the presence of an property value in the current feature's properties, or from another object if a second argument is provided.\n\n - [Create and style clusters](https://maplibre.org/maplibre-gl-js/docs/examples/cluster/)", - "example": { - "syntax": { - "method": ["string"], - "result": "boolean" - }, - "value": ["has", "someProperty"] + "doc": "Tests for the presence of a property value in the current feature's properties, or from another object if a second argument is provided.\n\n - [Create and style clusters](https://maplibre.org/maplibre-gl-js/docs/examples/create-and-style-clusters/)", + "syntax": { + "overloads": [ + { + "parameters": ["property_name", "object?"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "property_name", + "type": "string", + "description": "The name of the property to test for the presence of." + }, + { + "name": "object", + "type": "object", + "description": "The object in which to test for the presence of the `property_name` property." + } + ] }, + "example": ["has", "someProperty"], "group": "Lookup", "sdk-support": { "basic functionality": { @@ -3381,13 +4124,21 @@ }, "length": { "doc": "Gets the length of an array or string. In a string, a UTF-16 surrogate pair counts as a single position.", - "example": { - "syntax": { - "method": ["array"], - "result": "number" - }, - "value": ["length", ["get", "myArray"]] + "syntax": { + "overloads": [ + { + "parameters": ["array_or_string"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "array_or_string", + "type": "array | string" + } + ] }, + "example": ["length", ["get", "myArray"]], "group": "Lookup", "sdk-support": { "basic functionality": { @@ -3399,13 +4150,15 @@ }, "properties": { "doc": "Gets the feature properties object. Note that in some cases, it may be more efficient to use [\"get\", \"property_name\"] directly.", - "example": { - "syntax": { - "method": [], - "result": "value" - }, - "value": ["properties"] + "syntax": { + "overloads": [ + { + "parameters": [], + "output-type": "object" + } + ] }, + "example": ["properties"], "group": "Feature data", "sdk-support": { "basic functionality": { @@ -3416,30 +4169,42 @@ } }, "feature-state": { - "doc": "Retrieves a property value from the current feature's state. Returns null if the requested property is not present on the feature's state. A feature's state is not part of the GeoJSON or vector tile data, and must be set programmatically on each feature. When `source.promoteId` is not provided, features are identified by their `id` attribute, which must be an integer or a string that can be cast to an integer. When `source.promoteId` is provided, features are identified by their `promoteId` property, which may be a number, string, or any primitive data type. Note that [\"feature-state\"] can only be used with paint properties that support data-driven styling.\n\n - [Create a hover effect](https://maplibre.org/maplibre-gl-js/docs/examples/hover-styles/)", - "example": { - "syntax": { - "method": ["string"], - "result": "value" - }, - "value": ["feature-state", "hover"] + "doc": "Retrieves a property value from the current feature's state. Returns null if the requested property is not present on the feature's state. A feature's state is not part of the GeoJSON or vector tile data, and must be set programmatically on each feature. When `source.promoteId` is not provided, features are identified by their `id` attribute, which must be an integer or a string that can be cast to an integer. When `source.promoteId` is provided, features are identified by their `promoteId` property, which may be a number, string, or any primitive data type. Note that [\"feature-state\"] can only be used with paint properties that support data-driven styling.\n\n - [Create a hover effect](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-hover-effect/)", + "syntax": { + "overloads": [ + { + "parameters": ["property_name"], + "output-type": "any" + } + ], + "parameters": [ + { + "name": "property_name", + "type": "string" + } + ] }, + "example": ["feature-state", "hover"], "group": "Feature data", "sdk-support": { "basic functionality": { - "js": "0.46.0" + "js": "0.46.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/1698", + "android": "https://github.com/maplibre/maplibre-native/issues/1698" } } }, "geometry-type": { - "doc": "Gets the feature's geometry type: `Point`, `MultiPoint`, `LineString`, `MultiLineString`, `Polygon`, `MultiPolygon`.", - "example": { - "syntax": { - "method": [], - "result": "string" - }, - "value": ["==", ["geometry-type"], "Polygon"] + "doc": "Returns the feature's simple geometry type: `Point`, `LineString`, or `Polygon`. `MultiPoint`, `MultiLineString`, and `MultiPolygon` are returned as `Point`, `LineString`, and `Polygon`, respectively.", + "syntax": { + "overloads": [ + { + "parameters": [], + "output-type": "string" + } + ] }, + "example": ["==", ["geometry-type"], "Polygon"], "group": "Feature data", "sdk-support": { "basic functionality": { @@ -3451,13 +4216,15 @@ }, "id": { "doc": "Gets the feature's id, if it has one.", - "example": { - "syntax": { - "method": [], - "result": "value" - }, - "value": ["id"] + "syntax": { + "overloads": [ + { + "parameters": [], + "output-type": "any" + } + ] }, + "example": ["id"], "group": "Feature data", "sdk-support": { "basic functionality": { @@ -3469,13 +4236,15 @@ }, "zoom": { "doc": "Gets the current zoom level. Note that in style layout and paint properties, [\"zoom\"] may only appear as the input to a top-level \"step\" or \"interpolate\" expression.", - "example": { - "syntax": { - "method": [], - "result": "number" - }, - "value": ["interpolate",["linear"],["zoom"],15,0,15.05,["get","height"]] + "syntax": { + "overloads": [ + { + "parameters": [], + "output-type": "number" + } + ] }, + "example": ["interpolate", ["linear"], ["zoom"], 15, 0, 15.05, ["get", "height"]], "group": "Zoom", "sdk-support": { "basic functionality": { @@ -3487,13 +4256,15 @@ }, "heatmap-density": { "doc": "Gets the kernel density estimation of a pixel in a heatmap layer, which is a relative measure of how many data points are crowded around a particular pixel. Can only be used in the `heatmap-color` property.", - "example": { - "syntax": { - "method": [], - "result": "number" - }, - "value": ["heatmap-density"] + "syntax": { + "overloads": [ + { + "parameters": [], + "output-type": "number" + } + ] }, + "example": ["heatmap-density"], "group": "Heatmap", "sdk-support": { "basic functionality": { @@ -3503,15 +4274,37 @@ } } }, + "elevation": { + "doc": "Gets the elevation of a pixel (in meters above the vertical datum reference of the `raster-dem` tiles) from a `raster-dem` source. Can only be used in the `color-relief-color` property of a `color-relief` layer.", + "syntax": { + "overloads": [ + { + "parameters": [], + "output-type": "number" + } + ] + }, + "example": ["elevation"], + "group": "Color Relief", + "sdk-support": { + "basic functionality": { + "js": "5.6.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3408", + "ios": "https://github.com/maplibre/maplibre-native/issues/3408" + } + } + }, "line-progress": { "doc": "Gets the progress along a gradient line. Can only be used in the `line-gradient` property.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["line-progress", 0.5] + "syntax": { + "overloads": [ + { + "parameters": [], + "output-type": "number" + } + ] }, + "example": ["line-progress"], "group": "Feature data", "sdk-support": { "basic functionality": { @@ -3523,29 +4316,41 @@ }, "accumulated": { "doc": "Gets the value of a cluster property accumulated so far. Can only be used in the `clusterProperties` option of a clustered GeoJSON source.", - "example": { - "syntax": { - "method": ["string"], - "result": "value" - }, - "value": ["accumulated", "sum"] + "syntax": { + "overloads": [ + { + "parameters": [], + "output-type": "any" + } + ] }, + "example": ["accumulated"], "group": "Feature data", "sdk-support": { "basic functionality": { - "js": "0.53.0" + "js": "0.53.0", + "ios": "supported", + "android": "supported" } } }, "+": { "doc": "Returns the sum of the inputs.", - "example": { - "syntax": { - "method": ["number", "number", "..."], - "result": "number" - }, - "value": ["+", 2, 3] + "syntax": { + "overloads": [ + { + "parameters": ["input_1", "...", "input_n"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input_i", + "type": "number" + } + ] }, + "example": ["+", 2, 3], "group": "Math", "sdk-support": { "basic functionality": { @@ -3557,13 +4362,21 @@ }, "*": { "doc": "Returns the product of the inputs.", - "example": { - "syntax": { - "method": ["number", "number", "..."], - "result": "number" - }, - "value": ["*", 2, 3] + "syntax": { + "overloads": [ + { + "parameters": ["input_1", "...", "input_n"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input_i", + "type": "number" + } + ] }, + "example": ["*", 2, 3], "group": "Math", "sdk-support": { "basic functionality": { @@ -3575,13 +4388,36 @@ }, "-": { "doc": "For two inputs, returns the result of subtracting the second input from the first. For a single input, returns the result of subtracting it from 0.", - "example": { - "syntax": { - "method": ["number", "number?"], - "result": "number" - }, - "value": ["-", 10] + "syntax": { + "overloads": [ + { + "parameters": ["input_1", "input_2"], + "output-type": "number" + }, + { + "parameters": ["single_input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input_1", + "type": "number", + "description": "The number from which to subtract `input_2`." + }, + { + "name": "input_2", + "type": "number", + "description": "The number to subtract from `input_1`." + }, + { + "name": "single_input", + "type": "number", + "description": "The number to subtract from 0." + } + ] }, + "example": ["-", 10], "group": "Math", "sdk-support": { "basic functionality": { @@ -3593,13 +4429,27 @@ }, "/": { "doc": "Returns the result of floating point division of the first input by the second.\n\n - [Visualize population density](https://maplibre.org/maplibre-gl-js/docs/examples/visualize-population-density/)", - "example": { - "syntax": { - "method": ["number", "number"], - "result": "number" - }, - "value": ["/", ["get", "population"], ["get", "sq-km"]] + "syntax": { + "overloads": [ + { + "parameters": ["input_1", "input_2"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input_1", + "type": "number", + "description": "The dividend." + }, + { + "name": "input_2", + "type": "number", + "description": "The divisor." + } + ] }, + "example": ["/", ["get", "population"], ["get", "sq-km"]], "group": "Math", "sdk-support": { "basic functionality": { @@ -3611,13 +4461,27 @@ }, "%": { "doc": "Returns the remainder after integer division of the first input by the second.", - "example": { - "syntax": { - "method": ["number", "number"], - "result": "number" - }, - "value": ["%", 10, 3] + "syntax": { + "overloads": [ + { + "parameters": ["input_1", "input_2"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input_1", + "type": "number", + "description": "The dividend." + }, + { + "name": "input_2", + "type": "number", + "description": "The divisor." + } + ] }, + "example": ["%", 10, 3], "group": "Math", "sdk-support": { "basic functionality": { @@ -3629,13 +4493,27 @@ }, "^": { "doc": "Returns the result of raising the first input to the power specified by the second.", - "example": { - "syntax": { - "method": ["number", "number"], - "result": "number" - }, - "value": ["^", 2, 3] + "syntax": { + "overloads": [ + { + "parameters": ["input_1", "input_2"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input_1", + "type": "number", + "description": "The base." + }, + { + "name": "input_2", + "type": "number", + "description": "The exponent." + } + ] }, + "example": ["^", 2, 3], "group": "Math", "sdk-support": { "basic functionality": { @@ -3647,13 +4525,22 @@ }, "sqrt": { "doc": "Returns the square root of the input.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["sqrt", 9] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input", + "type": "number", + "description": "The radicand." + } + ] }, + "example": ["sqrt", 9], "group": "Math", "sdk-support": { "basic functionality": { @@ -3665,13 +4552,21 @@ }, "log10": { "doc": "Returns the base-ten logarithm of the input.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["log10", 8] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input", + "type": "number" + } + ] }, + "example": ["log10", 8], "group": "Math", "sdk-support": { "basic functionality": { @@ -3683,13 +4578,21 @@ }, "ln": { "doc": "Returns the natural logarithm of the input.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["ln", 8] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input", + "type": "number" + } + ] }, + "example": ["ln", 8], "group": "Math", "sdk-support": { "basic functionality": { @@ -3701,13 +4604,21 @@ }, "log2": { "doc": "Returns the base-two logarithm of the input.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["log2", 8] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input", + "type": "number" + } + ] }, + "example": ["log2", 8], "group": "Math", "sdk-support": { "basic functionality": { @@ -3719,13 +4630,21 @@ }, "sin": { "doc": "Returns the sine of the input.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["sin", 1] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input", + "type": "number" + } + ] }, + "example": ["sin", 1], "group": "Math", "sdk-support": { "basic functionality": { @@ -3737,13 +4656,21 @@ }, "cos": { "doc": "Returns the cosine of the input.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["cos", 1] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input", + "type": "number" + } + ] }, + "example": ["cos", 1], "group": "Math", "sdk-support": { "basic functionality": { @@ -3755,13 +4682,21 @@ }, "tan": { "doc": "Returns the tangent of the input.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["tan", 1] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input", + "type": "number" + } + ] }, + "example": ["tan", 1], "group": "Math", "sdk-support": { "basic functionality": { @@ -3773,13 +4708,21 @@ }, "asin": { "doc": "Returns the arcsine of the input.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["asin", 1] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input", + "type": "number" + } + ] }, + "example": ["asin", 1], "group": "Math", "sdk-support": { "basic functionality": { @@ -3791,13 +4734,21 @@ }, "acos": { "doc": "Returns the arccosine of the input.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["acos", 1] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input", + "type": "number" + } + ] }, + "example": ["acos", 1], "group": "Math", "sdk-support": { "basic functionality": { @@ -3809,13 +4760,21 @@ }, "atan": { "doc": "Returns the arctangent of the input.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["atan", 1] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input", + "type": "number" + } + ] }, + "example": ["atan", 1], "group": "Math", "sdk-support": { "basic functionality": { @@ -3827,13 +4786,21 @@ }, "min": { "doc": "Returns the minimum value of the inputs.", - "example": { - "syntax": { - "method": ["number", "number", "..."], - "result": "number" - }, - "value": ["min", 1, 2] + "syntax": { + "overloads": [ + { + "parameters": ["input_1", "...", "input_n"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input_i", + "type": "number" + } + ] }, + "example": ["min", 1, 2], "group": "Math", "sdk-support": { "basic functionality": { @@ -3845,13 +4812,21 @@ }, "max": { "doc": "Returns the maximum value of the inputs.", - "example": { - "syntax": { - "method": ["number", "number", "..."], - "result": "number" - }, - "value": ["max", 1, 2] + "syntax": { + "overloads": [ + { + "parameters": ["input_1", "...", "input_n"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input_i", + "type": "number" + } + ] }, + "example": ["max", 1, 2], "group": "Math", "sdk-support": { "basic functionality": { @@ -3863,13 +4838,21 @@ }, "round": { "doc": "Rounds the input to the nearest integer. Halfway values are rounded away from zero. For example, `[\"round\", -1.5]` evaluates to -2.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["round", 1.5] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input", + "type": "number" + } + ] }, + "example": ["round", 1.5], "group": "Math", "sdk-support": { "basic functionality": { @@ -3881,13 +4864,21 @@ }, "abs": { "doc": "Returns the absolute value of the input.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["abs", -1.5] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input", + "type": "number" + } + ] }, + "example": ["abs", -1.5], "group": "Math", "sdk-support": { "basic functionality": { @@ -3899,13 +4890,21 @@ }, "ceil": { "doc": "Returns the smallest integer that is greater than or equal to the input.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["ceil", 1.5] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input", + "type": "number" + } + ] }, + "example": ["ceil", 1.5], "group": "Math", "sdk-support": { "basic functionality": { @@ -3917,13 +4916,21 @@ }, "floor": { "doc": "Returns the largest integer that is less than or equal to the input.", - "example": { - "syntax": { - "method": ["number"], - "result": "number" - }, - "value": ["floor", 1.5] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "input", + "type": "number" + } + ] }, + "example": ["floor", 1.5], "group": "Math", "sdk-support": { "basic functionality": { @@ -3935,13 +4942,21 @@ }, "distance": { "doc": "Returns the shortest distance in meters between the evaluated feature and the input geometry. The input value can be a valid GeoJSON of type `Point`, `MultiPoint`, `LineString`, `MultiLineString`, `Polygon`, `MultiPolygon`, `Feature`, or `FeatureCollection`. Distance values returned may vary in precision due to loss in precision from encoding geometries, particularly below zoom level 13.", - "example": { - "syntax": { - "method": ["GeoJSON geometry"], - "result": "number" - }, - "value": ["distance", { "type": "Point", "coordinates": [0, 0]}] + "syntax": { + "overloads": [ + { + "parameters": ["geojson"], + "output-type": "number" + } + ], + "parameters": [ + { + "name": "geojson", + "type": "GeoJSON object" + } + ] }, + "example": ["distance", {"type": "Point", "coordinates": [0, 0]}], "group": "Math", "sdk-support": { "basic functionality": { @@ -3952,14 +4967,31 @@ } }, "==": { - "doc": "Returns `true` if the input values are equal, `false` otherwise. The comparison is strictly typed: values of different runtime types are always considered unequal. Cases where the types are known to be different at parse time are considered invalid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.\n\n - [Add multiple geometries from one GeoJSON source](https://maplibre.org/maplibre-gl-js/docs/examples/multiple-geometries/)\n - [Create a time slider](https://maplibre.org/maplibre-gl-js/docs/examples/timeline-animation/)\n - [Display buildings in 3D](https://maplibre.org/maplibre-gl-js/docs/examples/3d-buildings/)\n - [Filter symbols by toggling a list](https://maplibre.org/maplibre-gl-js/docs/examples/filter-markers/)", - "example": { - "syntax": { - "method": ["value", "value", "collator?"], - "result": "boolean" - }, - "value": ["==", "$type", "Polygon"] + "doc": "Returns `true` if the input values are equal, `false` otherwise. The comparison is strictly typed: values of different runtime types are always considered unequal. Cases where the types are known to be different at parse time are considered invalid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.\n\n - [Add multiple geometries from one GeoJSON source](https://maplibre.org/maplibre-gl-js/docs/examples/multiple-geometries/)\n\n - [Create a time slider](https://maplibre.org/maplibre-gl-js/docs/examples/timeline-animation/)\n\n - [Display buildings in 3D](https://maplibre.org/maplibre-gl-js/docs/examples/display-buildings-in-3d/)\n\n - [Filter symbols by toggling a list](https://maplibre.org/maplibre-gl-js/docs/examples/filter-symbols-by-toggling-a-list/)", + "syntax": { + "overloads": [ + { + "parameters": ["input_1", "input_2", "collator?"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "input_1", + "type": "any" + }, + { + "name": "input_2", + "type": "any" + }, + { + "name": "collator", + "type": "collator", + "description": "Options for locale-dependent comparison." + } + ] }, + "example": ["==", "$type", "Polygon"], "group": "Decision", "sdk-support": { "basic functionality": { @@ -3975,14 +5007,31 @@ } }, "!=": { - "doc": "Returns `true` if the input values are not equal, `false` otherwise. The comparison is strictly typed: values of different runtime types are always considered unequal. Cases where the types are known to be different at parse time are considered invalid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/cluster-html/)", - "example": { - "syntax": { - "method": ["value", "value", "collator?"], - "result": "boolean" - }, - "value": ["!=", "cluster", true] + "doc": "Returns `true` if the input values are not equal, `false` otherwise. The comparison is strictly typed: values of different runtime types are always considered unequal. Cases where the types are known to be different at parse time are considered invalid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/display-html-clusters-with-custom-properties/)", + "syntax": { + "overloads": [ + { + "parameters": ["input_1", "input_2", "collator?"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "input_1", + "type": "any" + }, + { + "name": "input_2", + "type": "any" + }, + { + "name": "collator", + "type": "collator", + "description": "Options for locale-dependent comparison." + } + ] }, + "example": ["!=", "cluster", true], "group": "Decision", "sdk-support": { "basic functionality": { @@ -3999,13 +5048,34 @@ }, ">": { "doc": "Returns `true` if the first input is strictly greater than the second, `false` otherwise. The arguments are required to be either both strings or both numbers; if during evaluation they are not, expression evaluation produces an error. Cases where this constraint is known not to hold at parse time are considered in valid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.", - "example": { - "syntax": { - "method": ["value", "value", "collator?"], - "result": "boolean" + "syntax": { + "overloads": [ + { + "parameters": ["string_1", "string_2", "collator?"], + "output-type": "boolean" + }, + { + "parameters": ["number_1", "number_2", "collator?"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "string_i", + "type": "string" + }, + { + "name": "number_i", + "type": "number" }, - "value": [">", ["get", "mag"], 2] + { + "name": "collator", + "type": "collator", + "description": "Options for locale-dependent comparison." + } + ] }, + "example": [">", ["get", "mag"], 2], "group": "Decision", "sdk-support": { "basic functionality": { @@ -4021,14 +5091,35 @@ } }, "<": { - "doc": "Returns `true` if the first input is strictly less than the second, `false` otherwise. The arguments are required to be either both strings or both numbers; if during evaluation they are not, expression evaluation produces an error. Cases where this constraint is known not to hold at parse time are considered in valid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/cluster-html/)", - "example": { - "syntax": { - "method": ["value", "value", "collator?"], - "result": "boolean" - }, - "value": ["<", ["get", "mag"], 2] + "doc": "Returns `true` if the first input is strictly less than the second, `false` otherwise. The arguments are required to be either both strings or both numbers; if during evaluation they are not, expression evaluation produces an error. Cases where this constraint is known not to hold at parse time are considered in valid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/display-html-clusters-with-custom-properties/)", + "syntax": { + "overloads": [ + { + "parameters": ["string_1", "string_2", "collator?"], + "output-type": "boolean" + }, + { + "parameters": ["number_1", "number_2", "collator?"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "string_i", + "type": "string" + }, + { + "name": "number_i", + "type": "number" + }, + { + "name": "collator", + "type": "collator", + "description": "Options for locale-dependent comparison." + } + ] }, + "example": ["<", ["get", "mag"], 2], "group": "Decision", "sdk-support": { "basic functionality": { @@ -4044,14 +5135,35 @@ } }, ">=": { - "doc": "Returns `true` if the first input is greater than or equal to the second, `false` otherwise. The arguments are required to be either both strings or both numbers; if during evaluation they are not, expression evaluation produces an error. Cases where this constraint is known not to hold at parse time are considered in valid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/cluster-html/)", - "example": { - "syntax": { - "method": ["value", "value", "collator?"], - "result": "boolean" - }, - "value": [">=", ["get", "mag"], 6] + "doc": "Returns `true` if the first input is greater than or equal to the second, `false` otherwise. The arguments are required to be either both strings or both numbers; if during evaluation they are not, expression evaluation produces an error. Cases where this constraint is known not to hold at parse time are considered in valid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/display-html-clusters-with-custom-properties/)", + "syntax": { + "overloads": [ + { + "parameters": ["string_1", "string_2", "collator?"], + "output-type": "boolean" + }, + { + "parameters": ["number_1", "number_2", "collator?"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "string_i", + "type": "string" + }, + { + "name": "number_i", + "type": "number" + }, + { + "name": "collator", + "type": "collator", + "description": "Options for locale-dependent comparison." + } + ] }, + "example": [">=", ["get", "mag"], 6], "group": "Decision", "sdk-support": { "basic functionality": { @@ -4068,13 +5180,34 @@ }, "<=": { "doc": "Returns `true` if the first input is less than or equal to the second, `false` otherwise. The arguments are required to be either both strings or both numbers; if during evaluation they are not, expression evaluation produces an error. Cases where this constraint is known not to hold at parse time are considered in valid and will produce a parse error. Accepts an optional `collator` argument to control locale-dependent string comparisons.", - "example": { - "syntax": { - "method": ["value", "value", "collator?"], - "result": "boolean" + "syntax": { + "overloads": [ + { + "parameters": ["string_1", "string_2", "collator?"], + "output-type": "boolean" + }, + { + "parameters": ["number_1", "number_2", "collator?"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "string_i", + "type": "string" }, - "value": ["<=", ["get", "mag"], 6] + { + "name": "number_i", + "type": "number" + }, + { + "name": "collator", + "type": "collator", + "description": "Options for locale-dependent comparison." + } + ] }, + "example": ["<=", ["get", "mag"], 6], "group": "Decision", "sdk-support": { "basic functionality": { @@ -4090,14 +5223,22 @@ } }, "all": { - "doc": "Returns `true` if all the inputs are `true`, `false` otherwise. The inputs are evaluated in order, and evaluation is short-circuiting: once an input expression evaluates to `false`, the result is `false` and no further input expressions are evaluated.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/cluster-html/)", - "example": { - "syntax": { - "method": ["boolean", "boolean", "..."], - "result": "boolean" - }, - "value": ["all", [">=", ["get", "mag"], 4], ["<", ["get", "mag"], 5]] + "doc": "Returns `true` if all the inputs are `true`, `false` otherwise. The inputs are evaluated in order, and evaluation is short-circuiting: once an input expression evaluates to `false`, the result is `false` and no further input expressions are evaluated.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/display-html-clusters-with-custom-properties/)", + "syntax": { + "overloads": [ + { + "parameters": ["input_1", "...", "input_n"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "input_i", + "type": "boolean" + } + ] }, + "example": ["all", [">=", ["get", "mag"], 4], ["<", ["get", "mag"], 5]], "group": "Decision", "sdk-support": { "basic functionality": { @@ -4109,13 +5250,21 @@ }, "any": { "doc": "Returns `true` if any of the inputs are `true`, `false` otherwise. The inputs are evaluated in order, and evaluation is short-circuiting: once an input expression evaluates to `true`, the result is `true` and no further input expressions are evaluated.", - "example": { - "syntax": { - "method": ["boolean", "boolean", "..."], - "result": "boolean" - }, - "value": ["any", [">=", ["get", "mag"], 4], ["<", ["get", "mag"], 5]] + "syntax": { + "overloads": [ + { + "parameters": ["input_1", "...", "input_n"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "input_i", + "type": "boolean" + } + ] }, + "example": ["any", [">=", ["get", "mag"], 4], ["<", ["get", "mag"], 5]], "group": "Decision", "sdk-support": { "basic functionality": { @@ -4126,14 +5275,22 @@ } }, "!": { - "doc": "Logical negation. Returns `true` if the input is `false`, and `false` if the input is `true`.\n\n - [Create and style clusters](https://maplibre.org/maplibre-gl-js/docs/examples/cluster/)", - "example": { - "syntax": { - "method": ["value", "value", "collator?"], - "result": "boolean" - }, - "value": ["!", ["has", "point_count"]] + "doc": "Logical negation. Returns `true` if the input is `false`, and `false` if the input is `true`.\n\n - [Create and style clusters](https://maplibre.org/maplibre-gl-js/docs/examples/create-and-style-clusters/)", + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "input", + "type": "boolean" + } + ] }, + "example": ["!", ["has", "point_count"]], "group": "Decision", "sdk-support": { "basic functionality": { @@ -4144,14 +5301,22 @@ } }, "within": { - "doc": "Returns `true` if the evaluated feature is fully contained inside a boundary of the input geometry, `false` otherwise. The input value can be a valid GeoJSON of type `Polygon`, `MultiPolygon`, `Feature`, or `FeatureCollection`. Supported features for evaluation:\n- `Point`: Returns `false` if a point is on the boundary or falls outside the boundary.\n- `LineString`: Returns `false` if any part of a line falls outside the boundary, the line intersects the boundary, or a line's endpoint is on the boundary.", - "example": { - "syntax": { - "method": ["GeoJSON geometry"], - "result": "boolean" - }, - "value": ["within", {"type": "Polygon","coordinates": [[[0, 0], [0, 5], [5, 5], [5, 0], [0, 0]]]}] + "doc": "Returns `true` if the evaluated feature is fully contained inside a boundary of the input geometry, `false` otherwise. The input value can be a valid GeoJSON of type `Polygon`, `MultiPolygon`, `Feature`, or `FeatureCollection`. Supported features for evaluation:\n\n- `Point`: Returns `false` if a point is on the boundary or falls outside the boundary.\n\n- `LineString`: Returns `false` if any part of a line falls outside the boundary, the line intersects the boundary, or a line's endpoint is on the boundary.", + "syntax": { + "overloads": [ + { + "parameters": ["geojson"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "geojson", + "type": "GeoJSON object" + } + ] }, + "example": ["within", {"type": "Polygon", "coordinates": [[[0, 0], [0, 5], [5, 5], [5, 0], [0, 0]]]}], "group": "Decision", "sdk-support": { "basic functionality": { @@ -4162,31 +5327,48 @@ } }, "is-supported-script": { - "doc": "Returns `true` if the input string is expected to render legibly. Returns `false` if the input string contains sections that cannot be rendered without potential loss of meaning (e.g. Indic scripts that require complex text shaping, or right-to-left scripts if the the `mapbox-gl-rtl-text` plugin is not in use in MapLibre GL JS).", - "example": { - "syntax": { - "method": ["string"], - "result": "boolean" - }, - "value": ["is-supported-script", "दिल्ली"] + "doc": "Returns `true` if the input string is expected to render legibly. Returns `false` if the input string contains sections that cannot be rendered without potential loss of meaning (e.g. Indic scripts that require complex text shaping, or right-to-left scripts if the `mapbox-gl-rtl-text` plugin is not in use in MapLibre GL JS).", + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "boolean" + } + ], + "parameters": [ + { + "name": "input", + "type": "string" + } + ] }, + "example": ["is-supported-script", "दिल्ली"], "group": "String", "sdk-support": { "basic functionality": { "js": "0.45.0", - "android": "6.6.0" + "android": "6.6.0", + "ios": "supported" } } }, "upcase": { "doc": "Returns the input string converted to uppercase. Follows the Unicode Default Case Conversion algorithm and the locale-insensitive case mappings in the Unicode Character Database.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)", - "example": { - "syntax": { - "method": ["string"], - "result": "string" - }, - "value": ["upcase", ["get", "name"]] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "string" + } + ], + "parameters": [ + { + "name": "input", + "type": "string" + } + ] }, + "example": ["upcase", ["get", "name"]], "group": "String", "sdk-support": { "basic functionality": { @@ -4198,13 +5380,21 @@ }, "downcase": { "doc": "Returns the input string converted to lowercase. Follows the Unicode Default Case Conversion algorithm and the locale-insensitive case mappings in the Unicode Character Database.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)", - "example": { - "syntax": { - "method": ["string"], - "result": "string" - }, - "value": ["downcase", ["get", "name"]] + "syntax": { + "overloads": [ + { + "parameters": ["input"], + "output-type": "string" + } + ], + "parameters": [ + { + "name": "input", + "type": "string" + } + ] }, + "example": ["downcase", ["get", "name"]], "group": "String", "sdk-support": { "basic functionality": { @@ -4215,14 +5405,22 @@ } }, "concat": { - "doc": "Returns a `string` consisting of the concatenation of the inputs. Each input is converted to a string as if by `to-string`.\n\n - [Add a generated icon to the map](https://maplibre.org/maplibre-gl-js/docs/examples/add-image-missing-generated/)\n - [Create a time slider](https://maplibre.org/maplibre-gl-js/docs/examples/timeline-animation/)\n - [Use a fallback image](https://maplibre.org/maplibre-gl-js/docs/examples/fallback-image/)\n - [Variable label placement](https://maplibre.org/maplibre-gl-js/docs/examples/variable-label-placement/)", - "example": { - "syntax": { - "method": ["string", "string", "..."], - "result": "string" - }, - "value": ["concat", "square-rgb-", ["get", "color"]] + "doc": "Returns a `string` consisting of the concatenation of the inputs. Each input is converted to a string as if by `to-string`.\n\n - [Add a generated icon to the map](https://maplibre.org/maplibre-gl-js/docs/examples/add-a-generated-icon-to-the-map/)\n\n - [Create a time slider](https://maplibre.org/maplibre-gl-js/docs/examples/create-a-time-slider/)\n\n - [Use a fallback image](https://maplibre.org/maplibre-gl-js/docs/examples/fallback-image/)\n\n - [Variable label placement](https://maplibre.org/maplibre-gl-js/docs/examples/variable-label-placement/)", + "syntax": { + "overloads": [ + { + "parameters": ["input_1", "...", "input_n"], + "output-type": "string" + } + ], + "parameters": [ + { + "name": "input_i", + "type": "any" + } + ] }, + "example": ["concat", "square-rgb-", ["get", "color"]], "group": "String", "sdk-support": { "basic functionality": { @@ -4234,16 +5432,24 @@ }, "resolved-locale": { "doc": "Returns the IETF language tag of the locale being used by the provided `collator`. This can be used to determine the default system locale, or to determine if a requested locale was successfully loaded.", - "example": { - "syntax": { - "method": ["collator"], - "result": "string" - }, - "value": ["resolved-locale", [ - "collator", - {"case-sensitive": true, "diacritic-sensitive": false, "locale": "de"} - ]] + "syntax": { + "overloads": [ + { + "parameters": ["collator"], + "output-type": "string" + } + ], + "parameters": [ + { + "name": "collator", + "type": "collator" + } + ] }, + "example": ["resolved-locale", [ + "collator", + {"case-sensitive": true, "diacritic-sensitive": false, "locale": "de"} + ]], "group": "String", "sdk-support": { "basic functionality": { @@ -4442,7 +5648,7 @@ ] }, "transition": true, - "doc": "How to blend the the sky color and the horizon color. Where 1 is blending the color at the middle of the sky and 0 is not blending at all and using the sky color only." + "doc": "How to blend the sky color and the horizon color. Where 1 is blending the color at the middle of the sky and 0 is not blending at all and using the sky color only." }, "atmosphere-blend": { "type": "number", @@ -4467,7 +5673,9 @@ "required": true, "sdk-support": { "basic functionality": { - "js": "2.2.0" + "js": "2.2.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/252", + "android": "https://github.com/maplibre/maplibre-native/issues/252" } } }, @@ -4478,23 +5686,25 @@ "default": 1.0, "sdk-support": { "basic functionality": { - "js": "2.2.0" + "js": "2.2.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/252", + "android": "https://github.com/maplibre/maplibre-native/issues/252" } } } }, "projection": { "type": { - "type": "enum", - "doc": "The projection type.", + "type": "projectionDefinition", + "doc": "The projection definition type. Can be specified as a string, a transition state, or an expression.", "default": "mercator", - "values": { - "mercator": { - "doc": "The Mercator projection." - }, - "globe": { - "doc": "The globe projection." - } + "property-type": "data-constant", + "transition": false, + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] } } }, @@ -4507,6 +5717,7 @@ "paint_symbol", "paint_raster", "paint_hillshade", + "paint_color-relief", "paint_background" ], "paint_fill": { @@ -4911,7 +6122,8 @@ "sdk-support": { "basic functionality": { "js": "0.50.0", - "ios": "4.7.0" + "ios": "4.7.0", + "android": "7.0.0" } }, "expression": { @@ -5795,7 +7007,7 @@ "minimum": 0, "transition": true, "units": "pixels", - "doc": "Distance of halo to the icon outline. \nThe unit is in pixels only for SDF sprites that were created with a blur radius of 8, multiplied by the display density. I.e., the radius needs to be 16 for `@2x` sprites, etc.", + "doc": "Distance of halo to the icon outline. \n\nThe unit is in pixels only for SDF sprites that were created with a blur radius of 8, multiplied by the display density. I.e., the radius needs to be 16 for `@2x` sprites, etc.", "requires": [ "icon-image" ], @@ -6316,17 +7528,49 @@ }, "paint_hillshade": { "hillshade-illumination-direction": { - "type": "number", + "type": "numberArray", "default": 335, "minimum": 0, "maximum": 359, - "doc": "The direction of the light source used to generate the hillshading with 0 as the top of the viewport if `hillshade-illumination-anchor` is set to `viewport` and due north if `hillshade-illumination-anchor` is set to `map`.", + "doc": "The direction of the light source(s) used to generate the hillshading with 0 as the top of the viewport if `hillshade-illumination-anchor` is set to `viewport` and due north if `hillshade-illumination-anchor` is set to `map`. Only when `hillshade-method` is set to `multidirectional` can you specify multiple light sources.", "transition": false, "sdk-support": { "basic functionality": { "js": "0.43.0", "android": "6.0.0", "ios": "4.0.0" + }, + "multidirectional": { + "js": "5.5.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3396", + "ios": "https://github.com/maplibre/maplibre-native/issues/3396" + } + }, + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] + }, + "property-type": "data-constant" + }, + "hillshade-illumination-altitude": { + "type": "numberArray", + "default": 45, + "minimum": 0, + "maximum": 90, + "doc": "The altitude of the light source(s) used to generate the hillshading with 0 as sunset and 90 as noon. Only when `hillshade-method` is set to `multidirectional` can you specify multiple light sources.", + "transition": false, + "sdk-support": { + "basic functionality": { + "js": "5.5.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3396", + "ios": "https://github.com/maplibre/maplibre-native/issues/3396" + }, + "multidirectional": { + "js": "5.5.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3396", + "ios": "https://github.com/maplibre/maplibre-native/issues/3396" } }, "expression": { @@ -6387,15 +7631,20 @@ "property-type": "data-constant" }, "hillshade-shadow-color": { - "type": "color", + "type": "colorArray", "default": "#000000", - "doc": "The shading color of areas that face away from the light source.", + "doc": "The shading color of areas that face away from the light source(s). Only when `hillshade-method` is set to `multidirectional` can you specify multiple light sources.", "transition": true, "sdk-support": { "basic functionality": { "js": "0.43.0", "android": "6.0.0", "ios": "4.0.0" + }, + "multidirectional": { + "js": "5.5.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3396", + "ios": "https://github.com/maplibre/maplibre-native/issues/3396" } }, "expression": { @@ -6407,15 +7656,20 @@ "property-type": "data-constant" }, "hillshade-highlight-color": { - "type": "color", + "type": "colorArray", "default": "#FFFFFF", - "doc": "The shading color of areas that faces towards the light source.", + "doc": "The shading color of areas that faces towards the light source(s). Only when `hillshade-method` is set to `multidirectional` can you specify multiple light sources.", "transition": true, "sdk-support": { "basic functionality": { "js": "0.43.0", "android": "6.0.0", "ios": "4.0.0" + }, + "multidirectional": { + "js": "5.5.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3396", + "ios": "https://github.com/maplibre/maplibre-native/issues/3396" } }, "expression": { @@ -6445,6 +7699,93 @@ ] }, "property-type": "data-constant" + }, + "hillshade-method": { + "type": "enum", + "values": { + "standard": { + "doc": "The legacy hillshade method." + }, + "basic": { + "doc": "Basic hillshade. Uses a simple physics model where the reflected light intensity is proportional to the cosine of the angle between the incident light and the surface normal. Similar to GDAL's `gdaldem` default algorithm." + }, + "combined": { + "doc": "Hillshade algorithm whose intensity scales with slope. Similar to GDAL's `gdaldem` with `-combined` option." + }, + "igor": { + "doc": "Hillshade algorithm which tries to minimize effects on other map features beneath. Similar to GDAL's `gdaldem` with `-igor` option." + }, + "multidirectional": { + "doc": "Hillshade with multiple illumination directions. Uses the basic hillshade model with multiple independent light sources." + } + }, + "default": "standard", + "doc": "The hillshade algorithm to use, one of `standard`, `basic`, `combined`, `igor`, or `multidirectional`. ![image](assets/hillshade_methods.png)", + "sdk-support": { + "basic functionality": { + "js": "5.5.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3396", + "ios": "https://github.com/maplibre/maplibre-native/issues/3396" + } + }, + "expression": { + "interpolated": false, + "parameters": [ + "zoom" + ] + }, + "property-type": "data-constant" + } + }, + "paint_color-relief": { + "color-relief-opacity": { + "type": "number", + "default": 1, + "minimum": 0, + "maximum": 1, + "doc": "The opacity at which the color-relief will be drawn.", + "transition": true, + "sdk-support": { + "basic functionality": { + "js": "5.6.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3408", + "ios": "https://github.com/maplibre/maplibre-native/issues/3408" + } + }, + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] + }, + "property-type": "data-constant" + }, + "color-relief-color": { + "type": "color", + "doc": "Defines the color of each pixel based on its elevation. Should be an expression that uses `[\"elevation\"]` as input.", + "example": [ + "interpolate", + ["linear"], + ["elevation"], + 0, "black", + 8849, "white" + ], + "transition": false, + "sdk-support": { + "basic functionality": { + "js": "5.6.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3408", + "ios": "https://github.com/maplibre/maplibre-native/issues/3408" + }, + "data-driven styling": {} + }, + "expression": { + "interpolated": true, + "parameters": [ + "elevation" + ] + }, + "property-type": "color-ramp" } }, "paint_background": { diff --git a/shaders/color_relief.fragment.glsl b/shaders/color_relief.fragment.glsl new file mode 100644 index 000000000000..2012b4a4011b --- /dev/null +++ b/shaders/color_relief.fragment.glsl @@ -0,0 +1,75 @@ +layout(std140) uniform ColorReliefDrawableUBO { + highp mat4 u_matrix; +}; + +layout(std140) uniform ColorReliefTilePropsUBO { + highp vec4 u_unpack; + highp vec2 u_dimension; + int u_color_ramp_size; + float pad_tile0; +}; + +layout(std140) uniform ColorReliefEvaluatedPropsUBO { + float u_opacity; + float pad_eval0; + float pad_eval1; + float pad_eval2; +}; + +uniform sampler2D u_image; +uniform sampler2D u_elevation_stops; +uniform sampler2D u_color_stops; + +in vec2 v_pos; + +float getElevation(vec2 coord) { + // Convert encoded elevation value to meters + vec4 data = texture(u_image, coord) * 255.0; + data.a = -1.0; + return dot(data, u_unpack); +} + +float getElevationStop(int stop) { + // Elevation stops are plain float values, not terrain-RGB encoded + float x = (float(stop) + 0.5) / float(u_color_ramp_size); + return texture(u_elevation_stops, vec2(x, 0.0)).r; +} + +vec4 getColorStop(int stop) { + float x = (float(stop) + 0.5) / float(u_color_ramp_size); + return texture(u_color_stops, vec2(x, 0.0)); +} + +void main() { + float el = getElevation(v_pos); + + // Binary search for color stops + int r = (u_color_ramp_size - 1); + int l = 0; + + while (r - l > 1) { + int m = (r + l) / 2; + float el_m = getElevationStop(m); + if (el < el_m) { + r = m; + } else { + l = m; + } + } + + // Get elevation values for interpolation + float el_l = getElevationStop(l); + float el_r = getElevationStop(r); + + // Get colors for interpolation + vec4 color_l = getColorStop(l); + vec4 color_r = getColorStop(r); + + // Interpolate between the two colors + float t = clamp((el - el_l) / (el_r - el_l), 0.0, 1.0); + fragColor = u_opacity * mix(color_l, color_r, t); + +#ifdef OVERDRAW_INSPECTOR + fragColor = vec4(1.0); +#endif +} \ No newline at end of file diff --git a/shaders/color_relief.vertex.glsl b/shaders/color_relief.vertex.glsl new file mode 100644 index 000000000000..5b6d46e15a45 --- /dev/null +++ b/shaders/color_relief.vertex.glsl @@ -0,0 +1,25 @@ +layout(std140) uniform ColorReliefDrawableUBO { + highp mat4 u_matrix; +}; + +layout(std140) uniform ColorReliefTilePropsUBO { + highp vec4 u_unpack; + highp vec2 u_dimension; + int u_color_ramp_size; + float pad_tile0; +}; + +in vec2 a_pos; +out vec2 v_pos; + +void main() { + gl_Position = u_matrix * vec4(a_pos, 0, 1); + + highp vec2 epsilon = 1.0 / u_dimension; + float scale = (u_dimension.x - 2.0) / u_dimension.x; + v_pos = (a_pos / 8192.0) * scale + epsilon; + + // Handle poles + if (a_pos.y < -32767.5) v_pos.y = 0.0; + if (a_pos.y > 32766.5) v_pos.y = 1.0; +} diff --git a/shaders/hillshade.fragment.glsl b/shaders/hillshade.fragment.glsl index 499aa3c67d0f..7981c59bf154 100644 --- a/shaders/hillshade.fragment.glsl +++ b/shaders/hillshade.fragment.glsl @@ -1,55 +1,183 @@ in vec2 v_pos; uniform sampler2D u_image; -layout (std140) uniform HillshadeTilePropsUBO { +layout(std140) uniform HillshadeTilePropsUBO { highp vec2 u_latrange; - highp vec2 u_light; + highp float u_exaggeration; + highp int u_method; // Hillshade method (0: STANDARD, 1: COMBINED, 2: IGOR, 3: MULTIDIRECTIONAL, 4: BASIC) + highp int u_num_lights; // Number of light sources (1-4) + highp float u_pad0; + highp float u_pad1; + highp float u_pad2; }; - -layout (std140) uniform HillshadeEvaluatedPropsUBO { - highp vec4 u_highlight; - highp vec4 u_shadow; +layout(std140) uniform HillshadeEvaluatedPropsUBO { highp vec4 u_accent; + highp vec4 u_altitudes; // Up to 4 altitude values (in radians) + highp vec4 u_azimuths; // Up to 4 azimuth values (in radians) + highp vec4 u_shadows[4]; // Shadow colors (up to 4 lights) + highp vec4 u_highlights[4]; // Highlight colors (up to 4 lights) }; #define PI 3.141592653589793 -void main() { - vec4 pixel = texture(u_image, v_pos); +#define STANDARD 0 +#define COMBINED 1 +#define IGOR 2 +#define MULTIDIRECTIONAL 3 +#define BASIC 4 - vec2 deriv = ((pixel.rg * 2.0) - 1.0); +float get_aspect(vec2 deriv) { + return deriv.x != 0.0 ? + atan(deriv.y, -deriv.x) : PI / 2.0 * (deriv.y > 0.0 ? 1.0 : -1.0); +} - // We divide the slope by a scale factor based on the cosin of the pixel's approximate latitude - // to account for mercator projection distortion. see #4807 for details - float scaleFactor = cos(radians((u_latrange[0] - u_latrange[1]) * (1.0 - v_pos.y) + u_latrange[1])); - // We also multiply the slope by an arbitrary z-factor of 1.25 - float slope = atan(1.25 * length(deriv) / scaleFactor); - float aspect = deriv.x != 0.0 ? atan(deriv.y, -deriv.x) : PI / 2.0 * (deriv.y > 0.0 ? 1.0 : -1.0); - - float intensity = u_light.x; - // We add PI to make this property match the global light object, which adds PI/2 to the light's azimuthal - // position property to account for 0deg corresponding to north/the top of the viewport in the style spec - // and the original shader was written to accept (-illuminationDirection - 90) as the azimuthal. - float azimuth = u_light.y + PI; - - // We scale the slope exponentially based on intensity, using a calculation similar to - // the exponential interpolation function in the style spec: - // https://github.com/mapbox/mapbox-gl-js/blob/master/src/style-spec/expression/definitions/interpolate.js#L217-L228 - // so that higher intensity values create more opaque hillshading. +// MapLibre's legacy hillshade algorithm (Method 0) +void standard_hillshade(vec2 deriv) { + float azimuth = u_azimuths.x + PI; + float slope = atan(0.625 * length(deriv)); + float aspect = get_aspect(deriv); + + // Note: This implementation uses u_exaggeration as intensity, though it typically should be derived from u_altitudes.x. + float intensity = u_exaggeration; + + // Scale the slope exponentially based on intensity float base = 1.875 - intensity * 1.75; float maxValue = 0.5 * PI; float scaledSlope = intensity != 0.5 ? ((pow(base, slope) - 1.0) / (pow(base, maxValue) - 1.0)) * maxValue : slope; - - // The accent color is calculated with the cosine of the slope while the shade color is calculated with the sine - // so that the accent color's rate of change eases in while the shade color's eases out. + float accent = cos(scaledSlope); - // We multiply both the accent and shade color by a clamped intensity value - // so that intensities >= 0.5 do not additionally affect the color values - // while intensity values < 0.5 make the overall color more transparent. vec4 accent_color = (1.0 - accent) * u_accent * clamp(intensity * 2.0, 0.0, 1.0); + + // Calculate shadow/highlight based on aspect float shade = abs(mod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); - vec4 shade_color = mix(u_shadow, u_highlight, shade) * sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0); + vec4 shade_color = mix(u_shadows[0], u_highlights[0], shade) * sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0); + fragColor = accent_color * (1.0 - shade_color.a) + shade_color; +} + +// Basic directional hillshade (Method 4) +void basic_hillshade(vec2 deriv) { + deriv = deriv * u_exaggeration * 2.0; // Exaggerate the slope derivative + float azimuth = u_azimuths.x + PI; + float cos_az = cos(azimuth); + float sin_az = sin(azimuth); + float cos_alt = cos(u_altitudes.x); + float sin_alt = sin(u_altitudes.x); + + // Calculate the cosine of the angle between the light vector and the surface normal + float cang = (sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / sqrt(1.0 + dot(deriv, deriv)); + float shade = clamp(cang, 0.0, 1.0); // cang is the hillshade intensity [0, 1] + + // Blend shadow and highlight based on intensity + if (shade > 0.5) { + fragColor = u_highlights[0] * (2.0 * shade - 1.0); // Highlight strength [0, 1] + } else { + fragColor = u_shadows[0] * (1.0 - 2.0 * shade); // Shadow strength [0, 1] + } +} + +// Multidirectional hillshade (Method 3) +void multidirectional_hillshade(vec2 deriv) { + deriv = deriv * u_exaggeration * 2.0; // Exaggerate the slope derivative + fragColor = vec4(0, 0, 0, 0); + + // Iterate through all light sources (up to u_num_lights, max 4) + for (int i = 0; i < u_num_lights; i++) { + // Access altitude and azimuth from vec4 UBOs + float altitude = (i == 0) ? u_altitudes.x : (i == 1) ? u_altitudes.y : (i == 2) ? u_altitudes.z : u_altitudes.w; + float azimuth = (i == 0) ? u_azimuths.x : (i == 1) ? u_azimuths.y : (i == 2) ? u_azimuths.z : u_azimuths.w; + + float cos_alt = cos(altitude); + float sin_alt = sin(altitude); + // Negate cos/sin azimuth for correct light direction in the normal calculation + float cos_az = -cos(azimuth); + float sin_az = -sin(azimuth); + + // Calculate the cosine of the angle between the light vector and the surface normal + float cang = (sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / + sqrt(1.0 + dot(deriv, deriv)); + float shade = clamp(cang, 0.0, 1.0); // cang is the hillshade intensity [0, 1] + + // Accumulate shadow/highlight contribution from each light + if (shade > 0.5) { + fragColor += u_highlights[i] * (2.0 * shade - 1.0) / float(u_num_lights); + } else { + fragColor += u_shadows[i] * (1.0 - 2.0 * shade) / float(u_num_lights); + } + } +} + +// Combined shadow and highlight method (Method 1) +void combined_hillshade(vec2 deriv) { + // Only supports one light source (index 0) + deriv = deriv * u_exaggeration * 2.0; + float azimuth = u_azimuths.x + PI; + float cos_az = cos(azimuth); + float sin_az = sin(azimuth); + float cos_alt = cos(u_altitudes.x); + float sin_alt = sin(u_altitudes.x); + + // Calculate the angle between the light vector and the surface normal + float cang = acos((sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / + sqrt(1.0 + dot(deriv, deriv))); + + cang = clamp(cang, 0.0, PI / 2.0); // Clamp angle to 90 degrees (half hemisphere) + + // The "shade" and "highlight" components are calculated from cang (angle) and the magnitude of the slope + float shade = cang * atan(length(deriv)) * 4.0 / PI / PI; + float highlight = (PI / 2.0 - cang) * atan(length(deriv)) * 4.0 / PI / PI; + + fragColor = u_shadows[0] * shade + u_highlights[0] * highlight; +} + +// Igor's shadow/highlight method (Method 2) +void igor_hillshade(vec2 deriv) { + // Only supports one light source (index 0) + deriv = deriv * u_exaggeration * 2.0; + float aspect = get_aspect(deriv); + float azimuth = u_azimuths.x + PI; + + // Slope strength is magnitude of slope vector, normalized to [0, 1] + float slope_strength = atan(length(deriv)) * 2.0 / PI; + + // Aspect strength is difference between aspect and light azimuth, normalized to [0, 1] + float aspect_strength = 1.0 - abs(mod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); + + float shadow_strength = slope_strength * aspect_strength; + float highlight_strength = slope_strength * (1.0 - aspect_strength); + + fragColor = u_shadows[0] * shadow_strength + u_highlights[0] * highlight_strength; +} + +void main() { + vec4 pixel = texture(u_image, v_pos); + + // Scale the derivative based on the mercator distortion at this latitude (v_pos.y) + float scaleFactor = cos(radians((u_latrange[0] - u_latrange[1]) * (1.0 - v_pos.y) + u_latrange[1])); + + // The derivative is scaled back from [0, 1] texture range to world-space slope + // Texture range [0, 1] corresponds to slope range [-4, 4] (8.0 * 0.5 * deriv) + vec2 deriv = ((pixel.rg * 8.0) - 4.0) / scaleFactor; + + // Dispatch to the selected hillshade method + switch (u_method) { + case BASIC: + basic_hillshade(deriv); + break; + case COMBINED: + combined_hillshade(deriv); + break; + case IGOR: + igor_hillshade(deriv); + break; + case MULTIDIRECTIONAL: + multidirectional_hillshade(deriv); + break; + case STANDARD: + default: + standard_hillshade(deriv); + break; + } #ifdef OVERDRAW_INSPECTOR fragColor = vec4(1.0); diff --git a/shaders/hillshade_prepare.fragment.glsl b/shaders/hillshade_prepare.fragment.glsl index 9f7988750092..f5fc7f38fb09 100644 --- a/shaders/hillshade_prepare.fragment.glsl +++ b/shaders/hillshade_prepare.fragment.glsl @@ -4,7 +4,6 @@ precision highp float; in vec2 v_pos; uniform sampler2D u_image; - layout (std140) uniform HillshadePrepareTilePropsUBO { highp vec4 u_unpack; highp vec2 u_dimension; @@ -16,13 +15,14 @@ float getElevation(vec2 coord, float bias) { // Convert encoded elevation value to meters vec4 data = texture(u_image, coord) * 255.0; data.a = -1.0; - return dot(data, u_unpack) / 4.0; + return dot(data, u_unpack); } void main() { vec2 epsilon = 1.0 / u_dimension; + float tileSize = u_dimension.x - 2.0; - // queried pixels: + // queried pixels (using Sobel operator kernel): // +-----------+ // | | | | // | a | b | c | @@ -46,31 +46,28 @@ void main() { float g = getElevation(v_pos + vec2(-epsilon.x, epsilon.y), 0.0); float h = getElevation(v_pos + vec2(0, epsilon.y), 0.0); float i = getElevation(v_pos + vec2(epsilon.x, epsilon.y), 0.0); - - // here we divide the x and y slopes by 8 * pixel size - // where pixel size (aka meters/pixel) is: - // circumference of the world / (pixels per tile * number of tiles) - // which is equivalent to: 8 * 40075016.6855785 / (512 * pow(2, u_zoom)) - // which can be reduced to: pow(2, 19.25619978527 - u_zoom) - // we want to vertically exaggerate the hillshading though, because otherwise - // it is barely noticeable at low zooms. to do this, we multiply this by some - // scale factor pow(2, (u_zoom - u_maxzoom) * a) where a is an arbitrary value - // Here we use a=0.3 which works out to the expression below. see - // nickidlugash's awesome breakdown for more info + + // Convert the raw pixel-space derivative (slope) into world-space slope. + // The conversion factor is: tileSize / (8 * meters_per_pixel). + // meters_per_pixel is calculated as pow(2.0, 28.2562 - u_zoom). + // The exaggeration factor is applied to scale the effect at lower zooms. + // See nickidlugash's awesome breakdown for more info // https://github.com/mapbox/mapbox-gl-js/pull/5286#discussion_r148419556 float exaggeration = u_zoom < 2.0 ? 0.4 : u_zoom < 4.5 ? 0.35 : 0.3; vec2 deriv = vec2( (c + f + f + i) - (a + d + d + g), (g + h + h + i) - (a + b + b + c) - ) / pow(2.0, (u_zoom - u_maxzoom) * exaggeration + 19.2562 - u_zoom); - + ) * tileSize / pow(2.0, (u_zoom - u_maxzoom) * exaggeration + 28.2562 - u_zoom); + + // Encode the derivative into the color channels (r and g) + // The derivative is scaled from world-space slope to the range [0, 1] for texture storage. + // The maximum possible world-space derivative is assumed to be 4 (hence division by 8.0). fragColor = clamp(vec4( - deriv.x / 2.0 + 0.5, - deriv.y / 2.0 + 0.5, + deriv.x / 8.0 + 0.5, + deriv.y / 8.0 + 0.5, 1.0, 1.0), 0.0, 1.0); - #ifdef OVERDRAW_INSPECTOR fragColor = vec4(1.0); #endif diff --git a/shaders/manifest.json b/shaders/manifest.json index 8ea7f8c5b608..4303fb829bd8 100644 --- a/shaders/manifest.json +++ b/shaders/manifest.json @@ -137,6 +137,13 @@ "glsl_frag": "hillshade_prepare.fragment.glsl", "uses_ubos": true }, + { + "name": "ColorReliefShader", + "header": "color_relief", + "glsl_vert": "color_relief.vertex.glsl", + "glsl_frag": "color_relief.fragment.glsl", + "uses_ubos": true + }, { "name": "HillshadeShader", "header": "hillshade", diff --git a/src/mbgl/gl/renderer_backend.cpp b/src/mbgl/gl/renderer_backend.cpp index cb93e9d33a1a..3729d6b04e92 100644 --- a/src/mbgl/gl/renderer_backend.cpp +++ b/src/mbgl/gl/renderer_backend.cpp @@ -137,6 +137,7 @@ void RendererBackend::initShaders(gfx::ShaderRegistry& shaders, const ProgramPar shaders::BuiltIn::HeatmapTextureShader, shaders::BuiltIn::HillshadePrepareShader, shaders::BuiltIn::HillshadeShader, + shaders::BuiltIn::ColorReliefShader, shaders::BuiltIn::LineShader, shaders::BuiltIn::LineGradientShader, shaders::BuiltIn::LinePatternShader, diff --git a/src/mbgl/gl/texture2d.cpp b/src/mbgl/gl/texture2d.cpp index a9551dfa8c72..72270b79d6bc 100644 --- a/src/mbgl/gl/texture2d.cpp +++ b/src/mbgl/gl/texture2d.cpp @@ -52,6 +52,8 @@ size_t Texture2D::getPixelStride() const noexcept { return 1 * numChannels(); case gfx::TextureChannelDataType::HalfFloat: return 2 * numChannels(); + case gfx::TextureChannelDataType::Float: + return 4 * numChannels(); default: return 0; } diff --git a/src/mbgl/layermanager/color_relief_layer_factory.cpp b/src/mbgl/layermanager/color_relief_layer_factory.cpp new file mode 100644 index 000000000000..fd5e49b4870d --- /dev/null +++ b/src/mbgl/layermanager/color_relief_layer_factory.cpp @@ -0,0 +1,27 @@ +#include + +#include +#include +#include + +namespace mbgl { + +const style::LayerTypeInfo* ColorReliefLayerFactory::getTypeInfo() const noexcept { + return style::ColorReliefLayer::Impl::staticTypeInfo(); +} + +std::unique_ptr ColorReliefLayerFactory::createLayer(const std::string& id, + const style::conversion::Convertible& value) noexcept { + const auto source = getSource(value); + if (!source) { + return nullptr; + } + return std::unique_ptr(new (std::nothrow) style::ColorReliefLayer(id, *source)); +} + +std::unique_ptr ColorReliefLayerFactory::createRenderLayer(Immutable impl) noexcept { + assert(impl->getTypeInfo() == getTypeInfo()); + return std::make_unique(staticImmutableCast(impl)); +} + +} // namespace mbgl diff --git a/src/mbgl/programs/programs.cpp b/src/mbgl/programs/programs.cpp index cedd07520bfa..2aa093dff0f8 100644 --- a/src/mbgl/programs/programs.cpp +++ b/src/mbgl/programs/programs.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -50,6 +51,7 @@ void Programs::registerWith(gfx::ShaderRegistry& registry) { HeatmapTextureProgram, HillshadeProgram, HillshadePrepareProgram, + ColorReliefProgram, FillProgram, FillPatternProgram, FillOutlineProgram, diff --git a/src/mbgl/renderer/layers/color_relief_layer_tweaker.cpp b/src/mbgl/renderer/layers/color_relief_layer_tweaker.cpp new file mode 100644 index 000000000000..b713703ce638 --- /dev/null +++ b/src/mbgl/renderer/layers/color_relief_layer_tweaker.cpp @@ -0,0 +1,44 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { + +using namespace shaders; + +void ColorReliefLayerTweaker::execute(LayerGroupBase& layerGroup, const PaintParameters& parameters) { + const auto& props = static_cast(*evaluatedProperties); + const auto& evaluated = props.evaluated; + + if (layerGroup.empty()) { + return; + } + + // Update evaluated properties UBO + evaluatedPropsUBO.opacity = evaluated.get(); + + auto& layerUniforms = layerGroup.mutableUniformBuffers(); + layerUniforms.createOrUpdate(idColorReliefEvaluatedPropsUBO, &evaluatedPropsUBO, parameters.context); // Use parameters.context + + visitLayerGroupDrawables(layerGroup, [&](gfx::Drawable& drawable) { + if (!drawable.getTileID()) { + return; + } + + const UnwrappedTileID tileID = drawable.getTileID()->toUnwrapped(); + + drawableUBO.matrix = util::cast(parameters.matrixForTile(tileID)); + + auto& drawableUniforms = drawable.mutableUniformBuffers(); + drawableUniforms.createOrUpdate(idColorReliefDrawableUBO, &drawableUBO, parameters.context); + + // Tile props UBO is set during drawable creation, doesn't change per frame + }); +} + +} // namespace mbgl diff --git a/src/mbgl/renderer/layers/color_relief_layer_tweaker.hpp b/src/mbgl/renderer/layers/color_relief_layer_tweaker.hpp new file mode 100644 index 000000000000..0e464e51a7ba --- /dev/null +++ b/src/mbgl/renderer/layers/color_relief_layer_tweaker.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +namespace mbgl { + +class ColorReliefLayerTweaker : public LayerTweaker { +public: + ColorReliefLayerTweaker(std::string id_, Immutable properties) + : LayerTweaker(std::move(id_), properties) {} + + ~ColorReliefLayerTweaker() override = default; + + void execute(LayerGroupBase&, const PaintParameters&) override; + +private: + shaders::ColorReliefDrawableUBO drawableUBO; + shaders::ColorReliefTilePropsUBO tilePropsUBO; + shaders::ColorReliefEvaluatedPropsUBO evaluatedPropsUBO; +}; + +} // namespace mbgl diff --git a/src/mbgl/renderer/layers/hillshade_layer_tweaker.cpp b/src/mbgl/renderer/layers/hillshade_layer_tweaker.cpp index 4382452b1000..88de1199c292 100644 --- a/src/mbgl/renderer/layers/hillshade_layer_tweaker.cpp +++ b/src/mbgl/renderer/layers/hillshade_layer_tweaker.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include namespace mbgl { @@ -15,20 +17,113 @@ using namespace style; using namespace shaders; namespace { + +// Structure to hold processed illumination properties +struct IlluminationProperties { + std::vector directionRadians; + std::vector altitudeRadians; + std::vector shadowColors; + std::vector highlightColors; + + size_t numSources() const { return directionRadians.size(); } +}; + +// Convert style properties to illumination properties +IlluminationProperties getIlluminationProperties(const HillshadePaintProperties::PossiblyEvaluated& evaluated) { + IlluminationProperties props; + + // Get the values from evaluated properties (these are now vectors) + std::vector directions = evaluated.get(); + std::vector altitudes = evaluated.get(); + std::vector highlights = evaluated.get(); + std::vector shadows = evaluated.get(); + + // Find the maximum length to ensure all arrays are the same size + size_t maxLength = std::max({directions.size(), altitudes.size(), highlights.size(), shadows.size()}); + + // Ensure we don't exceed the maximum supported + maxLength = std::min(maxLength, static_cast(MAX_ILLUMINATION_SOURCES)); + + // Pad shorter arrays by repeating the last element + auto padArray = [maxLength](auto& arr) { + if (arr.empty()) arr.push_back(typename std::decay::type::value_type{}); + while (arr.size() < maxLength) { + arr.push_back(arr.back()); + } + }; + + padArray(directions); + padArray(altitudes); + padArray(highlights); + padArray(shadows); + + // Convert degrees to radians + props.directionRadians.reserve(directions.size()); + for (float deg : directions) { + props.directionRadians.push_back(util::deg2radf(deg)); + } + + props.altitudeRadians.reserve(altitudes.size()); + for (float deg : altitudes) { + props.altitudeRadians.push_back(util::deg2radf(deg)); + } + + props.shadowColors = std::move(shadows); + props.highlightColors = std::move(highlights); + + return props; +} + +// Pack illumination properties into the UBO format +HillshadeEvaluatedPropsUBO packEvaluatedProps(const IlluminationProperties& illumination, const Color& accentColor) { + HillshadeEvaluatedPropsUBO ubo{}; + + // Pack accent color + ubo.accent = {accentColor.r, accentColor.g, accentColor.b, accentColor.a}; + + // Pack altitudes (up to 4) + for (size_t i = 0; i < illumination.altitudeRadians.size() && i < 4; ++i) { + ubo.altitudes[i] = illumination.altitudeRadians[i]; + } + + // Pack azimuths (up to 4) + for (size_t i = 0; i < illumination.directionRadians.size() && i < 4; ++i) { + ubo.azimuths[i] = illumination.directionRadians[i]; + } + + // Pack shadow colors (4 colors * 4 components = 16 floats) + for (size_t i = 0; i < illumination.shadowColors.size() && i < 4; ++i) { + const auto& color = illumination.shadowColors[i]; + ubo.shadows[i * 4 + 0] = color.r; + ubo.shadows[i * 4 + 1] = color.g; + ubo.shadows[i * 4 + 2] = color.b; + ubo.shadows[i * 4 + 3] = color.a; + } + + // Pack highlight colors (4 colors * 4 components = 16 floats) + for (size_t i = 0; i < illumination.highlightColors.size() && i < 4; ++i) { + const auto& color = illumination.highlightColors[i]; + ubo.highlights[i * 4 + 0] = color.r; + ubo.highlights[i * 4 + 1] = color.g; + ubo.highlights[i * 4 + 2] = color.b; + ubo.highlights[i * 4 + 3] = color.a; + } + + return ubo; +} + +// Convert HillshadeMethodType enum to int for shader +int32_t methodToInt(HillshadeMethodType method) { + return static_cast(method); +} + +// Calculate latitude range for tile std::array getLatRange(const UnwrappedTileID& id) { const LatLng latlng0 = LatLng(id); const LatLng latlng1 = LatLng(UnwrappedTileID(id.canonical.z, id.canonical.x, id.canonical.y + 1)); return {{static_cast(latlng0.latitude()), static_cast(latlng1.latitude())}}; } -std::array getLight(const PaintParameters& parameters, - const HillshadePaintProperties::PossiblyEvaluated& evaluated) { - float azimuthal = util::deg2radf(evaluated.get()); - if (evaluated.get() == HillshadeIlluminationAnchorType::Viewport) { - azimuthal = azimuthal - static_cast(parameters.state.getBearing()); - } - return {{evaluated.get(), azimuthal}}; -} } // namespace void HillshadeLayerTweaker::execute(LayerGroupBase& layerGroup, const PaintParameters& parameters) { @@ -43,10 +138,21 @@ void HillshadeLayerTweaker::execute(LayerGroupBase& layerGroup, const PaintParam const auto debugGroup = parameters.encoder->createDebugGroup(label.c_str()); #endif + // Get illumination properties + auto illumination = getIlluminationProperties(evaluated); + + // Adjust azimuths if anchor is viewport + if (evaluated.get() == HillshadeIlluminationAnchorType::Viewport) { + float bearing = static_cast(parameters.state.getBearing()); + for (auto& azimuth : illumination.directionRadians) { + azimuth -= bearing; + } + } + if (!evaluatedPropsUniformBuffer || propertiesUpdated) { - const HillshadeEvaluatedPropsUBO evaluatedPropsUBO = {.highlight = evaluated.get(), - .shadow = evaluated.get(), - .accent = evaluated.get()}; + const HillshadeEvaluatedPropsUBO evaluatedPropsUBO = packEvaluatedProps(illumination, + evaluated.get()); + parameters.context.emplaceOrUpdateUniformBuffer(evaluatedPropsUniformBuffer, &evaluatedPropsUBO); propertiesUpdated = false; } @@ -83,7 +189,12 @@ void HillshadeLayerTweaker::execute(LayerGroupBase& layerGroup, const PaintParam const HillshadeTilePropsUBO tilePropsUBO = { #endif .latrange = getLatRange(tileID), - .light = getLight(parameters, evaluated) + .exaggeration = evaluated.get(), + .method = methodToInt(evaluated.get()), + .num_lights = static_cast(illumination.numSources()), + .pad0 = 0.0f, + .pad1 = 0.0f, + .pad2 = 0.0f }; #if MLN_UBO_CONSOLIDATION diff --git a/src/mbgl/renderer/layers/render_color_relief_layer.cpp b/src/mbgl/renderer/layers/render_color_relief_layer.cpp new file mode 100644 index 000000000000..df11c5e573c6 --- /dev/null +++ b/src/mbgl/renderer/layers/render_color_relief_layer.cpp @@ -0,0 +1,405 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { + +using namespace style; +using namespace shaders; + +namespace { + +inline const ColorReliefLayer::Impl& impl_cast(const Immutable& impl) { + assert(impl->getTypeInfo() == ColorReliefLayer::Impl::staticTypeInfo()); + return static_cast(*impl); +} + +} // namespace + +RenderColorReliefLayer::RenderColorReliefLayer(Immutable _impl) + : RenderLayer(makeMutable(std::move(_impl))), + unevaluated(impl_cast(baseImpl).paint.untransitioned()) { + styleDependencies = unevaluated.getDependencies(); + + // Initialize color ramp data + colorRampSize = 256; + elevationStopsData = std::make_shared>(colorRampSize); + colorStops = std::make_shared(Size{colorRampSize, 1}); +} + +RenderColorReliefLayer::~RenderColorReliefLayer() = default; + +void RenderColorReliefLayer::transition(const TransitionParameters& parameters) { + unevaluated = impl_cast(baseImpl).paint.transitioned(parameters, std::move(unevaluated)); + styleDependencies = unevaluated.getDependencies(); + updateColorRamp(); +} + +void RenderColorReliefLayer::evaluate(const PropertyEvaluationParameters& parameters) { + const auto previousProperties = staticImmutableCast(evaluatedProperties); + auto properties = makeMutable( + staticImmutableCast(baseImpl), + unevaluated.evaluate(parameters, previousProperties->evaluated)); + + passes = (properties->evaluated.get() > 0) ? RenderPass::Translucent : RenderPass::None; + properties->renderPasses = mbgl::underlying_type(passes); + evaluatedProperties = std::move(properties); + + if (layerTweaker) { + layerTweaker->updateProperties(evaluatedProperties); + } +} + +bool RenderColorReliefLayer::hasTransition() const { + return unevaluated.hasTransition(); +} + +bool RenderColorReliefLayer::hasCrossfade() const { + return false; +} + +void RenderColorReliefLayer::prepare(const LayerPrepareParameters& params) { + renderTiles = params.source->getRenderTiles(); + updateRenderTileIDs(); +} + +void RenderColorReliefLayer::updateColorRamp() { + // Ensure data structures are initialized + if (!elevationStopsData || !colorStops) { + return; + } + + // Get the color property value + auto colorValue = unevaluated.get().getValue(); + if (colorValue.isUndefined()) { + return; + } + + std::vector elevationStopsVector; + std::vector colorStopsVector; + + // Get the expression from ColorRampPropertyValue + const mbgl::style::expression::Expression& expr = colorValue.getExpression(); + + if (expr.getKind() == mbgl::style::expression::Kind::Interpolate) { + const auto* interpolate = static_cast(&expr); + + size_t stopCount = interpolate->getStopCount(); + + elevationStopsVector.reserve(stopCount); + colorStopsVector.reserve(stopCount); + + // Extract elevation values from stops + interpolate->eachStop([&](double elevation, const mbgl::style::expression::Expression& /*outputExpr*/) { + elevationStopsVector.push_back(static_cast(elevation)); + }); + + // Evaluate expression at each elevation to get colors + for (float elevation : elevationStopsVector) { + mbgl::style::expression::EvaluationContext context; + context.withElevation(elevation); + + expression::EvaluationResult result = expr.evaluate(context); + + Color color = {0.0f, 0.0f, 0.0f, 0.0f}; // Default to transparent black + + if (result && result->is()) { + color = result->get(); + } + + colorStopsVector.push_back(color); + } + + } else { + // Fallback: Sample the color ramp uniformly + const uint32_t numSamples = 256; + const float minElevation = -500.0f; + const float maxElevation = 9000.0f; + + elevationStopsVector.reserve(numSamples); + colorStopsVector.reserve(numSamples); + + for (uint32_t i = 0; i < numSamples; ++i) { + float t = static_cast(i) / static_cast(numSamples - 1); + float elevation = minElevation + t * (maxElevation - minElevation); + elevationStopsVector.push_back(elevation); + + Color color = colorValue.evaluate(static_cast(elevation)); + colorStopsVector.push_back(color); + } + } + + const uint32_t rampSize = elevationStopsVector.size(); + if (rampSize == 0) { + return; + } + + // Resize and prepare structures + elevationStopsData->resize(rampSize * 4); // RGBA float for compatibility + colorStops->resize({rampSize, 1}); + this->colorRampSize = rampSize; + + for (uint32_t i = 0; i < rampSize; ++i) { + // Store elevation in the R channel of an RGBA float vector + (*elevationStopsData)[i*4 + 0] = elevationStopsVector[i]; // R = elevation + (*elevationStopsData)[i*4 + 1] = 0.0f; // G = unused + (*elevationStopsData)[i*4 + 2] = 0.0f; // B = unused + (*elevationStopsData)[i*4 + 3] = 1.0f; // A = unused + + // Store colors without premultiplication for proper interpolation + Color color = colorStopsVector[i]; + colorStops->data[i * 4 + 0] = static_cast(color.r * 255.0f); + colorStops->data[i * 4 + 1] = static_cast(color.g * 255.0f); + colorStops->data[i * 4 + 2] = static_cast(color.b * 255.0f); + colorStops->data[i * 4 + 3] = static_cast(color.a * 255.0f); + } + + colorRampChanged = true; +} + +static const std::string ColorReliefShaderGroupName = "ColorReliefShader"; + +void RenderColorReliefLayer::update(gfx::ShaderRegistry& shaders, + gfx::Context& context, + const TransformState&, + const std::shared_ptr&, + const RenderTree&, + UniqueChangeRequestVec& changes) { + if (!renderTiles || renderTiles->empty()) { + removeAllDrawables(); + return; + } + + // Set up layer group + if (!layerGroup) { + if (auto layerGroup_ = context.createTileLayerGroup(layerIndex, /*initialCapacity=*/64, getID())) { + setLayerGroup(std::move(layerGroup_), changes); + } else { + return; + } + } + + auto* tileLayerGroup = static_cast(layerGroup.get()); + + if (!layerTweaker) { + layerTweaker = std::make_shared(getID(), evaluatedProperties); + layerGroup->addLayerTweaker(layerTweaker); + } + + if (!colorReliefShader) { + colorReliefShader = context.getGenericShader(shaders, ColorReliefShaderGroupName); + } + + if (!colorReliefShader) { + removeAllDrawables(); + return; + } + + auto renderPass = RenderPass::Translucent; + if (!(mbgl::underlying_type(renderPass) & evaluatedProperties->renderPasses)) { + return; + } + + stats.drawablesRemoved += tileLayerGroup->removeDrawablesIf( + [&](gfx::Drawable& drawable) { return drawable.getTileID() && !hasRenderTile(*drawable.getTileID()); }); + + if (!staticDataSharedVertices) { + staticDataSharedVertices = std::make_shared(RenderStaticData::rasterVertices()); + } + const auto staticDataIndices = RenderStaticData::quadTriangleIndices(); + const auto staticDataSegments = RenderStaticData::rasterSegments(); + + // Update color ramp textures if changed + if (colorRampChanged && elevationStopsData && colorStops) { + if (!elevationStopsTexture) { + elevationStopsTexture = context.createTexture2D(); + } + + // Use RGBA32F instead of R32F for llvmpipe compatibility + elevationStopsTexture->setFormat(gfx::TexturePixelType::RGBA, gfx::TextureChannelDataType::Float); + elevationStopsTexture->upload(elevationStopsData->data(), Size{colorRampSize, 1}); + elevationStopsTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest, + .wrapU = gfx::TextureWrapType::Clamp, + .wrapV = gfx::TextureWrapType::Clamp}); + + if (!colorStopsTexture) { + colorStopsTexture = context.createTexture2D(); + } + + colorStopsTexture->setImage(colorStops); + colorStopsTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Linear, + .wrapU = gfx::TextureWrapType::Clamp, + .wrapV = gfx::TextureWrapType::Clamp}); + + colorRampChanged = false; + } + + std::unique_ptr builder; + + for (const RenderTile& tile : *renderTiles) { + const auto& tileID = tile.getOverscaledTileID(); + + auto* bucket_ = tile.getBucket(*baseImpl); + if (!bucket_ || !bucket_->hasData()) { + removeTile(renderPass, tileID); + continue; + } + + auto& bucket = static_cast(*bucket_); + + const auto prevBucketID = getRenderTileBucketID(tileID); + if (prevBucketID != util::SimpleIdentity::Empty && prevBucketID != bucket.getID()) { + removeTile(renderPass, tileID); + } + setRenderTileBucketID(tileID, bucket.getID()); + + // Set up tile drawable + std::shared_ptr vertices; + std::shared_ptr> indices; + auto* segments = &staticDataSegments; + + if (!bucket.vertices.empty() && !bucket.indices.empty() && !bucket.segments.empty()) { + vertices = bucket.sharedVertices; + indices = bucket.sharedIndices; + segments = &bucket.segments; + } else { + vertices = staticDataSharedVertices; + indices = std::make_shared>(staticDataIndices); + } + + if (!builder) { + builder = context.createDrawableBuilder("colorRelief"); + } + + gfx::VertexAttributeArrayPtr vertexAttrs; + auto buildVertexAttributes = [&] { + if (!vertexAttrs) { + vertexAttrs = context.createVertexAttributeArray(); + + if (const auto& attr = vertexAttrs->set(idColorReliefPosVertexAttribute)) { + attr->setSharedRawData(vertices, + offsetof(HillshadeLayoutVertex, a1), + 0, + sizeof(HillshadeLayoutVertex), + gfx::AttributeDataType::Short2); + } + } + return vertexAttrs; + }; + + const auto updateExisting = [&](gfx::Drawable& drawable) { + if (drawable.getLayerTweaker() != layerTweaker) { + return false; + } + + drawable.updateVertexAttributes(buildVertexAttributes(), + vertices->elements(), + gfx::Triangles(), + std::move(indices), + segments->data(), + segments->size()); + + // Update textures + std::shared_ptr demTexture = context.createTexture2D(); + demTexture->setImage(bucket.getDEMData().getImagePtr()); + demTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Linear, + .wrapU = gfx::TextureWrapType::Clamp, + .wrapV = gfx::TextureWrapType::Clamp}); + drawable.setTexture(demTexture, idColorReliefImageTexture); + + if (elevationStopsTexture) { + drawable.setTexture(elevationStopsTexture, idColorReliefElevationStopsTexture); + } + if (colorStopsTexture) { + drawable.setTexture(colorStopsTexture, idColorReliefColorStopsTexture); + } + + return true; + }; + + if (updateTile(renderPass, tileID, std::move(updateExisting))) { + continue; + } + + builder->setShader(colorReliefShader); + builder->setDepthType(gfx::DepthMaskType::ReadOnly); + builder->setColorMode(gfx::ColorMode::alphaBlended()); + builder->setCullFaceMode(gfx::CullFaceMode::disabled()); + builder->setRenderPass(renderPass); + builder->setVertexAttributes(buildVertexAttributes()); + builder->setRawVertices({}, vertices->elements(), gfx::AttributeDataType::Short2); + builder->setSegments(gfx::Triangles(), indices->vector(), segments->data(), segments->size()); + + // Bind DEM texture + std::shared_ptr demTexture = context.createTexture2D(); + demTexture->setImage(bucket.getDEMData().getImagePtr()); + demTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Linear, + .wrapU = gfx::TextureWrapType::Clamp, + .wrapV = gfx::TextureWrapType::Clamp}); + builder->setTexture(demTexture, idColorReliefImageTexture); + + // Bind color ramp textures + if (elevationStopsTexture) { + builder->setTexture(elevationStopsTexture, idColorReliefElevationStopsTexture); + } + + if (colorStopsTexture) { + builder->setTexture(colorStopsTexture, idColorReliefColorStopsTexture); + } + + builder->flush(context); + + for (auto& drawable : builder->clearDrawables()) { + drawable->setTileID(tileID); + drawable->setLayerTweaker(layerTweaker); + + // Set up tile properties UBO + shaders::ColorReliefTilePropsUBO tilePropsUBO; + + const auto& demData = bucket.getDEMData(); + const auto unpackVector = demData.getUnpackVector(); + tilePropsUBO.unpack = {{unpackVector[0], unpackVector[1], unpackVector[2], unpackVector[3]}}; + tilePropsUBO.dimension = {{static_cast(demData.dim), static_cast(demData.dim)}}; + tilePropsUBO.color_ramp_size = static_cast(colorRampSize); + tilePropsUBO.pad_tile0 = 0.0f; + + auto& drawableUniforms = drawable->mutableUniformBuffers(); + drawableUniforms.createOrUpdate(idColorReliefTilePropsUBO, &tilePropsUBO, context); + + tileLayerGroup->addDrawable(renderPass, tileID, std::move(drawable)); + ++stats.drawablesAdded; + } + } +} + +bool RenderColorReliefLayer::queryIntersectsFeature(const GeometryCoordinates&, + const GeometryTileFeature&, + float, + const TransformState&, + float, + const mat4&, + const FeatureState&) const { + return false; +} + +} // namespace mbgl diff --git a/src/mbgl/renderer/layers/render_color_relief_layer.hpp b/src/mbgl/renderer/layers/render_color_relief_layer.hpp new file mode 100644 index 000000000000..a90a8d1e7dcb --- /dev/null +++ b/src/mbgl/renderer/layers/render_color_relief_layer.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include // Added for std::vector + +namespace mbgl { +namespace gfx { +class Texture2D; +class ShaderProgramBase; +} // namespace gfx +} // namespace mbgl + +namespace mbgl { + +class RenderColorReliefLayer final : public RenderLayer { +public: + explicit RenderColorReliefLayer(Immutable); + ~RenderColorReliefLayer() override; + + /// Generate any changes needed by the layer + void update(gfx::ShaderRegistry&, + gfx::Context&, + const TransformState&, + const std::shared_ptr&, + const RenderTree&, + UniqueChangeRequestVec&) override; + +private: + void transition(const TransitionParameters&) override; + void evaluate(const PropertyEvaluationParameters&) override; + bool hasTransition() const override; + bool hasCrossfade() const override; + void prepare(const LayerPrepareParameters&) override; + bool queryIntersectsFeature(const GeometryCoordinates&, + const GeometryTileFeature&, + float, + const TransformState&, + float, + const mat4&, + const FeatureState&) const override; + void updateColorRamp(); + + // Paint properties + style::ColorReliefPaintProperties::Unevaluated unevaluated; + + // Color ramp data + uint32_t colorRampSize = 256; + bool colorRampChanged = true; + + // FIX 1: Changed type and name to correctly hold float elevation data + std::shared_ptr> elevationStopsData; // Elevation values for each stop + + std::shared_ptr colorStops; // RGB colors for each stop + + // GPU textures + std::shared_ptr elevationStopsTexture; + std::shared_ptr colorStopsTexture; + + // Shader + gfx::ShaderProgramBasePtr colorReliefShader; + + // Vertex data + using ColorReliefVertexVector = gfx::VertexVector; + std::shared_ptr staticDataSharedVertices; +}; + +} // namespace mbgl diff --git a/src/mbgl/renderer/layers/render_hillshade_layer.cpp b/src/mbgl/renderer/layers/render_hillshade_layer.cpp index 872f78563204..23154738b794 100644 --- a/src/mbgl/renderer/layers/render_hillshade_layer.cpp +++ b/src/mbgl/renderer/layers/render_hillshade_layer.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -52,9 +53,14 @@ std::array RenderHillshadeLayer::getLatRange(const UnwrappedTileID& id return {{static_cast(latlng0.latitude()), static_cast(latlng1.latitude())}}; } +// Keep old function for backward compatibility during transition std::array RenderHillshadeLayer::getLight(const PaintParameters& parameters) { const auto& evaluated = static_cast(*evaluatedProperties).evaluated; - float azimuthal = util::deg2radf(evaluated.get()); + + // Get first element from vectors (for backward compatibility) + std::vector directions = evaluated.get(); + float azimuthal = util::deg2radf(directions.empty() ? 335.0f : directions[0]); + if (evaluated.get() == HillshadeIlluminationAnchorType::Viewport) azimuthal = azimuthal - static_cast(parameters.state.getBearing()); return {{evaluated.get(), azimuthal}}; @@ -175,9 +181,22 @@ void RenderHillshadeLayer::update(gfx::ShaderRegistry& shaders, if (!hillshadePrepareShader) { hillshadePrepareShader = context.getGenericShader(shaders, HillshadePrepareShaderGroupName); } + + // NEW: Get shader with defines for number of illumination sources + // For now, we'll use the default shader. When you implement multiple light sources, + // you'll need to determine the number of lights and select the appropriate shader variant if (!hillshadeShader) { + // TODO: Add shader variant selection based on number of lights + // For now, use default (1 light source) hillshadeShader = context.getGenericShader(shaders, HillshadeShaderGroupName); + + // Future implementation: + // const auto& evaluated = static_cast(*evaluatedProperties).evaluated; + // auto illumination = getIlluminationProperties(evaluated); + // std::string shaderName = HillshadeShaderGroupName + "/lights:" + std::to_string(illumination.numSources()); + // hillshadeShader = context.getGenericShader(shaders, shaderName); } + if (!hillshadePrepareShader || !hillshadeShader) { removeAllDrawables(); return; diff --git a/src/mbgl/shaders/gl/shader_info.cpp b/src/mbgl/shaders/gl/shader_info.cpp index 03643e4a0e3b..9d6026702186 100644 --- a/src/mbgl/shaders/gl/shader_info.cpp +++ b/src/mbgl/shaders/gl/shader_info.cpp @@ -316,6 +316,23 @@ const std::vector HillshadeShaderInfo::textures = { TextureInfo{"u_image", idHillshadeImageTexture}, }; +// Color Relief +using ColorReliefShaderInfo = ShaderInfo; + +const std::vector ColorReliefShaderInfo::uniformBlocks = { + UniformBlockInfo{"ColorReliefDrawableUBO", idColorReliefDrawableUBO}, + UniformBlockInfo{"ColorReliefTilePropsUBO", idColorReliefTilePropsUBO}, + UniformBlockInfo{"ColorReliefEvaluatedPropsUBO", idColorReliefEvaluatedPropsUBO}, +}; +const std::vector ColorReliefShaderInfo::attributes = { + AttributeInfo{"a_pos", idColorReliefPosVertexAttribute}, +}; +const std::vector ColorReliefShaderInfo::textures = { + TextureInfo{"u_image", idColorReliefImageTexture}, + TextureInfo{"u_elevation_stops", idColorReliefElevationStopsTexture}, + TextureInfo{"u_color_stops", idColorReliefColorStopsTexture}, +}; + // Line using LineShaderInfo = ShaderInfo; diff --git a/src/mbgl/shaders/mtl/color_relief.cpp b/src/mbgl/shaders/mtl/color_relief.cpp new file mode 100644 index 000000000000..0abb6be71a74 --- /dev/null +++ b/src/mbgl/shaders/mtl/color_relief.cpp @@ -0,0 +1,21 @@ +#include +#include + +namespace mbgl { +namespace shaders { + +using ColorReliefShaderSource = ShaderSource; + +const std::array ColorReliefShaderSource::attributes = { + AttributeInfo{colorReliefUBOCount + 0, gfx::AttributeDataType::Short2, idColorReliefPosVertexAttribute}, + AttributeInfo{colorReliefUBOCount + 1, gfx::AttributeDataType::Short2, idColorReliefTexturePosVertexAttribute}, +}; + +const std::array ColorReliefShaderSource::textures = { + TextureInfo{0, idColorReliefImageTexture}, + TextureInfo{1, idColorReliefElevationStopsTexture}, + TextureInfo{2, idColorReliefColorStopsTexture}, +}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/shader_source.cpp b/src/mbgl/shaders/shader_source.cpp index e14fee616d10..9db6f08c79a9 100644 --- a/src/mbgl/shaders/shader_source.cpp +++ b/src/mbgl/shaders/shader_source.cpp @@ -28,6 +28,7 @@ MBGL_DEFINE_ENUM(BuiltIn, {BuiltIn::HeatmapShader, "HeatmapShader"}, {BuiltIn::HeatmapTextureShader, "HeatmapTextureShader"}, {BuiltIn::HillshadePrepareShader, "HillshadePrepareShader"}, + {BuiltIn::ColorReliefShader, "ColorReliefShader"}, {BuiltIn::HillshadeShader, "HillshadeShader"}, {BuiltIn::LineShader, "LineShader"}, {BuiltIn::LineGradientShader, "LineGradientShader"}, diff --git a/src/mbgl/shaders/vulkan/color_relief.cpp b/src/mbgl/shaders/vulkan/color_relief.cpp new file mode 100644 index 000000000000..0d074edb00ad --- /dev/null +++ b/src/mbgl/shaders/vulkan/color_relief.cpp @@ -0,0 +1,22 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +using ColorReliefShaderSource = ShaderSource; + +const std::array ColorReliefShaderSource::attributes = { + AttributeInfo{0, gfx::AttributeDataType::Short2, idColorReliefPosVertexAttribute}, + AttributeInfo{1, gfx::AttributeDataType::Short2, idColorReliefTexturePosVertexAttribute}, +}; + +const std::array ColorReliefShaderSource::textures = { + TextureInfo{0, idColorReliefImageTexture}, + TextureInfo{1, idColorReliefElevationStopsTexture}, + TextureInfo{2, idColorReliefColorStopsTexture}, +}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/color_relief.cpp b/src/mbgl/shaders/webgpu/color_relief.cpp new file mode 100644 index 000000000000..3ce95ca28243 --- /dev/null +++ b/src/mbgl/shaders/webgpu/color_relief.cpp @@ -0,0 +1,22 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +using ColorReliefShaderSource = ShaderSource; + +const std::array ColorReliefShaderSource::attributes = { + AttributeInfo{5, gfx::AttributeDataType::Short2, idColorReliefPosVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::Short2, idColorReliefTexturePosVertexAttribute}, +}; + +const std::array ColorReliefShaderSource::textures = { + TextureInfo{0, idColorReliefImageTexture}, + TextureInfo{1, idColorReliefElevationStopsTexture}, + TextureInfo{2, idColorReliefColorStopsTexture}, +}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/style/conversion/color_ramp_property_value.cpp b/src/mbgl/style/conversion/color_ramp_property_value.cpp index c2d91ffb9d2d..bd01260ec52c 100644 --- a/src/mbgl/style/conversion/color_ramp_property_value.cpp +++ b/src/mbgl/style/conversion/color_ramp_property_value.cpp @@ -25,14 +25,17 @@ std::optional Converter::operato return std::nullopt; } assert(*expression); - if (!isFeatureConstant(**expression)) { + + auto deps = (*expression)->dependencies; + if ((deps & Dependency::Feature) && !(deps & Dependency::Elevation)) { error.message = "data expressions not supported"; return std::nullopt; } - if (!isZoomConstant(**expression)) { + if (deps & Dependency::Zoom) { error.message = "zoom expressions not supported"; return std::nullopt; } + return ColorRampPropertyValue(std::move(*expression)); } else { error.message = "color ramp must be an expression"; diff --git a/src/mbgl/style/conversion/constant.cpp b/src/mbgl/style/conversion/constant.cpp index 88f44edb8a92..dd9db0509fb9 100644 --- a/src/mbgl/style/conversion/constant.cpp +++ b/src/mbgl/style/conversion/constant.cpp @@ -78,6 +78,8 @@ template std::optional Converter::op Error&) const; template std::optional Converter::operator()( const Convertible&, Error&) const; +template std::optional Converter::operator()(const Convertible&, + Error&) const; template std::optional Converter::operator()(const Convertible&, Error&) const; template std::optional Converter::operator()(const Convertible&, Error&) const; template std::optional Converter::operator()(const Convertible&, Error&) const; @@ -114,6 +116,28 @@ std::optional Converter::operator()(const Convertible& value, Erro return color; } +std::optional> Converter>::operator()(const Convertible& value, + Error& error) const { + if (!isArray(value)) { + // Try single color + auto color = convert(value, error); + if (!color) return std::nullopt; + return std::vector{*color}; + } + + std::vector result; + auto length = arrayLength(value); + result.reserve(length); + + for (std::size_t i = 0; i < length; ++i) { + auto color = convert(arrayMember(value, i), error); + if (!color) return std::nullopt; + result.push_back(*color); + } + + return result; +} + std::optional Converter::operator()(const Convertible& value, Error& error) const { std::optional result; if (isArray(value)) { @@ -238,8 +262,13 @@ template std::optional> Converter>:: std::optional> Converter>::operator()(const Convertible& value, Error& error) const { if (!isArray(value)) { - error.message = "value must be an array"; - return std::nullopt; + // Try single number - wrap in array + std::optional number = toNumber(value); + if (!number) { + error.message = "value must be a number or an array of numbers"; + return std::nullopt; + } + return std::vector{*number}; } std::vector result; diff --git a/src/mbgl/style/conversion/function.cpp b/src/mbgl/style/conversion/function.cpp index e605be860b9b..2c055e8d6a7b 100644 --- a/src/mbgl/style/conversion/function.cpp +++ b/src/mbgl/style/conversion/function.cpp @@ -33,7 +33,8 @@ bool hasTokens(const std::string& source) { while (pos != end) { auto brace = std::find(pos, end, '{'); if (brace == end) return false; - for (brace++; brace != end && tokenReservedChars.find(*brace) == std::string::npos; brace++); + for (brace++; brace != end && tokenReservedChars.find(*brace) == std::string::npos; brace++) + ; if (brace != end && *brace == '}') { return true; } @@ -66,7 +67,8 @@ std::unique_ptr convertTokenStringToExpression(const std::string& so } pos = brace; if (pos != end) { - for (brace++; brace != end && tokenReservedChars.find(*brace) == std::string::npos; brace++); + for (brace++; brace != end && tokenReservedChars.find(*brace) == std::string::npos; brace++) + ; if (brace != end && *brace == '}') { inputs.push_back(get(literal(std::string(pos + 1, brace)))); pos = brace + 1; @@ -166,6 +168,10 @@ template std::optional> convertFunctionToE const Convertible&, Error&, bool); template std::optional> convertFunctionToExpression( const Convertible&, Error&, bool); +template std::optional>> convertFunctionToExpression>( + const Convertible&, Error&, bool); +template std::optional> convertFunctionToExpression( + const Convertible&, Error&, bool); template std::optional> convertFunctionToExpression(const Convertible&, Error&, bool); @@ -216,7 +222,14 @@ bool interpolatable(type::Type type) noexcept { [&](const type::ColorType&) { return true; }, [&](const type::PaddingType&) { return true; }, [&](const type::VariableAnchorOffsetCollectionType&) { return true; }, - [&](const type::Array& array) { return array.N && array.itemType == type::Number; }, + [&](const type::Array& array) { + // Arrays are interpolatable if they have a fixed size and item type is Number, + // OR if the item type itself is interpolatable (e.g., Array) + if (array.N && array.itemType == type::Number) { + return true; + } + return interpolatable(array.itemType); + }, [&](const auto&) { return false; }); } @@ -267,11 +280,8 @@ std::optional> convertLiteral(type::Type type, } return literal(*result); }, - [&](const type::Array& array) -> std::optional> { - if (!isArray(value)) { - error.message = "value must be an array"; - return std::nullopt; - } + [&](const type::Array& array) -> std::optional> { + // Handle array values if (array.N && arrayLength(value) != *array.N) { error.message = "value must be an array of length " + util::toString(*array.N); return std::nullopt; @@ -354,45 +364,46 @@ std::optional>> convertStops(const error.message = "function value must specify stops"; return std::nullopt; } - if (!isArray(*stopsValue)) { error.message = "function stops must be an array"; return std::nullopt; } - if (arrayLength(*stopsValue) == 0) { error.message = "function must have at least one stop"; return std::nullopt; } + // For array output types, parse stops as the item type + type::Type stopType = type; + type.match( + [&](const type::Array& arr) { + stopType = arr.itemType; + }, + [](const auto&) {} + ); + std::map> stops; for (std::size_t i = 0; i < arrayLength(*stopsValue); ++i) { const auto& stopValue = arrayMember(*stopsValue, i); - if (!isArray(stopValue)) { error.message = "function stop must be an array"; return std::nullopt; } - if (arrayLength(stopValue) != 2) { error.message = "function stop must have two elements"; return std::nullopt; } - std::optional t = convert(arrayMember(stopValue, 0), error); if (!t) { return std::nullopt; } - std::optional> e = convertLiteral( - type, arrayMember(stopValue, 1), error, convertTokens); + stopType, arrayMember(stopValue, 1), error, convertTokens); if (!e) { return std::nullopt; } - stops.emplace(*t, std::move(*e)); } - return {std::move(stops)}; } @@ -550,13 +561,31 @@ std::optional> convertIntervalFunction( const std::function(bool)>& makeInput, std::unique_ptr def, bool convertTokens = false) { + auto stops = convertStops(type, value, error, convertTokens); if (!stops) { return std::nullopt; } omitFirstStop(*stops); - - auto expr = step(type, makeInput(true), std::move(*stops)); + + // For array types, create step with item type + type::Type exprType = type; + bool isArrayType = false; + type.match( + [&](const type::Array& arr) { + exprType = arr.itemType; + isArrayType = true; + }, + [](const auto&) {} + ); + + auto expr = step(exprType, makeInput(true), std::move(*stops)); + + // For array types with camera functions, return the step directly + if (isArrayType && !def) { + return expr; + } + return numberOrDefault(std::move(type), makeInput(false), std::move(expr), std::move(def)); } @@ -567,6 +596,7 @@ std::optional> convertExponentialFunction( const std::function(bool)>& makeInput, std::unique_ptr def, bool convertTokens = false) { + auto stops = convertStops(type, value, error, convertTokens); if (!stops) { return std::nullopt; @@ -575,8 +605,26 @@ std::optional> convertExponentialFunction( if (!base) { return std::nullopt; } - - auto expr = interpolate(type, exponential(*base), makeInput(true), std::move(*stops)); + + // For array types, create interpolation with item type + type::Type exprType = type; + bool isArrayType = false; + type.match( + [&](const type::Array& arr) { + exprType = arr.itemType; + isArrayType = true; + }, + [](const auto&) {} + ); + + auto expr = interpolate(exprType, exponential(*base), makeInput(true), std::move(*stops)); + + // For array types with camera functions, return the interpolation directly + // The value.cpp wrapping will convert Color -> std::vector + if (isArrayType && !def) { + return expr; + } + return numberOrDefault(std::move(type), makeInput(false), std::move(expr), std::move(def)); } diff --git a/src/mbgl/style/conversion/hillshade_conversions.cpp b/src/mbgl/style/conversion/hillshade_conversions.cpp new file mode 100644 index 000000000000..90cd1f5fffb6 --- /dev/null +++ b/src/mbgl/style/conversion/hillshade_conversions.cpp @@ -0,0 +1,90 @@ +#include +#include +#include +#include +#include +#include + +namespace mbgl { + +// Enum specializations +template <> +const char* Enum::toString(style::HillshadeMethodType t) { + switch (t) { + case style::HillshadeMethodType::Standard: + return "standard"; + case style::HillshadeMethodType::Basic: + return "basic"; + case style::HillshadeMethodType::Combined: + return "combined"; + case style::HillshadeMethodType::Igor: + return "igor"; + case style::HillshadeMethodType::Multidirectional: + return "multidirectional"; + } + return "standard"; +} + +template <> +std::optional Enum::toEnum(const std::string& s) { + if (s == "standard") return style::HillshadeMethodType::Standard; + if (s == "basic") return style::HillshadeMethodType::Basic; + if (s == "combined") return style::HillshadeMethodType::Combined; + if (s == "igor") return style::HillshadeMethodType::Igor; + if (s == "multidirectional") return style::HillshadeMethodType::Multidirectional; + return std::nullopt; +} + +namespace style { +namespace expression { + +// valueTypeToExpressionType specializations +template <> +type::Type valueTypeToExpressionType>() { + return type::Array(type::Color); +} + +template <> +type::Type valueTypeToExpressionType() { + return type::String; +} + +// ValueConverter specializations for runtime expression evaluation + +template <> +std::optional> ValueConverter>::fromExpressionValue(const Value& value) { + if (value.is>()) { + const auto& values = value.get>(); + std::vector result; + result.reserve(values.size()); + for (const auto& v : values) { + auto color = ValueConverter::fromExpressionValue(v); + if (!color) return std::nullopt; + result.push_back(*color); + } + return result; + } + + // Handle single color - wrap in array + auto color = ValueConverter::fromExpressionValue(value); + if (!color) return std::nullopt; + return std::vector{*color}; +} + +template <> +std::optional ValueConverter::fromExpressionValue(const Value& value) { + if (!value.is()) return std::nullopt; + + const auto& str = value.get(); + if (str == "standard") return HillshadeMethodType::Standard; + if (str == "basic") return HillshadeMethodType::Basic; + if (str == "combined") return HillshadeMethodType::Combined; + if (str == "igor") return HillshadeMethodType::Igor; + if (str == "multidirectional") return HillshadeMethodType::Multidirectional; + + return std::nullopt; +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/conversion/property_value.cpp b/src/mbgl/style/conversion/property_value.cpp index 811a5cf1f30e..d86641f9576e 100644 --- a/src/mbgl/style/conversion/property_value.cpp +++ b/src/mbgl/style/conversion/property_value.cpp @@ -2,6 +2,7 @@ #include #include #include +#include namespace mbgl { namespace style { @@ -168,6 +169,12 @@ mbgl::style::conversion::Converter>, void>:: template std::optional> Converter>::operator()( conversion::Convertible const&, conversion::Error&, bool, bool) const; +// Hillshade array and enum types +template std::optional>> Converter>>::operator()( + conversion::Convertible const&, conversion::Error&, bool, bool) const; +template std::optional> Converter>::operator()( + conversion::Convertible const&, conversion::Error&, bool, bool) const; + } // namespace conversion } // namespace style } // namespace mbgl diff --git a/src/mbgl/style/expression/compound_expression.cpp b/src/mbgl/style/expression/compound_expression.cpp index 36211667cbb5..9ce0f8fbeff1 100644 --- a/src/mbgl/style/expression/compound_expression.cpp +++ b/src/mbgl/style/expression/compound_expression.cpp @@ -369,6 +369,26 @@ const auto& heatmapDensityCompoundExpression() { return signature; } +const auto& elevationCompoundExpression() { + static auto signature = detail::makeSignature( + "elevation", + [](const EvaluationContext& params) -> Result { + // For color-relief, elevation is passed via colorRampParameter + if (params.colorRampParameter) { + return *(params.colorRampParameter); + } + // For 3D terrain, elevation is passed directly + if (params.elevation) { + return static_cast(*(params.elevation)); + } + // During parsing/validation, return a valid value to allow parsing to succeed + // The Dependency::Elevation flag prevents this from being optimized to a constant + return 0.0; + }, + Dependency::Elevation); + return signature; +} + const auto& lineProgressCompoundExpression() { static auto signature = detail::makeSignature("line-progress", [](const EvaluationContext& params) -> Result { @@ -1007,6 +1027,7 @@ constexpr const auto compoundExpressionRegistry = {"rgb", rgbCompoundExpression}, {"zoom", zoomCompoundExpression}, {"heatmap-density", heatmapDensityCompoundExpression}, + {"elevation", elevationCompoundExpression}, {"line-progress", lineProgressCompoundExpression}, {"accumulated", accumulatedCompoundExpression}, {"has", hasContextCompoundExpression}, diff --git a/src/mbgl/style/expression/parsing_context.cpp b/src/mbgl/style/expression/parsing_context.cpp index eff15749e3b5..c282e0362c30 100644 --- a/src/mbgl/style/expression/parsing_context.cpp +++ b/src/mbgl/style/expression/parsing_context.cpp @@ -44,7 +44,7 @@ namespace style { namespace expression { namespace { -const auto requiredProps = std::array{"zoom", "heatmap-density", "line-progress", "accumulated"}; +const auto requiredProps = std::array{"zoom", "heatmap-density", "line-progress", "accumulated", "elevation"}; bool isConstant(const Expression& expression) { const auto kind = expression.getKind(); diff --git a/src/mbgl/style/expression/value.cpp b/src/mbgl/style/expression/value.cpp index afd691f2ad4b..215d528a5ac8 100644 --- a/src/mbgl/style/expression/value.cpp +++ b/src/mbgl/style/expression/value.cpp @@ -244,6 +244,7 @@ template std::optional> ValueConverter>::fromExpressionValue(const Value& value) { return value.match( [&](const std::vector& v) -> std::optional> { + // Convert array of expression values to vector of typed values std::vector result; result.reserve(v.size()); for (const Value& item : v) { @@ -255,7 +256,17 @@ std::optional> ValueConverter>::fromExpressionValu } return result; }, - [&](const auto&) { return std::optional>(); }); + [&](const auto&) -> std::optional> { + // Handle zoom function interpolation for array-typed properties. + // When array properties (e.g., std::vector) use zoom functions, + // the expression interpolates individual item values (e.g., Color), + // so we wrap the single interpolated value into a vector. + std::optional convertedItem = ValueConverter::fromExpressionValue(value); + if (convertedItem) { + return std::vector{*convertedItem}; + } + return std::nullopt; + }); } Value ValueConverter::toExpressionValue(const mbgl::style::Position& value) { diff --git a/src/mbgl/style/layers/color_relief_layer.cpp b/src/mbgl/style/layers/color_relief_layer.cpp new file mode 100644 index 000000000000..883237f5ecc4 --- /dev/null +++ b/src/mbgl/style/layers/color_relief_layer.cpp @@ -0,0 +1,239 @@ +// clang-format off + +// This file is generated. Edit scripts/generate-style-code.js, then run `make style-code`. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace mbgl { +namespace style { + + +// static +const LayerTypeInfo* ColorReliefLayer::Impl::staticTypeInfo() noexcept { + const static LayerTypeInfo typeInfo{.type="color-relief", + .source=LayerTypeInfo::Source::Required, + .pass3d=LayerTypeInfo::Pass3D::NotRequired, + .layout=LayerTypeInfo::Layout::NotRequired, + .fadingTiles=LayerTypeInfo::FadingTiles::NotRequired, + .crossTileIndex=LayerTypeInfo::CrossTileIndex::NotRequired, + .tileKind=LayerTypeInfo::TileKind::RasterDEM}; + return &typeInfo; +} + +ColorReliefLayer::ColorReliefLayer(const std::string& layerID, const std::string& sourceID) + : Layer(makeMutable(layerID, sourceID)) { +} + +ColorReliefLayer::ColorReliefLayer(Immutable impl_) + : Layer(std::move(impl_)) { +} + +ColorReliefLayer::~ColorReliefLayer() { + weakFactory.invalidateWeakPtrs(); +} + +const ColorReliefLayer::Impl& ColorReliefLayer::impl() const { + return static_cast(*baseImpl); +} + +Mutable ColorReliefLayer::mutableImpl() const { + return makeMutable(impl()); +} + +std::unique_ptr ColorReliefLayer::cloneRef(const std::string& id_) const { + auto impl_ = mutableImpl(); + impl_->id = id_; + impl_->paint = ColorReliefPaintProperties::Transitionable(); + return std::make_unique(std::move(impl_)); +} + +void ColorReliefLayer::Impl::stringifyLayout(rapidjson::Writer&) const { +} + +// Layout properties + + +// Paint properties + +ColorRampPropertyValue ColorReliefLayer::getDefaultColorReliefColor() { + return {{}}; +} + +const ColorRampPropertyValue& ColorReliefLayer::getColorReliefColor() const { + return impl().paint.template get().value; +} + +void ColorReliefLayer::setColorReliefColor(const ColorRampPropertyValue& value) { + if (value == getColorReliefColor()) + return; + auto impl_ = mutableImpl(); + impl_->paint.template get().value = value; + baseImpl = std::move(impl_); + observer->onLayerChanged(*this); +} + +void ColorReliefLayer::setColorReliefColorTransition(const TransitionOptions& options) { + auto impl_ = mutableImpl(); + impl_->paint.template get().options = options; + baseImpl = std::move(impl_); +} + +TransitionOptions ColorReliefLayer::getColorReliefColorTransition() const { + return impl().paint.template get().options; +} + +PropertyValue ColorReliefLayer::getDefaultColorReliefOpacity() { + return {1.f}; +} + +const PropertyValue& ColorReliefLayer::getColorReliefOpacity() const { + return impl().paint.template get().value; +} + +void ColorReliefLayer::setColorReliefOpacity(const PropertyValue& value) { + if (value == getColorReliefOpacity()) + return; + auto impl_ = mutableImpl(); + impl_->paint.template get().value = value; + baseImpl = std::move(impl_); + observer->onLayerChanged(*this); +} + +void ColorReliefLayer::setColorReliefOpacityTransition(const TransitionOptions& options) { + auto impl_ = mutableImpl(); + impl_->paint.template get().options = options; + baseImpl = std::move(impl_); +} + +TransitionOptions ColorReliefLayer::getColorReliefOpacityTransition() const { + return impl().paint.template get().options; +} + +using namespace conversion; + +namespace { + +constexpr uint8_t kPaintPropertyCount = 4u; + +enum class Property : uint8_t { + ColorReliefColor, + ColorReliefOpacity, + ColorReliefColorTransition, + ColorReliefOpacityTransition, +}; + +template +constexpr uint8_t toUint8(T t) noexcept { + return uint8_t(mbgl::underlying_type(t)); +} + +constexpr const auto layerProperties = mapbox::eternal::hash_map( + {{"color-relief-color", toUint8(Property::ColorReliefColor)}, + {"color-relief-opacity", toUint8(Property::ColorReliefOpacity)}, + {"color-relief-color-transition", toUint8(Property::ColorReliefColorTransition)}, + {"color-relief-opacity-transition", toUint8(Property::ColorReliefOpacityTransition)}}); + +StyleProperty getLayerProperty(const ColorReliefLayer& layer, Property property) { + switch (property) { + case Property::ColorReliefColor: + return makeStyleProperty(layer.getColorReliefColor()); + case Property::ColorReliefOpacity: + return makeStyleProperty(layer.getColorReliefOpacity()); + case Property::ColorReliefColorTransition: + return makeStyleProperty(layer.getColorReliefColorTransition()); + case Property::ColorReliefOpacityTransition: + return makeStyleProperty(layer.getColorReliefOpacityTransition()); + } + return {}; +} + +StyleProperty getLayerProperty(const ColorReliefLayer& layer, const std::string& name) { + const auto it = layerProperties.find(name.c_str()); + if (it == layerProperties.end()) { + return {}; + } + return getLayerProperty(layer, static_cast(it->second)); +} + +} // namespace + +Value ColorReliefLayer::serialize() const { + auto result = Layer::serialize(); + assert(result.getObject()); + for (const auto& property : layerProperties) { + auto styleProperty = getLayerProperty(*this, static_cast(property.second)); + if (styleProperty.getKind() == StyleProperty::Kind::Undefined) continue; + serializeProperty(result, styleProperty, property.first.c_str(), property.second < kPaintPropertyCount); + } + return result; +} + +std::optional ColorReliefLayer::setPropertyInternal(const std::string& name, const Convertible& value) { + const auto it = layerProperties.find(name.c_str()); + if (it == layerProperties.end()) return Error{"layer doesn't support this property"}; + + auto property = static_cast(it->second); + + if (property == Property::ColorReliefColor) { + Error error; + const auto& typedValue = convert(value, error, false, false); + if (!typedValue) { + return error; + } + + setColorReliefColor(*typedValue); + return std::nullopt; + } + if (property == Property::ColorReliefOpacity) { + Error error; + const auto& typedValue = convert>(value, error, false, false); + if (!typedValue) { + return error; + } + + setColorReliefOpacity(*typedValue); + return std::nullopt; + } + + Error error; + std::optional transition = convert(value, error); + if (!transition) { + return error; + } + + if (property == Property::ColorReliefColorTransition) { + setColorReliefColorTransition(*transition); + return std::nullopt; + } + + if (property == Property::ColorReliefOpacityTransition) { + setColorReliefOpacityTransition(*transition); + return std::nullopt; + } + + return Error{"layer doesn't support this property"}; +} + +StyleProperty ColorReliefLayer::getProperty(const std::string& name) const { + return getLayerProperty(*this, name); +} + +Mutable ColorReliefLayer::mutableBaseImpl() const { + return staticMutableCast(mutableImpl()); +} + +} // namespace style +} // namespace mbgl + +// clang-format on diff --git a/src/mbgl/style/layers/color_relief_layer_impl.cpp b/src/mbgl/style/layers/color_relief_layer_impl.cpp new file mode 100644 index 000000000000..0ee20a5b9793 --- /dev/null +++ b/src/mbgl/style/layers/color_relief_layer_impl.cpp @@ -0,0 +1,11 @@ +#include + +namespace mbgl { +namespace style { + +bool ColorReliefLayer::Impl::hasLayoutDifference(const Layer::Impl&) const { + return false; +} + +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/layers/color_relief_layer_impl.hpp b/src/mbgl/style/layers/color_relief_layer_impl.hpp new file mode 100644 index 000000000000..7e2f4ca31eee --- /dev/null +++ b/src/mbgl/style/layers/color_relief_layer_impl.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace style { + +class ColorReliefLayer::Impl : public Layer::Impl { +public: + using Layer::Impl::Impl; + + bool hasLayoutDifference(const Layer::Impl&) const override; + void stringifyLayout(rapidjson::Writer&) const override; + + ColorReliefPaintProperties::Transitionable paint; + + DECLARE_LAYER_TYPE_INFO; +}; + +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/layers/color_relief_layer_properties.cpp b/src/mbgl/style/layers/color_relief_layer_properties.cpp new file mode 100644 index 000000000000..63aa7561c40a --- /dev/null +++ b/src/mbgl/style/layers/color_relief_layer_properties.cpp @@ -0,0 +1,39 @@ +// clang-format off + +// This file is generated. Edit scripts/generate-style-code.js, then run `make style-code`. + +#include + +#include + +namespace mbgl { +namespace style { + +ColorReliefLayerProperties::ColorReliefLayerProperties( + Immutable impl_) + : LayerProperties(std::move(impl_)) {} + +ColorReliefLayerProperties::ColorReliefLayerProperties( + Immutable impl_, + ColorReliefPaintProperties::PossiblyEvaluated evaluated_) + : LayerProperties(std::move(impl_)), + evaluated(std::move(evaluated_)) {} + +ColorReliefLayerProperties::~ColorReliefLayerProperties() = default; + +unsigned long ColorReliefLayerProperties::constantsMask() const { + return evaluated.constantsMask(); +} + +const ColorReliefLayer::Impl& ColorReliefLayerProperties::layerImpl() const noexcept { + return static_cast(*baseImpl); +} + +expression::Dependency ColorReliefLayerProperties::getDependencies() const noexcept { + return layerImpl().paint.getDependencies(); +} + +} // namespace style +} // namespace mbgl + +// clang-format on diff --git a/src/mbgl/style/layers/color_relief_layer_properties.hpp b/src/mbgl/style/layers/color_relief_layer_properties.hpp new file mode 100644 index 000000000000..c026831fba33 --- /dev/null +++ b/src/mbgl/style/layers/color_relief_layer_properties.hpp @@ -0,0 +1,51 @@ +// clang-format off + +// This file is generated. Edit scripts/generate-style-code.js, then run `make style-code`. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { +namespace style { + +struct ColorReliefColor : ColorRampProperty { +}; + +struct ColorReliefOpacity : PaintProperty { + static float defaultValue() { return 1.f; } +}; + +class ColorReliefPaintProperties : public Properties< + ColorReliefColor, + ColorReliefOpacity +> {}; + +class ColorReliefLayerProperties final : public LayerProperties { +public: + explicit ColorReliefLayerProperties(Immutable); + ColorReliefLayerProperties( + Immutable, + ColorReliefPaintProperties::PossiblyEvaluated); + ~ColorReliefLayerProperties() override; + + unsigned long constantsMask() const override; + + expression::Dependency getDependencies() const noexcept override; + + const ColorReliefLayer::Impl& layerImpl() const noexcept; + // Data members. + ColorReliefPaintProperties::PossiblyEvaluated evaluated; +}; + +} // namespace style +} // namespace mbgl + +// clang-format on diff --git a/src/mbgl/style/layers/hillshade_layer.cpp b/src/mbgl/style/layers/hillshade_layer.cpp index e1af91d64fb8..e2a61435336e 100644 --- a/src/mbgl/style/layers/hillshade_layer.cpp +++ b/src/mbgl/style/layers/hillshade_layer.cpp @@ -120,15 +120,15 @@ TransitionOptions HillshadeLayer::getHillshadeExaggerationTransition() const { return impl().paint.template get().options; } -PropertyValue HillshadeLayer::getDefaultHillshadeHighlightColor() { - return {Color::white()}; +PropertyValue> HillshadeLayer::getDefaultHillshadeHighlightColor() { + return {{Color::white()}}; } -const PropertyValue& HillshadeLayer::getHillshadeHighlightColor() const { +const PropertyValue>& HillshadeLayer::getHillshadeHighlightColor() const { return impl().paint.template get().value; } -void HillshadeLayer::setHillshadeHighlightColor(const PropertyValue& value) { +void HillshadeLayer::setHillshadeHighlightColor(const PropertyValue>& value) { if (value == getHillshadeHighlightColor()) return; auto impl_ = mutableImpl(); @@ -147,6 +147,33 @@ TransitionOptions HillshadeLayer::getHillshadeHighlightColorTransition() const { return impl().paint.template get().options; } +PropertyValue> HillshadeLayer::getDefaultHillshadeIlluminationAltitude() { + return {{45.f}}; +} + +const PropertyValue>& HillshadeLayer::getHillshadeIlluminationAltitude() const { + return impl().paint.template get().value; +} + +void HillshadeLayer::setHillshadeIlluminationAltitude(const PropertyValue>& value) { + if (value == getHillshadeIlluminationAltitude()) + return; + auto impl_ = mutableImpl(); + impl_->paint.template get().value = value; + baseImpl = std::move(impl_); + observer->onLayerChanged(*this); +} + +void HillshadeLayer::setHillshadeIlluminationAltitudeTransition(const TransitionOptions& options) { + auto impl_ = mutableImpl(); + impl_->paint.template get().options = options; + baseImpl = std::move(impl_); +} + +TransitionOptions HillshadeLayer::getHillshadeIlluminationAltitudeTransition() const { + return impl().paint.template get().options; +} + PropertyValue HillshadeLayer::getDefaultHillshadeIlluminationAnchor() { return {HillshadeIlluminationAnchorType::Viewport}; } @@ -174,15 +201,15 @@ TransitionOptions HillshadeLayer::getHillshadeIlluminationAnchorTransition() con return impl().paint.template get().options; } -PropertyValue HillshadeLayer::getDefaultHillshadeIlluminationDirection() { - return {335.f}; +PropertyValue> HillshadeLayer::getDefaultHillshadeIlluminationDirection() { + return {{335.f}}; } -const PropertyValue& HillshadeLayer::getHillshadeIlluminationDirection() const { +const PropertyValue>& HillshadeLayer::getHillshadeIlluminationDirection() const { return impl().paint.template get().value; } -void HillshadeLayer::setHillshadeIlluminationDirection(const PropertyValue& value) { +void HillshadeLayer::setHillshadeIlluminationDirection(const PropertyValue>& value) { if (value == getHillshadeIlluminationDirection()) return; auto impl_ = mutableImpl(); @@ -201,15 +228,42 @@ TransitionOptions HillshadeLayer::getHillshadeIlluminationDirectionTransition() return impl().paint.template get().options; } -PropertyValue HillshadeLayer::getDefaultHillshadeShadowColor() { - return {Color::black()}; +PropertyValue HillshadeLayer::getDefaultHillshadeMethod() { + return {HillshadeMethodType::Standard}; +} + +const PropertyValue& HillshadeLayer::getHillshadeMethod() const { + return impl().paint.template get().value; +} + +void HillshadeLayer::setHillshadeMethod(const PropertyValue& value) { + if (value == getHillshadeMethod()) + return; + auto impl_ = mutableImpl(); + impl_->paint.template get().value = value; + baseImpl = std::move(impl_); + observer->onLayerChanged(*this); +} + +void HillshadeLayer::setHillshadeMethodTransition(const TransitionOptions& options) { + auto impl_ = mutableImpl(); + impl_->paint.template get().options = options; + baseImpl = std::move(impl_); +} + +TransitionOptions HillshadeLayer::getHillshadeMethodTransition() const { + return impl().paint.template get().options; } -const PropertyValue& HillshadeLayer::getHillshadeShadowColor() const { +PropertyValue> HillshadeLayer::getDefaultHillshadeShadowColor() { + return {{Color::black()}}; +} + +const PropertyValue>& HillshadeLayer::getHillshadeShadowColor() const { return impl().paint.template get().value; } -void HillshadeLayer::setHillshadeShadowColor(const PropertyValue& value) { +void HillshadeLayer::setHillshadeShadowColor(const PropertyValue>& value) { if (value == getHillshadeShadowColor()) return; auto impl_ = mutableImpl(); @@ -232,20 +286,24 @@ using namespace conversion; namespace { -constexpr uint8_t kPaintPropertyCount = 12u; +constexpr uint8_t kPaintPropertyCount = 16u; enum class Property : uint8_t { HillshadeAccentColor, HillshadeExaggeration, HillshadeHighlightColor, + HillshadeIlluminationAltitude, HillshadeIlluminationAnchor, HillshadeIlluminationDirection, + HillshadeMethod, HillshadeShadowColor, HillshadeAccentColorTransition, HillshadeExaggerationTransition, HillshadeHighlightColorTransition, + HillshadeIlluminationAltitudeTransition, HillshadeIlluminationAnchorTransition, HillshadeIlluminationDirectionTransition, + HillshadeMethodTransition, HillshadeShadowColorTransition, }; @@ -258,14 +316,18 @@ constexpr const auto layerProperties = mapbox::eternal::hash_map HillshadeLayer::setPropertyInternal(const std::string& name auto property = static_cast(it->second); - if (property == Property::HillshadeAccentColor || property == Property::HillshadeHighlightColor || - property == Property::HillshadeShadowColor) { + if (property == Property::HillshadeAccentColor) { Error error; const auto& typedValue = convert>(value, error, false, false); if (!typedValue) { return error; } - if (property == Property::HillshadeAccentColor) { - setHillshadeAccentColor(*typedValue); - return std::nullopt; + setHillshadeAccentColor(*typedValue); + return std::nullopt; + } + if (property == Property::HillshadeExaggeration) { + Error error; + const auto& typedValue = convert>(value, error, false, false); + if (!typedValue) { + return error; + } + + setHillshadeExaggeration(*typedValue); + return std::nullopt; + } + if (property == Property::HillshadeHighlightColor || property == Property::HillshadeShadowColor) { + Error error; + const auto& typedValue = convert>>(value, error, false, false); + if (!typedValue) { + return error; } if (property == Property::HillshadeHighlightColor) { @@ -348,15 +432,15 @@ std::optional HillshadeLayer::setPropertyInternal(const std::string& name return std::nullopt; } } - if (property == Property::HillshadeExaggeration || property == Property::HillshadeIlluminationDirection) { + if (property == Property::HillshadeIlluminationAltitude || property == Property::HillshadeIlluminationDirection) { Error error; - const auto& typedValue = convert>(value, error, false, false); + const auto& typedValue = convert>>(value, error, false, false); if (!typedValue) { return error; } - if (property == Property::HillshadeExaggeration) { - setHillshadeExaggeration(*typedValue); + if (property == Property::HillshadeIlluminationAltitude) { + setHillshadeIlluminationAltitude(*typedValue); return std::nullopt; } @@ -375,6 +459,16 @@ std::optional HillshadeLayer::setPropertyInternal(const std::string& name setHillshadeIlluminationAnchor(*typedValue); return std::nullopt; } + if (property == Property::HillshadeMethod) { + Error error; + const auto& typedValue = convert>(value, error, false, false); + if (!typedValue) { + return error; + } + + setHillshadeMethod(*typedValue); + return std::nullopt; + } Error error; std::optional transition = convert(value, error); @@ -397,6 +491,11 @@ std::optional HillshadeLayer::setPropertyInternal(const std::string& name return std::nullopt; } + if (property == Property::HillshadeIlluminationAltitudeTransition) { + setHillshadeIlluminationAltitudeTransition(*transition); + return std::nullopt; + } + if (property == Property::HillshadeIlluminationAnchorTransition) { setHillshadeIlluminationAnchorTransition(*transition); return std::nullopt; @@ -407,6 +506,11 @@ std::optional HillshadeLayer::setPropertyInternal(const std::string& name return std::nullopt; } + if (property == Property::HillshadeMethodTransition) { + setHillshadeMethodTransition(*transition); + return std::nullopt; + } + if (property == Property::HillshadeShadowColorTransition) { setHillshadeShadowColorTransition(*transition); return std::nullopt; diff --git a/src/mbgl/style/layers/hillshade_layer_properties.hpp b/src/mbgl/style/layers/hillshade_layer_properties.hpp index a87526548e7a..498dfbeaf808 100644 --- a/src/mbgl/style/layers/hillshade_layer_properties.hpp +++ b/src/mbgl/style/layers/hillshade_layer_properties.hpp @@ -24,28 +24,38 @@ struct HillshadeExaggeration : PaintProperty { static float defaultValue() { return 0.5f; } }; -struct HillshadeHighlightColor : PaintProperty { - static Color defaultValue() { return Color::white(); } +struct HillshadeHighlightColor : PaintProperty> { + static std::vector defaultValue() { return {Color::white()}; } +}; + +struct HillshadeIlluminationAltitude : PaintProperty> { + static std::vector defaultValue() { return {45.f}; } }; struct HillshadeIlluminationAnchor : PaintProperty { static HillshadeIlluminationAnchorType defaultValue() { return HillshadeIlluminationAnchorType::Viewport; } }; -struct HillshadeIlluminationDirection : PaintProperty { - static float defaultValue() { return 335.f; } +struct HillshadeIlluminationDirection : PaintProperty> { + static std::vector defaultValue() { return {335.f}; } }; -struct HillshadeShadowColor : PaintProperty { - static Color defaultValue() { return Color::black(); } +struct HillshadeMethod : PaintProperty { + static HillshadeMethodType defaultValue() { return HillshadeMethodType::Standard; } +}; + +struct HillshadeShadowColor : PaintProperty> { + static std::vector defaultValue() { return {Color::black()}; } }; class HillshadePaintProperties : public Properties< HillshadeAccentColor, HillshadeExaggeration, HillshadeHighlightColor, + HillshadeIlluminationAltitude, HillshadeIlluminationAnchor, HillshadeIlluminationDirection, + HillshadeMethod, HillshadeShadowColor > {}; diff --git a/src/mbgl/style/layers/layer.cpp.ejs b/src/mbgl/style/layers/layer.cpp.ejs index 9121c96226b8..04438e8a36c0 100644 --- a/src/mbgl/style/layers/layer.cpp.ejs +++ b/src/mbgl/style/layers/layer.cpp.ejs @@ -77,6 +77,9 @@ layerCapabilities['hillshade'] = defaults.require('Source') .require('Pass3D') .set('TileKind', 'RasterDEM') .finalize(); +layerCapabilities['color-relief'] = defaults.require('Source') + .set('TileKind', 'RasterDEM') + .finalize(); layerCapabilities['symbol'] = defaults.require('Source') .require('Layout') .require('FadingTiles') diff --git a/src/mbgl/style/layers/location_indicator_layer.cpp b/src/mbgl/style/layers/location_indicator_layer.cpp index b443cec97602..b3becaa96b8e 100644 --- a/src/mbgl/style/layers/location_indicator_layer.cpp +++ b/src/mbgl/style/layers/location_indicator_layer.cpp @@ -194,7 +194,7 @@ TransitionOptions LocationIndicatorLayer::getAccuracyRadiusColorTransition() con } PropertyValue LocationIndicatorLayer::getDefaultBearing() { - return {0}; + return {0.f}; } const PropertyValue& LocationIndicatorLayer::getBearing() const { diff --git a/src/mbgl/style/layers/location_indicator_layer_properties.hpp b/src/mbgl/style/layers/location_indicator_layer_properties.hpp index e41979fefd5b..fb74576592a8 100644 --- a/src/mbgl/style/layers/location_indicator_layer_properties.hpp +++ b/src/mbgl/style/layers/location_indicator_layer_properties.hpp @@ -44,7 +44,7 @@ struct AccuracyRadiusColor : PaintProperty { }; struct Bearing : PaintProperty { - static Rotation defaultValue() { return 0; } + static Rotation defaultValue() { return 0.f; } }; struct BearingImageSize : PaintProperty { diff --git a/src/mbgl/style/light.cpp b/src/mbgl/style/light.cpp index f3a4c7f88680..4d7b2c93d5ad 100644 --- a/src/mbgl/style/light.cpp +++ b/src/mbgl/style/light.cpp @@ -17,16 +17,11 @@ namespace mbgl { namespace style { -namespace { -LightObserver nullObserver; -} +static LightObserver nullObserver; -Light::Light(Immutable impl_) - : impl(std::move(impl_)), - observer(&nullObserver) {} +Light::Light(Immutable impl_) : impl(std::move(impl_)), observer(&nullObserver) {} -Light::Light() - : Light(makeMutable()) {} +Light::Light() : Light(makeMutable()) {} Light::~Light() = default; @@ -73,84 +68,89 @@ constexpr const auto properties = mapbox::eternal::hash_map Light::setProperty(const std::string& name, const Convertible& value) { const auto it = properties.find(name.c_str()); if (it == properties.end()) { - return Error{"light doesn't support this property"}; + return Error { "light doesn't support this property" }; } auto property = static_cast(it->second); + if (property == Property::Anchor) { Error error; - std::optional> typedValue = convert>( - value, error, false, false); + std::optional> typedValue = convert>(value, error, false, false); if (!typedValue) { return error; } - + setAnchor(*typedValue); return std::nullopt; + } - + if (property == Property::Color) { Error error; std::optional> typedValue = convert>(value, error, false, false); if (!typedValue) { return error; } - + setColor(*typedValue); return std::nullopt; + } - + if (property == Property::Intensity) { Error error; std::optional> typedValue = convert>(value, error, false, false); if (!typedValue) { return error; } - + setIntensity(*typedValue); return std::nullopt; + } - + if (property == Property::Position) { Error error; - std::optional> typedValue = convert>( - value, error, false, false); + std::optional> typedValue = convert>(value, error, false, false); if (!typedValue) { return error; } - + setPosition(*typedValue); return std::nullopt; + } + Error error; std::optional transition = convert(value, error); if (!transition) { return error; } - + if (property == Property::AnchorTransition) { setAnchorTransition(*transition); return std::nullopt; } - + if (property == Property::ColorTransition) { setColorTransition(*transition); return std::nullopt; } - + if (property == Property::IntensityTransition) { setIntensityTransition(*transition); return std::nullopt; } - + if (property == Property::PositionTransition) { setPositionTransition(*transition); return std::nullopt; } + - return Error{"light doesn't support this property"}; + return Error { "light doesn't support this property" }; } StyleProperty Light::getProperty(const std::string& name) const { @@ -284,5 +284,6 @@ TransitionOptions Light::getPositionTransition() const { return impl->properties.template get().options; } + } // namespace style } // namespace mbgl diff --git a/src/mbgl/util/color.cpp b/src/mbgl/util/color.cpp index cd4258aa44ad..18bdcfd45839 100644 --- a/src/mbgl/util/color.cpp +++ b/src/mbgl/util/color.cpp @@ -6,7 +6,39 @@ namespace mbgl { std::optional Color::parse(const std::string& s) { - const auto css_color = CSSColorParser::parse(s); + std::string colorString = s; + + // --- START: FIX to support #RGBA (4-digit hex) --- + // Check for the 4-digit hex format: #RGBA (length 5, including '#') + if (colorString.length() == 5 && colorString[0] == '#') { + // Convert #RGBA to rgba() format since CSSColorParser doesn't support 8-digit hex + auto hexToInt = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return 0; + }; + + int r = hexToInt(colorString[1]); + int g = hexToInt(colorString[2]); + int b = hexToInt(colorString[3]); + int a = hexToInt(colorString[4]); + + // Expand from 0-15 range to 0-255 range + r = (r << 4) | r; + g = (g << 4) | g; + b = (b << 4) | b; + a = (a << 4) | a; + + // Convert to rgba() format with alpha in 0-1 range + colorString = "rgba(" + util::toString(r) + "," + + util::toString(g) + "," + + util::toString(b) + "," + + util::toString(a / 255.0) + ")"; + } + // --- END: FIX to support #RGBA (4-digit hex) --- + + const auto css_color = CSSColorParser::parse(colorString); // Premultiply the color. if (css_color) { @@ -23,6 +55,10 @@ std::string Color::stringify() const { util::toString(array[3]) + ")"; } +mbgl::Value Color::serialize() const { + return toObject(); +} + std::array Color::toArray() const { if (a == 0) { return {{0, 0, 0, 0}}; @@ -43,15 +79,4 @@ mbgl::Value Color::toObject() const { {"a", static_cast(a)}}; } -mbgl::Value Color::serialize() const { - std::array array = toArray(); - return std::vector{ - std::string("rgba"), - array[0], - array[1], - array[2], - array[3], - }; -} - } // namespace mbgl