# Layers 图层类

## Layers（内部模块）

路径 `src/layers/`。由 `_getLayer` 按 `type` 创建。下列为高频 API。

### CloudLayer

* **归属**：融合 — `extends ol/layer/Vector` + `Object.assign(this, options)`

#### 构造 options 常用字段

| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | `string` | 图层编码 |
| `minZoom` | `number` | 点可见最小级别 |
| `label` | `Object` | `{ minZoom, field, hidden… }` |
| `cloudMinZoom` | `number` | 飘窗最小级别 |
| `cloudDirection` | `string` | 如 `'bottom-center'` |
| `cloudOffset` | `number[]` | 像素偏移 |
| `cloudHover` | `boolean` | 悬停显示（会注册 pointermove） |
| `alarmColor` / `alarmColors` | `string` / `Object` | 告警色 |
| `cloudClass` | `string` | 飘窗 CSS 类 |

#### 方法

| 方法 | 参数 | 说明 |
|------|------|------|
| `addGraphics(data)` | `Array<Object>` | 加点 |
| `removeGraphics(data)` | `Array` | 删点 |
| `refreshData(data)` | `*` | 刷新 |
| `refreshAlarm(objCodes)` | `Array` | 告警 |
| `showGraphics(codes)` | `Array` | 仅显示指定 code |
| `displayPart(data)` | `Array\|Object` | 条件显示 |
| `addCloud(cloudData)` | `Object` | 飘窗 |
| `hideCloud` / `showCloud` | `objCode` | 单个飘窗 |
| `hideClouds` / `showClouds` | — / `objCodes` | 批量 |
| `removeCloud` / `removeAllClouds` | `objCode?` | 移除飘窗 |
| `changeStatus(status)` | `*` | 状态样式 |
| `clear` / `clearGraphics` | — | 清空 |
| `setVisible(bool)` | `boolean` | 显隐（扩展 OL） |

```js
const layer = map.layers.hydrant;
layer.addGraphics([{ objCode: 'H1', gpsX: 13528430, gpsY: 3676466 }]);
layer.setVisible(true);
```

### ClusterLayer

* **归属**：融合 — `extends CloudLayer`（进而 `ol/layer/Vector`）
* **业务 type**：`'12'` / `'ClusterLayer'`
* **说明**：在 CloudLayer 点数据之上，按 `unitCode` 建多组 **OL** `Cluster` 源做聚合展示；可在聚合模式与散点模式间切换。

#### new ClusterLayer(options, main)

* **参数**：
  * `{Object} options` — 见下表（并继承 CloudLayer 选项）
  * `{Map} main` — 业务地图
* **常用 options**：

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `clusterStatus` | `boolean` | `false` | 初始是否开聚合 |
| `distance` | `number` | `100` | 聚合像素距离（传给 `ol/source/Cluster`） |
| `clusterBreaks` | `number[]` | `[40,80,160,320]` | 聚合数量分级断点（内部会补 `1` 与上限） |
| `clusterColor` | `string` | 随配置 | 聚合圆颜色（如 `'50,149,172'` 或 `#…`） |
| `labelColoe` | `string` | `'#fff'` | 聚合文字色（源码字段名保持拼写） |
| `labels` | `string[]` | 内置字号档 | 各级字号，如 `['12px','14px',…]` |
| `stopZoom` | `number` | — | 大于该级别分辨率时切回散点层逻辑 |
| `color` / `path` | `string` | — | 自定义聚合图标/颜色样式 |

#### 方法

##### addGraphics(data)

* **类型**：`Function`
* **参数**：
  * `{Array<Object>} data` — 点列表。常用字段：
    * `{string\|number} objCode` — 主键（作 Feature id）
    * `{number} objX` / `{number} objY` — 坐标（有自定义投影时会转到 3857）
    * `{number} [minZoom]` — 单点最小级别
    * `{string} [unitCode]` — 聚合分组键，默认 `'other'`
    * `{string} [innerHTML]` — 有则同时建飘窗
    * `{string} [objName]` — 名称（单点聚合时展示）
* **返回值**：无
* **用法**：

```js
map.layers.device.addGraphics([
  { objCode: 'D1', objX: 13528430, objY: 3676466, unitCode: 'A', objName: '设备1' },
  { objCode: 'D2', objX: 13528500, objY: 3676500, unitCode: 'A', objName: '设备2' }
]);
```

##### setVisible(bool)

* **类型**：`Function`
* **参数**：`{boolean} bool`
* **说明**：同步本层与各 `unitCode` 聚合子层的可见性；聚合开启时主 CloudLayer 可能隐藏、子 Cluster 层显示。
* **相关 OL**：`layer.set('visible', …)` / `setVisible`

##### displayPart(data)

* **类型**：`Function`
* **参数**：
  * `{Array} data` — 写入 `partCondition`，结构与 CloudLayer 条件显示一致；`data[0]` 可为 `unitCode` 过滤键，`data[1]` 为是否仅显示告警等
* **返回值**：无

##### changeStatus(bool)

* **类型**：`Function`
* **参数**：`{boolean} bool` — `true` 开聚合，`false` 回散点
* **返回值**：无
* **用法**：

```js
map.layers.device.changeStatus(true);  // 聚合
map.layers.device.changeStatus(false); // 散点
```

---

### HistoryDispatch

* **归属**：融合 — `extends CloudLayer`
* **说明**：历史调度回放层。由 `map.historyDispatch(options)` 创建，构造时注入 `dispatchGraphics`、`map`、`layers`、飘窗模板等。

#### new HistoryDispatch(options, main)

* **参数**：
  * `{Object} options`
    * `{Array<ol/Feature>} options.dispatchGraphics` — 调度点要素（用于取 geometry）
    * `{Object} [options.layers]` — 业务图层字典，供 `play` 时 `refreshData`
    * `{Object} [options.cloudData]` — 飘窗基础配置
    * `{string} [options.innerHTML]` — 飘窗 HTML 模板，支持 `{objName}` `{levelAdjust}` `{levelSet}` `{levelSetPre}` `{createDate}` `{judgeInfo}` 占位
    * `{ol/Map} [options.map]`
  * `{Map} main`

#### 方法

##### setData(data)

* **参数**：
  * `{Array<Object>} data` — 按时间帧排列的调度数据；含 `dispatch` 数组时会整理为飘窗 clouds
* **返回值**：无

##### play(index)

* **参数**：
  * `{number} index` — 帧下标
* **说明**：若帧含 `dispatch`，清空并重建飘窗；其它 key 对 `this.layers[key].refreshData(...)`。
* **用法**：

```js
const hd = map.historyDispatch({
  layerCodes: ['vehicle'],
  cloudData: { cloudDirection: 'top-center' },
  innerHTML: '<div>{objName} {levelSet}</div>'
});
hd.setData(frames);
hd.play(0);
```

##### destroy()

* **参数**：无
* **说明**：清空 `data` 并 `removeAllClouds()`。

---

### GLayer

* **归属**：融合 — `extends ol/layer/Vector`
* **业务 type**：`'11'` / `'GLayer'`
* **说明**：通用矢量绘图层，可手动加点/线/面，也可从 GeoServer WFS 或 JSON 拉数；样式支持扁平字段归一化后转 **OL** `Style`。

#### new GLayer(options, main)

* **参数**：
  * `{Object} options`
    * `{string} [options.addType]` — `'geoserver'` | `'json'` | 省略（空 VectorSource）
    * `{string} [options.serviceAddress]` — geoserver workspace 或 json url
    * `{string} [options.layer]` / `{string} [options.code]` — WFS typeName 后缀
    * `{string} [options.animate]` — 如 `'arrow'` / `'arrowFlow'` / 其它动画类型
    * 以及线宽、颜色等样式字段（见 `_getStyles` / 点线面 styleParam）
  * `{Map} main`

```js
map.addGLayer({ code: 'sketch', type: 'GLayer' });
// 或 initLayers 中 type: '11'
```

#### 方法

##### setData(res, addType?)

* **参数**：
  * `{Object|string} res` — GeoJSON（`ol/format/GeoJSON.readFeatures`）
  * `{string} [addType]` — 保留参数位
* **返回值**：无

##### addPoint(data, styleParam?, text?, attributes?)

* **参数**：
  * `{number[]} data` — `[x, y]`（有自定义投影时先转到 3857）
  * `{Object} [styleParam]` — 点样式，常用：
    * `{string} color` / `circleColor` — 填充色
    * `{number} size` / `circleRadius` — 半径，默认 `6`
    * `{string} outColor` / `{number} outWidth` — 描边
    * `{string} icon` / `{number} iconScale` — 图标
    * `{number} textSize` — 文字字号
  * `{string} [text]` — 标注文字
  * `{Object} [attributes]` — Feature 属性
* **返回值**：无

##### addPoints(data, styleParam?)

* **参数**：
  * `{Array<number[]|Object>} data` — 多个点坐标（实现上按 `_addPoint` 消费；传坐标数组更稳妥）
  * `{Object} [styleParam]` — 同 `addPoint`
* **用法**：

```js
map.layers.sketch.addPoints(
  [
    [13528430, 3676466],
    [13528500, 3676500]
  ],
  { color: '#2563eb', size: 6 }
);
map.layers.sketch.addPoint([13528600, 3676600], { color: '#f00', size: 8 }, 'A', { objCode: 'A1' });
```

##### addPolyline(data, styleParam?, animateType?, attributes?)

* **参数**：
  * `{number[][]} data` — 折线坐标 `[[x,y], …]`
  * `{Object} [styleParam]` — `{ color|lineColor, width|lineWidth }`
  * `{string} [animateType]` — 传给 `animateFeatue`，如 `'flow'`
  * `{Object} [attributes]`
* **返回值**：无

##### addPolylines(data, styleParam?, animateType?)

* **参数**：`{Array<number[][]>} data` — 多条线；其余同上。

##### addPolygon(data, style?, text?)

* **参数**：
  * `{number[][]} data` — 环坐标（不闭合也可，由 `Polygon` 处理）
  * `{Object} [style]` — `{ fillColor, lineColor, lineWidth, textSize }`
  * `{string} [text]` — 面标注
* **返回值**：无

```js
map.layers.sketch.addPolygon(
  [[13528400, 3676400], [13528600, 3676400], [13528600, 3676600], [13528400, 3676400]],
  { fillColor: 'rgba(37,99,235,0.2)', lineColor: '#2563eb', lineWidth: 2 },
  '区域'
);
```

##### clear()

* **参数**：无
* **说明**：`getSource().clear()`。

##### addFilters / resume

* **说明**：从 `VectorTileLayer` 原型挂到实例上，用法与矢量瓦片过滤/恢复默认样式一致。

---

### ContourLayer

* **归属**：融合 — `extends ol/layer/Vector`
* **业务 type**：`'13'` / `'ContourLayer'`
* **说明**：根据离散点值插值生成等值线/等值面（turf + 可选 kriging）。

#### new ContourLayer(options)

* **参数**：`{Object} options`（`Object.assign` 到实例）

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `valueField` | `string` | `'value'` | 取值字段 |
| `contourType` | `string` | `'line'` | `'line'` \| `'polygon'` \| `'square'` \| `'hex'` \| `'triangle'` |
| `distance` | `number` | `1` | 插值网格间距（公里） |
| `width` | `number` | `2` | 线宽 |
| `opacity` | `number` | `0.4` | 面填充透明度 |
| `precision` | `boolean` | `false` | `true` 走 kriging 网格 |
| `breaks` / `colors` | `number[]` / `string[]` | 内置色带 | 分级断点与颜色（长度须一致） |
| `layerId` | `string` | — | WFS typeName，用于拉裁剪范围 polygon |
| `data` | `Array` | — | 可在 `update` 时传入 |

#### 方法

##### update(options)

* **参数**：`{Object} options` — 更新样式；若含 `data` 则 `setData(options.data, 'data')`
* **返回值**：无

##### setData(features, addType?)

* **参数**：
  * `{Array} features` — 点源数据
  * `{string} [addType]`
    * `'data'`：业务对象，含 `objX`/`objY` 与 `valueField`
    * `'geoserver'`：GeoJSON feature（`geometry.coordinates` + `properties`）
    * 省略：OL Feature 数组
* **返回值**：无
* **用法**：

```js
map.addContour({
  code: 'pm25',
  type: 'ContourLayer',
  contourType: 'polygon',
  valueField: 'aqi',
  distance: 2
});
map.layers.pm25.setData([
  { objX: 13528430, objY: 3676466, aqi: 42 },
  { objX: 13529000, objY: 3677000, aqi: 88 }
], 'data');
```

##### setGridData(grid)

* **参数**：
  * `{Object} grid` — kriging 风格网格，含 `xlim`/`ylim` 与二维数值
* **返回值**：无

---

### HeatmapLayer

* **归属**：融合 — `extends ol/layer/Heatmap`
* **业务 type**：`'15'` / `'HeatmapLayer'`

#### new HeatmapLayer(options)

* **参数**：
  * `{Object} options`
    * `{number} [options.radius]` — 热力半径，默认 `20`
    * `{string} [options.valueField]` — 权重属性名；有则 `weight(feature) => feature.get(valueField)`

#### 方法

##### setData(features, addType?)

* **参数**：
  * `{Array} features`
  * `{string} [addType]` — `'data'` 时用 `objX`/`objY` 建 Point Feature；否则视为已有 OL Feature 数组
* **返回值**：无

##### refreshData(data)

* **参数**：`{Array<Object>} data` — 业务点数组
* **说明**：先 `clear` 再 `setData(data, 'data')`
* **用法**：

```js
map.layers.heat.refreshData([
  { objX: 13528430, objY: 3676466, weight: 0.8 },
  { objX: 13528500, objY: 3676500, weight: 0.3 }
]);
```

---

### GeoTIFFLayer

* **归属**：融合 — `extends ol/layer/WebGLTile`，数据源为 `ol/source/GeoTIFF`
* **业务 type**：`'16'` / `'GeoTIFFLayer'` / `'TifLayer'`
* **说明**：通过 **服务器 URL** 加载 `.tif` / COG 栅格。用法与 GeoServer、ArcGIS 图层一致：可在 `initLayers` 注册，也可用 `addLayer` 临时插入。加载后**不会**自动缩放到影像范围。大文件推荐 **COG**（Cloud Optimized GeoTIFF）；跨域需 CORS。建议 `parameter.queryable: false`，避免点查走 WFS。**单波段数据（水深等）必须配色**，否则接近纯黑。

#### 业务表 / parameter 怎么配

`initLayers` 会把 `parameter`（JSON 字符串或对象）展开合并到图层配置。表字段与 `parameter` 建议这样拆：

| 位置 | 字段 | 说明 |
|------|------|------|
| 表字段 | `code` | 图层 code |
| 表字段 | `type` | `'16'` |
| 表字段 | `serviceAddress` | `.tif` / COG 的 URL |
| `parameter` | `queryable` | **建议 `false`**，栅格不参与 WFS 点查 |
| `parameter` | 配色 / 投影 / 性能项 | 见下方各小节，均可写在 `parameter` 里 |

`parameter` 最小示例：

```json
{ "queryable": false }
```

水深着色完整示例（写入 GIS_LAYER_CONFIG.PARAMETER）：

```json
{
  "queryable": false,
  "wkid": 4549,
  "min": 0,
  "max": 5,
  "nodata": 0,
  "transparentBelow": 0,
  "opacity": 0.85,
  "colorRamp": [
    [0, "#00000000"],
    [0.1, "#c6dbef"],
    [0.5, "#6baed6"],
    [1, "#2171b5"],
    [2, "#08519c"],
    [3.5, "#fc9272"],
    [5, "#a50f15"]
  ]
}
```

#### 色带与值域（最重要）

三种配色方式**任选其一**（优先级：`style` > `colorRamp` > `colors` > `colorize`）。内部均使用 WebGL `interpolate` **线性插值**，不是“每个颜色一块、中间不混色”。

##### `min` / `max` / `nodata` / `transparentBelow`

| 字段 | 作用 |
|------|------|
| `min` / `max` | 色带数值范围；像素按该区间拉伸后再上色。小于 `min`、大于 `max` 会落到色带两端。 |
| `nodata` | 无数据标记值；等于该值的像元视为无效并透明。常与文件 nodata 一致（水深图常用 `0`）。 |
| `transparentBelow` | 小于等于该值的像元额外强制透明（压掉极浅水/噪声）。 |

##### 方式 1：`colorRamp`（推荐，按真实数值断点）

```js
colorRamp: [
  [0, '#00000000'],      // [数值, 颜色]
  [0.5, '#6baed6'],
  [2, '#2171b5'],
  [5, '#a50f15']
]
// 或对象写法：[{ value: 0, color: '#00000000' }, …]
```

颜色支持 `#rrggbb`、`#rrggbbaa`、`rgba()`、`[r,g,b,a]`。断点之间线性插值。

##### 方式 2：`colors`（颜色均匀铺在 min～max）

```js
min: 0,
max: 5,
colors: ['#00000000', '#c6dbef', '#2171b5', '#a50f15']
```

同样会在断点间线性插值。若画面仍呈大色块，多半是 **tif 本身为分类/离散值**（像元从 1 直接跳到 2），不是 `colors` 没做渐变。连续数值（0.1、0.3、1.2…）才会看到平滑过渡。

##### 方式 3：`colorize: true`（内置默认水深色带）

```js
min: 0,
max: 5,
nodata: 0,
colorize: true
```

也可 `colorize: [颜色数组]` 自定义默认带。

##### 看起来像色块、不够渐变时

1. 确认栅格是**连续数值**，不是分类码。  
2. 优先用 `colorRamp` 多设几个断点。  
3. 需要更柔的空间边界时加 `"interpolate": true`（或 `"fastResample": false`）。配色模式下默认偏 nearest，重投影更快但边缘更“像素块”。

#### new GeoTIFFLayer(options, main)

* **参数**：
  * `{Object} options`
    * `{string} [options.url]` — GeoTIFF 地址（优先）
    * `{string} [options.serviceAddress]` — 同 url（`initLayers` 配置习惯）
    * `{boolean} [options.queryable]` — 建议 `false`；也可写在 `parameter.queryable`
    * `{string|number} [options.wkid]` / `[options.projection]` — 源 CRS（如 `4549`）；地图为 3857 时会重投影
    * `{string} [options.projectionDef]` / `[options.wkidDef]` — 可选自定义 proj4 定义
    * `{boolean} [options.normalize]` — 有 `min`/`max` + 色带时默认倾向 `true`（Uint8 拉伸，更快）
    * `{number} [options.min]` / `{number} [options.max]` — 拉伸与色带值域
    * `{number|number[]} [options.nodata]` — 无数据；设置后色带用 alpha 波段透明化
    * `{Array} [options.colorRamp]` — `[[value, color], …]` 或 `[{ value, color }, …]`
    * `{Array} [options.colors]` — 在 min–max 上均匀取色（仍线性插值）
    * `{boolean|Array} [options.colorize]` — `true` 用默认水深色带；或直接传颜色数组
    * `{number} [options.transparentBelow]` — 小于等于该值透明
    * `{Object} [options.style]` — 原始 WebGLTile `style`（优先于 colorRamp）
    * `{number[]} [options.bands]` — 读取波段（从 1 起）；配色时默认 `[1]`
    * `{boolean|string} [options.convertToRGB]`
    * `{boolean} [options.interpolate]` — `false` 为 nearest（配色默认偏此）；`true` 双线性更柔
    * `{boolean} [options.fastResample]` — `true` 等价 nearest；`false` 保持双线性
    * `{number} [options.transition=0]` — 瓦片淡入毫秒；`0` 首屏更干脆
    * `{number} [options.preload=1]` — 预加载低分辨率 overview
    * `{Object} [options.sourceOptions]` — 传给 geotiff.js `fromUrl`（如 `cacheSize`）
    * `{number} [options.opacity]` / `{number} [options.zIndex]` / `{boolean} [options.visible]` 等 OL 图层选项
  * `{Map} [main]` — 业务地图（工厂对称参数；本层不 fit view）

#### 方法

##### setColorStyle(styleOptions)

* **参数**：`{Object}` — 同构造里的 `colorRamp` / `colors` / `min` / `max` / `style` 等
* **说明**：运行时更新色带。`map.layers[code].setColorStyle({ max: 8, colorRamp: […] })`

##### setUrl(url, sourceInfo)

* **参数**：`{string} url`；`{Object} [sourceInfo]` — 可选覆盖 `nodata` / `min` / `max` / `bands` 等
* **说明**：换影像地址并重建 GeoTIFF source（保留当前色带配置）

#### 用法

```js
// 1) addLayer：单波段水深（必须配色）
map.addLayer({
  code: 'flood',
  type: 'GeoTIFFLayer',
  url: 'https://example.com/flood.tif',
  queryable: false,
  wkid: 4549,
  min: 0,
  max: 5,
  nodata: 0,
  transparentBelow: 0,
  interpolate: true,
  colorRamp: [
    [0, [0, 0, 0, 0]],
    [0.5, '#6baed6'],
    [2, '#2171b5'],
    [5, '#a50f15']
  ]
});

// 2) initLayers / 业务表：type=16，着色写在 parameter
map.initLayers({
  dem: {
    code: 'dem',
    type: '16',
    serviceAddress: '/data/dem.tif',
    parameter: {
      queryable: false,
      min: 0,
      max: 100,
      colorize: true
    }
  }
});

// 3) 仅 colors（均匀断点，仍会线性插值）
map.addLayer({
  code: 'depth',
  type: 'GeoTIFFLayer',
  url: '/data/depth.tif',
  queryable: false,
  min: 0,
  max: 5,
  colors: ['#00000000', '#c6dbef', '#2171b5', '#a50f15']
});
```

示例页：[`/geotiff`](/geotiff)

---

### VectorTileLayer

* **归属**：融合 — `extends ol/layer/VectorTile`
* **业务 type**：`'6'` / `'VectorTileLayer'`
* **说明**：默认 TMS MVT 地址：`/geoserver/gwc/service/tms/1.0.0/{title}:{layer}@EPSG%3A900913@pbf/{z}/{x}/{-y}.pbf`。

#### new VectorTileLayer(options, main)

* **参数**：
  * `{Object} options`
    * `{string} options.title` — GeoServer workspace（拼进瓦片 URL）
    * `{string} [options.layer]` / `{string} [options.code]` — 图层名
    * `{number} [options.minZoom]` — 会 `Math.ceil(minZoom) - 0.5`
    * `{string} [options.scopeLayerId]` / `{string} [options.scopeObjCode]` — 构造时即可按范围裁剪
    * 样式相关扁平字段（lineColor、lineWidth、filter 等，经 `_getStyles`）
  * `{Map} main`
* **副作用**：`Object.assign(this, options)`，自定义字段挂在 OL Layer 实例上。

#### 方法

##### changeScope(objCode)

* **参数**：`{string|null} objCode` — 范围面要素 code；空则取消裁剪
* **说明**：WFS 查 `title:scopeLayerId`，设置 `extent` 与自定义 `tileLoadFunction`，再 `source.refresh()`。

##### addFilters(filters)

* **参数**：
  * `{Array} filters` — OL 扁平 style 的 filter 表达式数组，如 `[['==', ['get', 'objCode'], 'P1']]`
* **说明**：与默认样式 AND 合并后 `setStyle`。

##### changeStyle(data)

* **参数**：`{Object} data` — 增量样式配置（经 `_getStyles`）
* **说明**：与 `defaultStyle` 组合后应用。

##### resume()

* **参数**：无
* **说明**：恢复 `defaultStyle`。

##### refreshData(data)

* **参数**：
  * `{Array<Object>} data` — 含 `objCode` 与 `valueField` 对应值，按 `preStyles` 着色
* **返回值**：无

##### refreshDataByGroup(data, statusMap)

* **参数**：
  * `{Object<string, Array>} data` — 分组 → 要素列表（含 `objCode`）
  * `{Object<string, {color: string}>} statusMap` — 分组色
* **返回值**：无

##### refreshGridData(data, polygon, relateLayers?)

* **参数**：
  * `{Array} data` — 含 `objX`/`objY` 与 `valueField`（负值跳过）
  * `{*} polygon` — 插值范围
  * `{Array} [relateLayers]` — 关联层
* **说明**：kriging 插值后驱动样式/关联层（实现见源码）。

```js
map.displayLayer('pipe', true);
map.layers.pipe.addFilters([['==', ['get', 'caliber'], 'DN200']]);
map.layers.pipe.resume();
```

---

### GraphicLayer

* **归属**：融合 — `extends ol/layer/Layer`，整层 Canvas 绘制（与 `BlinkNetworkLayer` 同路，适合大批量）
* **业务 type**：`'17'` / `'GraphicLayer'`
* **说明**：echarts 风格的 **series** 分类画线/点（实线/虚线，圆/三角/方）。支持增量增删改；**本层** `highlight` 闪烁，不进全局 `layers.highlight` / `highlightList`。不走 OL Vector Feature。默认 `queryable: false`（不参与 WFS 点查）。地图点击通过本层 `hitTest` 拾取，结果并入 `clickCallback`（默认开启，可用 `hitDetect: false` 关闭）。

#### 默认样式 vs series vs 高亮

| 层级 | 作用 |
|------|------|
| 图层默认 | 未写 `series.style` 时：线实线蓝色 `#1890ff`、宽 2、无描边；点白填充、蓝边、无外描边，半径 6 |
| `series.style` | 覆盖该组。`type` 为 `solid` 或 `dash`；线 `color/width/strokeColor/strokeWidth`；点 `fillColor/color/width`（图形边）+ `strokeColor/strokeWidth`（外描边）+ `radius` |
| `highlightStyle` | 另一套，默认对齐现有闪烁层：白描边 + 绿色芯，`blinkSpeed: 1.6`。只画 `highlight(ids)` 命中的条目，同一 rAF 改 alpha，不建 Feature。按图层一套，不按条配色 |

「已存在 / 即将添加」用两组 series 区分（实线 vs `type:'dash'`），不要在引擎里写死业务状态字段。

#### 坐标与 id

| 几何 | 字段 |
|------|------|
| 线 | `objXStart/objYStart/objXEnd/objYEnd` 或 `x1/y1/x2/y2` |
| 点 | `objX/objY` 或 `objx/objy` |
| id | `objCode` → `id`；都没有时用坐标串（删除/更新请带业务 id） |

有自定义投影时与现有高亮层一样转到 3857。`series` 可用 `id` / `name` 区分同形状不同样式的组。

#### new GraphicLayer(options, main)

* **参数**：
  * `{Object} [options]`
    * `{string} [options.code]` — 图层编码
    * `{Array} [options.series]` — 见下方结构
    * `{Object} [options.style]` — `{ line, point }`，覆盖图层默认
    * `{Object} [options.highlightStyle]` — 见高亮字段
    * `{number} [options.zIndex]` — 默认 `400`
    * `{boolean} [options.queryable]` — 默认 `false`
    * `{boolean} [options.hitDetect]` — 默认 `true`；地图点击是否拾取本层
    * `{number} [options.hitTolerance]` — 线拾取像素容差，默认 `8`；点为 `radius + 4`
  * `{Map} main`

```js
var layer = map.addGraphicLayer({
  code: 'deviceSketch',
  series: [
    { type: 'line', style: { type: 'solid', color: '#1890ff', width: 2 },
      data: [{ objCode: 'L1', objXStart: 13528430, objYStart: 3676466, objXEnd: 13528500, objYEnd: 3676500 }] },
    { type: 'line', style: { type: 'dash' },
      data: [{ objCode: 'L2', x1: 13528430, y1: 3676466, x2: 13528500, y2: 3676500 }] },
    { type: 'circle', style: { type: 'solid', fillColor: '#fff', color: '#1890ff' },
      data: [{ objCode: 'P1', objX: 13528430, objY: 3676466 }] },
    { type: 'triangle', style: { type: 'dash' },
      data: [{ objCode: 'P2', objx: 13528440, objy: 3676470 }] },
    { type: 'square', style: { type: 'solid' },
      data: [{ objCode: 'P3', objX: 13528450, objY: 3676480 }] }
  ]
});

layer.addData(series);
layer.updateData(series);   // upsert，同 id 更新字段（可跨组）
layer.removeData(['L1', 'P2']);
layer.getData();            // 当前全部 series + 原始字段
layer.highlight(['L1', 'P1']);
layer.clearHighlight();
```

也可构造后再 `setSeries(series)`。`initLayers` / `addLayer` 用 `type: '17'` 或 `'GraphicLayer'`。

#### series 结构

```js
{
  id: 'existing',          // 可选；也可用 name。省略则按 type+样式指纹分组
  type: 'line',            // 'line' | 'circle' | 'triangle' | 'square'（'point' 视为 circle）
  style: { type: 'solid' },
  data: [ /* 行对象 */ ]
}
```

##### style 字段

| 字段 | 线 | 点 | 默认 |
|------|----|----|------|
| `type` | `solid` / `dash` | 同左（虚线描边） | `solid` |
| `color` | 芯线颜色 | 图形边颜色 | `#1890ff` |
| `width` | 芯线宽 | 图形边宽 | 线 `2` / 点 `1.5` |
| `strokeColor` | 外描边 | 外描边填充色 | 无 |
| `strokeWidth` | 外描边宽 | 外描边半径增量 | `0` |
| `fillColor` | — | 填充 | `#ffffff` |
| `radius` | — | 半径 | `6` |

##### highlightStyle 字段

| 字段 | 说明 | 默认 |
|------|------|------|
| `lineColor` | 线芯颜色 | `#00ff00` |
| `lineWidth` | 线芯宽 | `3` |
| `haloColor` | 线白描边 | `#ffffff` |
| `haloWidth` | 线白描边宽 | `14` |
| `fillColor` | 点填充（闪烁） | `#00ff00` |
| `color` | 点白描边 | `#ffffff` |
| `width` | 点描边宽 | `4` |
| `radius` | 点半径 | `6` |
| `blinkSpeed` | 闪烁半周期速度 | `1.6` |

#### 方法

##### setSeries(series)

* **参数**：`{Array|Object} series` — 整表替换（先清空再写入）
* **返回值**：本图层

##### addData(series) / updateData(series)

* **参数**：`{Array|Object} series` — 按 item id **upsert**（已有 id 改坐标/字段，可跨组；没有则并入本次 series）
* **返回值**：本图层
* **说明**：`updateData` 与 `addData` 相同。

##### removeData(ids)

* **参数**：`{string|string[]|Object|Object[]} ids` — 业务 id，或带 `objCode`/`id` 的行
* **返回值**：本图层
* **说明**：从所有组删除后 rebuild。

##### getData()

* **参数**：无
* **返回值**：`Array<{ id, type, style, data }>` — `data` 为原始行字段拷贝

##### highlight(ids, highlightStyle?)

* **参数**：
  * `{string|string[]|Object|Object[]} ids`
  * `{Object} [highlightStyle]` — 合并进本层高亮样式
* **返回值**：本图层
* **说明**：只闪烁命中条目；不写入 `layers.highlight`。

##### clearHighlight()

* **参数**：无
* **返回值**：本图层
* **说明**：停止本层闪烁。

##### hitTest(coordinate, pixel, options?)

* **参数**：
  * `{number[]} coordinate` — 地图坐标（保留位，与点击事件一致）
  * `{number[]} pixel` — CSS 像素 `[x, y]`
  * `{Object} [options]` — `hitTolerance` / `pointTolerance` / `lineTolerance`
* **返回值**：`Object[]` — 命中条目的原始字段拷贝，按距离近→远；保证有 `objCode`，并带 `layerCode`、`_graphicKind`（`point`/`line`）
* **说明**：地图 `_clickFun` 会自动调用可见且 `hitDetect !== false` 的 GraphicLayer，结果并入 `clickCallback`。业务也可直接调用。

```js
map.callback.clickCallback = function (results, current) {
  // results 可能含 GraphicLayer 点/线：{ objCode, layerCode, ...原始字段 }
};

// 关闭某层拾取
map.addGraphicLayer({ code: 'sketch', hitDetect: false, series: [...] });
```

##### clear()

* **参数**：无
* **返回值**：本图层
* **说明**：清空全部 series 并 `clearHighlight()`。

---

### EchartsLayer

* **归属**：融合 — `extends ol-echarts`（EChartsLayerBase）
* **业务 type**：`'14'` / `'EchartsLayer'`
* **说明**：在 OL 地图上叠加 ECharts 系列（常用飞线 `lines`）。由 `_getLayer` 创建后调用 `appendTo(this.map)`。

#### new EchartsLayer(options)

* **参数**：
  * `{Object} options` — `Object.assign` 到实例
    * `{Array<string>} [options.fields]` — 从 GeoJSON properties 拷贝到 series data 的字段
    * `{number} [options.lineSlice]` — 按公里切片长线（turf `lineSliceAlong`）
    * `{Array} [options.series]` — 自定义 ECharts series；省略则用默认流动 lines

#### 方法

##### setData(features, addType?)

* **参数**：
  * `{Array} features` — 目前主要处理 `addType === 'geoserver'` 且几何为 `MultiLineString`
  * `{string} [addType]` — `'geoserver'`
* **说明**：组装 `coords` 后 `setChartOptions({ series })`。
* **用法**：

```js
// 一般由 initLayers / 关联图层 setData 驱动
map.layers.flow.setData(geoJsonFeatures, 'geoserver');
```

##### getLayerStatesArray()

* **参数**：无
* **返回值**：无（空实现）
* **说明**：占位，避免 `addLayer` 流程报错。

##### appendTo(olMap)

* **类型**：来自 ol-echarts 基类
* **参数**：`{ol/Map} olMap` — 即 `map.map`
* **说明**：挂到 OL 地图；`_getLayer` 中已自动调用。

---

### 图层 type 与类对照（速查）

| businessLayers.type | 类 | 基类 |
|---------------------|----|------|
| `1` / `CloudLayer` | CloudLayer | `ol/layer/Vector` |
| `12` / `ClusterLayer` | ClusterLayer | CloudLayer |
| `11` / `GLayer` | GLayer | `ol/layer/Vector` |
| `13` / `ContourLayer` | ContourLayer | `ol/layer/Vector` |
| `15` / `HeatmapLayer` | HeatmapLayer | `ol/layer/Heatmap` |
| `16` / `GeoTIFFLayer` / `TifLayer` | GeoTIFFLayer | `ol/layer/WebGLTile` |
| `17` / `GraphicLayer` | GraphicLayer | `ol/layer/Layer`（Canvas） |
| `6` / `VectorTileLayer` | VectorTileLayer | `ol/layer/VectorTile` |
| `14` / `EchartsLayer` | EchartsLayer | ol-echarts |
| （工厂） | HistoryDispatch | CloudLayer |

> 以上类**未**从包入口导出，请通过 `initLayers` / `addLayer` / `addContour` / `historyDispatch` 等创建，再用 `map.layers[code]` 调用。

---
