---
title: "<ScriptMapLibreGeoJson>"
description: "添加一个 GeoJSON 源和一个或多个 MapLibre 样式图层。替换 data 会更新现有源。更改 paint、layout 或 filter 会就地更新图层。更改 cluster、clusterRadius 或 clusterMaxZoom 会就地更新源。更改其他图层或源选项，则会按受控顺序重建源和图层。"
canonical_url: "https://nuxt-scripts.zhcndoc.com/scripts/maplibre/api/geojson"
last_updated: "2026-09-23T10:33:14.926Z"
---

添加一个 GeoJSON 源和一个或多个 MapLibre 样式图层。替换 `data` 会更新现有源。更改 paint、layout 或 filter 会就地更新图层。更改 `cluster`、`clusterRadius` 或 `clusterMaxZoom` 会就地更新源。更改其他图层或源选项，则会按受控顺序重建源和图层。

更改 `source-id` 会先移除之前由组件管理的源和图层，然后在新的 ID 下重新创建它们。

::script-types{script-key="maplibre" filter="ScriptMapLibreGeoJson"}
::

除非图层明确指定了其他源，否则每个图层都会使用组件的 `source-id`。地图样式发生变化后，组件也会恢复其源和图层。

## 指针事件

组件会为其自身的图层触发 `click`、`dblclick`、`mouseenter`、`mousemove` 和 `mouseleave`。每个事件都包含 `features`，并按从上到下的顺序排列。

MapLibre 会将这些图层视为一个组。指针进入该组时会触发 `mouseenter`。指针离开所有要素时会触发 `mouseleave`。指针从一个要素滑到与之相接的要素上时，两者都不会触发。

要跟踪指针下方的要素，请在 `mousemove` 时读取它，并在 `mouseleave` 时清除它：

```vue
<script setup lang="ts">
import type { MapLayerMouseEvent } from 'maplibre-gl'

const hoveredId = ref<string | number>()

function onMove(event: MapLayerMouseEvent) {
  hoveredId.value = event.features?.[0]?.id
}
</script>

<template>
  <ScriptMapLibreGeoJson
    source-id="depots"
    :data="depots"
    :layers="depotLayers"
    cursor="pointer"
    @mousemove="onMove"
    @mouseleave="hoveredId = undefined"
  />
</template>
```

### 双击

双击也会触发 MapLibre 的双击缩放。在 `dblclick` 处理程序中调用 `event.preventDefault()`{lang="ts"} 可阻止缩放。

MapLibre 会停止每个在其自身事件处理程序内开始的相机移动。要用自己的移动替换缩放，请在下一个 tick 开始移动：

```ts
function onDoubleClick(event: MapLayerMouseEvent) {
  event.preventDefault()
  nextTick(() => mapRef.value?.easeTo({ center: event.lngLat, zoom: 14 }))
}
```

## 恢复要素状态

样式切换会移除所有源，MapLibre 也会随之清除要素状态。组件会重新添加其源和图层，然后触发 `sourceready`。首次添加后以及每次重建后，它也会触发 `sourceready`。

在该处理程序中恢复要素状态。地图的 `styleload` 触发早于源恢复，因此此时还太早。

```vue
<script setup lang="ts">
import type { ScriptMapLibreGeoJsonEmits } from '@nuxt/scripts'

function onSourceReady({ map, sourceId }: ScriptMapLibreGeoJsonEmits['sourceready'][0]) {
  if (selectedId.value !== undefined)
    map.setFeatureState({ source: sourceId, id: selectedId.value }, { selected: true })
}
</script>

<template>
  <ScriptMapLibreGeoJson
    source-id="depots"
    :data="depots"
    :layers="depotLayers"
    @sourceready="onSourceReady"
  />
</template>
```

## 错误

`error` 触发事件会报告此组件无法应用的源或图层。

对于无效的 paint、layout 或 filter 值，MapLibre 不会抛出异常。它会跳过该值，并在地图上触发 `error` 事件。组件会捕获该事件并将其触发，因此表达式中的拼写错误不会导致图层空白却毫无提示。

- 如果首次添加或重建失败，组件会移除其自身的源和图层。
- 如果 paint、layout 或 filter 更新失败，图层会保留在地图上，并继续使用最后一个有效值。
- 如果集群选项更新失败，源会保留在地图上。下次更改选项时会重建该源。
- 组件只会触发其自身源和图层的错误。底图、瓦片和其他图层的错误会通过 `<ScriptMapLibreMap>`{lang="html"} 上的 `error` 触发事件传递。

```vue
<ScriptMapLibreGeoJson
  source-id="depots"
  :data="depots"
  :layers="depotLayers"
  @error="error => layerError = error.message"
/>
```

验证消息会指出对应的值，例如 `layers.depot-point.paint.circle-color: color expected, array found`。

组件有意不负责远程获取数据。请使用 `useFetch` 获取并验证数据，然后将得到的对象传给 `data`，以明确保留加载和失败状态。

## 源选项

`sourceOptions` 是 [MapLibre GeoJSON 源规范](https://maplibre.org/maplibre-style-spec/sources/#geojson)，但不包含 `type` 和 `data`。这两项由组件提供，其余选项由你自行设置：

| 选项                  | 类型                    | 作用                                                  |
| ------------------- | --------------------- | --------------------------------------------------- |
| `cluster`           | `boolean`             | 将附近的点合并为集群要素。                                       |
| `clusterRadius`     | `number`              | 集群半径，单位为像素。默认值为 `50`。                               |
| `clusterMaxZoom`    | `number`              | 仍会进行集群处理的最高缩放级别。默认值比 `maxzoom` 小一级。                 |
| `clusterMinPoints`  | `number`              | 形成集群所需的点数。默认值为 `2`。                                 |
| `clusterProperties` | `object`              | 每个集群要素上的聚合属性。                                       |
| `promoteId`         | `string \| object`    | 用作要素状态要素 ID 的属性。                                    |
| `generateId`        | `boolean`             | 根据数组索引为每个要素分配 ID。第一个 ID 为 `0`。要素状态优先使用 `promoteId`。 |
| `filter`            | `FilterSpecification` | 在切片前过滤要素。                                           |
| `maxzoom`           | `number`              | 仍会生成瓦片的最高缩放级别。默认值为 `18`。                            |
| `buffer`            | `number`              | 瓦片缓冲区，单位为像素。默认值为 `128`。                             |
| `tolerance`         | `number`              | 简化容差。默认值为 `0.375`。                                  |
| `lineMetrics`       | `boolean`             | 使用 `line-gradient` 的线图层必需。                          |
| `attribution`       | `string`              | 显示在此源上的署名文本。                                        |

## 集群

在 `sourceOptions` 中设置 `cluster: true`。MapLibre 随后会向每个集群要素添加 `cluster`、`cluster_id`、`point_count` 和 `point_count_abbreviated`。使用三个图层渲染集群和单个点：

- 使用 `['has', 'point_count']` 过滤的 `circle` 图层，用于绘制集群气泡，
- 使用相同过滤条件的 `symbol` 图层，用于显示计数标签，
- 使用 `['!', ['has', 'point_count']]` 过滤的 `circle` 图层，用于绘制未聚类的点。

`getClusterExpansionZoom` 会返回集群拆分时的缩放级别。在 `ready` 处理程序中从源读取该值，然后将相机平滑移动到该位置。

```vue
<script setup lang="ts">
import type { FeatureCollection, Point } from 'geojson'
import type {
  CircleLayerSpecification,
  GeoJSONSource,
  Map as MapLibreMap,
  SymbolLayerSpecification,
} from 'maplibre-gl'
import type { ShallowRef } from 'vue'

type DepotLayer = Omit<CircleLayerSpecification, 'source'> | Omit<SymbolLayerSpecification, 'source'>

const depots: FeatureCollection<Point> = {
  type: 'FeatureCollection',
  features: [
    { type: 'Feature', properties: { depotId: 'west-melbourne', parcels: 12 }, geometry: { type: 'Point', coordinates: [144.9495, -37.8101] } },
    { type: 'Feature', properties: { depotId: 'docklands', parcels: 4 }, geometry: { type: 'Point', coordinates: [144.9538, -37.8151] } },
    { type: 'Feature', properties: { depotId: 'southbank', parcels: 9 }, geometry: { type: 'Point', coordinates: [144.9632, -37.8227] } },
    { type: 'Feature', properties: { depotId: 'flinders-lane', parcels: 7 }, geometry: { type: 'Point', coordinates: [144.9687, -37.8154] } },
    // ...more depots
  ],
}

const depotSourceOptions = {
  cluster: true,
  clusterRadius: 60,
  clusterMaxZoom: 14,
  clusterProperties: {
    parcels: ['+', ['get', 'parcels']],
  },
}

const depotLayers: DepotLayer[] = [
  {
    id: 'depot-clusters',
    type: 'circle',
    filter: ['has', 'point_count'],
    paint: {
      'circle-color': '#2563eb',
      'circle-opacity': 0.85,
      'circle-radius': ['step', ['get', 'point_count'], 16, 10, 22, 50, 30],
    },
  },
  {
    id: 'depot-cluster-count',
    type: 'symbol',
    filter: ['has', 'point_count'],
    layout: {
      'text-field': ['get', 'point_count_abbreviated'],
      'text-font': ['Noto Sans Bold'],
      'text-size': 12,
    },
    paint: { 'text-color': '#ffffff' },
  },
  {
    id: 'depot-point',
    type: 'circle',
    filter: ['!', ['has', 'point_count']],
    paint: {
      'circle-color': '#f97316',
      'circle-radius': 7,
      'circle-stroke-color': '#ffffff',
      'circle-stroke-width': 2,
    },
  },
]

function onMapReady({ map }: { map: ShallowRef<MapLibreMap | undefined> }) {
  const instance = map.value
  if (!instance)
    return

  instance.on('click', 'depot-clusters', async (event) => {
    const feature = event.features?.[0]
    const source = instance.getSource<GeoJSONSource>('depots')
    if (!feature || !source)
      return

    const zoom = await source.getClusterExpansionZoom(feature.properties.cluster_id as number)
    instance.easeTo({
      center: (feature.geometry as Point).coordinates as [number, number],
      zoom,
    })
  })
}
</script>

<template>
  <ScriptMapLibreMap
    :center="[144.9631, -37.8136]"
    map-style="https://tiles.openfreemap.org/styles/liberty"
    :zoom="11"
    width="100%"
    :height="480"
    aria-label="Depot locations across Melbourne"
    @ready="onMapReady"
  >
    <ScriptMapLibreGeoJson
      source-id="depots"
      :data="depots"
      :source-options="depotSourceOptions"
      :layers="depotLayers"
    />
  </ScriptMapLibreMap>
</template>
```

::callout{icon="i-heroicons-exclamation-triangle" color="amber"}
将 `layers` 和 `source-options` 声明为常量。如果在模板中内联编写其中任意一项，每次父组件重新渲染时都会传入一个新的数组或对象。随后组件会移除并重新添加源。这会丢弃集群索引，并导致地图闪烁。
::

::callout{icon="i-heroicons-information-circle"}
`text-font` 必须指定瓦片服务提供的字体栈。OpenFreeMap 提供 `Noto Sans Regular`、`Noto Sans Bold` 和 `Noto Sans Italic`。如果字形请求失败，MapLibre 会改用本地浏览器字体绘制文本。标签仍会显示，但字体不正确。请在控制台中查找 `Unable to load glyph range`。
::

### 更改集群选项

MapLibre 可以在运行中的源上更新三个集群选项：`cluster`、`clusterRadius` 和 `clusterMaxZoom`。如果只更改这些选项，组件会调用 `setClusterOptions()`{lang="ts"}。源会保留其数据，图层也会继续留在地图上。

更改其他任何 `sourceOptions` 选项都会重建源，包括 `clusterMinPoints` 和 `clusterProperties`。移除 `clusterRadius` 或 `clusterMaxZoom` 也会重建源，因为缺少选项时 MapLibre 会保留旧值。

```vue
<script setup lang="ts">
const radius = ref(60)
const depotSourceOptions = computed(() => ({ cluster: true, clusterRadius: radius.value }))
</script>

<template>
  <input v-model.number="radius" type="range" min="10" max="120">
  <ScriptMapLibreMap map-style="https://tiles.openfreemap.org/styles/liberty" :center="[144.9631, -37.8136]">
    <ScriptMapLibreGeoJson
      source-id="depots"
      :data="depots"
      :source-options="depotSourceOptions"
      :layers="depotLayers"
    />
  </ScriptMapLibreMap>
</template>
```

MapLibre 会在工作线程中应用新选项，因此更新会稍后完成。MapLibre 一次只运行一个工作线程更新，组件会跟踪每个正在运行的更新所携带的操作。如果集群更新失败，组件会触发一次 `error`。随后更改选项时会重建源。如果较新的更改取代了某次更新，组件会忽略较早更新的失败。其他操作（例如 `data` 更改）失败时，仍会触发其自身的 `error`。

集群检查通过源进行。`getClusterExpansionZoom`、`getClusterChildren` 和 `getClusterLeaves` 都需要原始地图。[原始地图实例](/scripts/maplibre/guides/raw-map-instance)对此进行了介绍，还涵盖图层点击、悬停状态和相机。

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
