迪图
发布于 2026-10-20 / 15 阅读
0
0

MapLibre 样式表达式完全指南:把地图外观变成可配置的数据

MapLibre 样式表达式完全指南:把地图外观变成可配置的数据

样式即产品。这份指南会让你彻底摆脱"改个配色要发版"的困境。

为什么表达式是现代地图的关键

在表达式出现之前,做动态样式只有一条路:在运行时改图层配置。

// 老做法
map.setPaintProperty('water', 'fill-color', '#000080');

问题很明显:每个需要动态区分的类目都要写一遍分支,数据一变就要重新执行代码,样式逻辑和业务逻辑纠缠在一起。

表达式的意义在于把样式规则本身变成数据。你写一次规则,引擎在渲染每个要素时求值:

{
  "paint": {
    "fill-color": [
      "interpolate", ["linear"], ["get", "population"],
      0, "#f0f9e8",
      1000000, "#7bccc4"
    ]
  }
}

这一段 JSON 就完成了一个连续色彩映射:人口少的浅绿,人口多的深青。不需要任何 JavaScript。

表达式的语法结构

表达式是 JSON 数组,第一个元素是操作符名称,其后是参数:

["operator_name", argument_0, argument_1, ...]

每个参数要么是字面量(字符串、数字、布尔、null),要么是另一个表达式数组——所以表达式可以任意嵌套。

五大类操作符:

类别

用途

数学运算

算术、比较、逻辑

字符串运算

拼接、格式化

数据运算

get、has、id、geometry-type、properties、feature-state

相机运算

zoom,以及隐含的 pitch / bearing

断言与类型转换

number、string、boolean 等

数据表达式

数据表达式访问要素自身的数据,是数据驱动样式(DDS)的基础。

{
  "circle-color": [
    "rgb",
    ["get", "temperature"],      // 温度越高红色越强
    0,                           // 绿色恒为 0
    ["-", 100, ["get", "temperature"]]  // 温度越低蓝色越强
  ]
}

数据表达式可以用于 filter 属性,以及绝大多数 paint 和 layout 属性。但不是全部——规范里专门标注了哪些属性支持数据表达式(data-driven styling 一栏),要用的时候查一下。

常见的例外:line-dasharray、text-line-height、line-cap 目前只支持随 zoom 变化,不支持数据表达式。

相机表达式:随缩放变化

用 zoom 让外观随缩放级别演化:

{
  "circle-radius": [
    "interpolate", ["linear"], ["zoom"],
    5, 1,     // zoom ≤ 5 时半径 1px
    10, 5     // zoom ≥ 10 时半径 5px,中间线性插值
  ]
}

这里有一个非常容易踩的坑,官方文档特别强调过:

当相机表达式作为 paint 或 layout 属性的值时,它必须严格是以下四种形式之一:

["interpolate", interpolation, ["zoom"], ...]
["step", ["zoom"], ...]
["let", ...变量绑定..., ["interpolate", interpolation, ["zoom"], ...]]
["let", ...变量绑定..., ["step", ["zoom"], ...]]

也就是说,["zoom"] 只能作为最外层 interpolate 或 step 的输入,不能嵌套在别的表达式中间。

layout 与 paint 的求值时机差异

这是一个实际会影响表现的区别:

  • paint 属性的相机表达式:缩放级别变化时持续重新求值,包括小数。比如从 zoom 4.1 平滑移动到 4.6,圆点半径会连续变化

  • layout 属性的相机表达式:只在整数 zoom 级别求值。从 4.1 到 4.6 不会重新求值,只有超过 5 或低于 4 才会

因为 layout 影响几何布局(文字位置、面的大小),频繁重算代价高;paint 只影响着色,重算便宜。

实践建议:想让点随缩放平滑变大 → 用 circle-radius(paint);想让文字标签只在整数级别出现 → 用 text-size(layout)。

三个核心表达式

match:等值匹配

用于按属性值分支,功能上类似 switch:

["match", ["get", "type"],
  "highway", "#ff0000",
  "secondary", "#00ff00",
  "#0000ff"                              // 默认值
]

当输入是枚举值(类型、分类、等级)时,优先用 match 而不是 case——match 的求值效率更高,因为它是哈希匹配而非顺序求值。

case:多条件分支

条件判断,按顺序求值,第一个为真的条件决定结果:

["case",
  ["==", ["get", "type"], "forest"], "green",
  ["any",
    ["==", ["get", "type"], "industrial"],
    ["==", ["get", "type"], "residential"]], "orange",
  [">=", ["get", "population"], 100000], "blue",
  "pink"                                   // 默认值
]

读法:森林是绿色;工业或居住区是橙色;其余中人口 ≥10 万的蓝色;其余粉色。

注意顺序的重要性:居住区人口即使超过 10 万,仍然是橙色——因为居住区的判断在前。

step:分段取值

["step", ["zoom"],
  2,        // 默认值
  8, 4,     // zoom ≥ 8 时为 4
  12, 8     // zoom ≥ 12 时为 8
]

step 和 interpolate 的选择:

需求

用哪个

数值连续变化(半径、宽度、不透明度)

interpolate

离散跳变(图标切换、字体大小分级)

step

一个典型场景:低缩放显示一个简单小点,高缩放无闪烁地切换成完整图标——用 step,因为中间状态没有意义。

interpolate:连续插值

["interpolate", ["linear"], ["get", "value"],
  0,    "#ffffff",
  0.5,  "#ffffcc",
  1,    "#ff0000"
]

也支持指数/三次贝塞尔插值:

["interpolate", ["exponential", 1.5], ...]
["interpolate", ["cubic-bezier", 0.4, 0, 0.6, 1], ...]

在数据可视化里做热力色带、坡度分级、密度分级,interpolate 是主力工具。

let:绑定变量避免重复

表达式嵌套深了会很难读,let 可以绑定中间值:

["let",
  "temp", ["get", "temperature"],
  "pop",  ["get", "population"],
  ["case",
    [">", ["temp", 30], "#ff0000",
    [">", ["var", "pop"], 100000], "#ffaa00",
    "#0000ff"
  ]
]

["var", "name"] 引用绑定变量。规范还要求:相机表达式若通过 let 使用,zoom 仍必须是最外层 interpolate/step 的输入,即 ["let", ..., ["interpolate", ..., ["zoom"], ...]] 这个形式。

类型断言与转换

这两个操作符容易混淆,区别很重要:

断言(assertion)—— 类型不符则报错

["string", ["get", "feature_property"]]

如果属性值不是字符串,求值时抛错。它不转换类型,只是声明"我期望这里是字符串"。

转换(coercion)—— 类型不符则用默认值

["to-number", ["get", "feature_property"], 0]

尝试转成数字,失败则返回 0。

命名约定:形如 to-xxx 的是转换,形如 xxx(不加 to-)的是断言。

实际开发中的建议:处理可能缺失或类型不一致的外部数据时,用 to-number 带默认值,而不是用断言。让地图因为一个脏数据崩掉是不可接受的。

综合运用:数据 + 缩放 + 变量

一个真实的样式片段——道路按等级分色,同时线宽随缩放变化:

{
  "id": "roads",
  "type": "line",
  "source": "osm",
  "source-layer": "transportation",
  "paint": {
    "line-color": [
      "match", ["get", "class"],
      "motorway", "#e74c3c",
      "trunk",     "#e67e22",
      "primary",   "#f1c40f",
      "secondary", "#95a5a6",
      "#7f8c8d"
    ],
    "line-width": [
      "interpolate", ["linear"], ["zoom"],
      8,  ["match", ["get", "class"], "motorway", 2, 1],
      16, ["match", ["get", "class"], "motorway", 8, 4]
    ],
    "line-opacity": [
      "interpolate", ["linear"], ["zoom"],
      10, 0.6,
      14, 1
    ]
  }
}

数据驱动颜色、缩放驱动宽度,两者完全解耦。想调配色?改 line-color。想调线宽?改 line-width。互不影响,也不需要碰 JavaScript。

这就是样式表达式的真正价值。

filter 也可以是表达式

别忘了过滤器本身支持完整表达式能力:

{
  "id": "big-roads-only",
  "type": "line",
  "source": "osm",
  "filter": [
    "all",
    ["==", ["get", "class"], "motorway"],
    [">", ["get", "lanes"], 4]
  ]
}

all / any / none 用于组合条件,in 用于集合判断:

["in", ["get", "type"], ["literal", ["industrial", "residential"]]]

literal 是必需的——没有它,数组会被解释为表达式而不是字面量数组。这是新手最常见的报错来源。

常见错误速查

现象

原因

样式不生效,无报错

用了不支持数据表达式的属性(如 line-dasharray)

表达式解析失败

数组字面量漏了 ["literal", ...]

类型不匹配报错

用了断言(string)而非转换(to-number 带默认值)

缩放时行为怪异

["zoom"] 没有作为最外层 interpolate/step 的输入

要素渲染但颜色是默认的

属性名拼错,get 返回 undefined,case/match 落到默认分支

调试建议:先用 ["get", "xxx"] 单独作为颜色值,如果要素变成某个异常颜色,说明属性取到了;如果没变化,说明属性不存在或名称不对。

参考资料

  • MapLibre Expressions 官方文档 — https://www.maplibre.org/maplibre-style-spec/expressions

  • MapLibre Style Specification — https://www.maplibre.org/maplibre-style-spec/

  • MapLibre GL JS 官方站点 — https://maplibre.org/

  • MapLibre Native Expressions 设计文档 — https://maplibre.org/maplibre-native/docs/book/design/expressions.html


评论