# 瓦片 / 样式

> 分类代号：C.2  本章共 **10 个接口**（3 个纯 URL 拼装 + 7 个 REST）

vjmap 的瓦片出图有两套机制：

1. **按地图内置默认 layer 出图**  
   `open_map` 之后，可直接用 `raster_tile_url()` / `vector_tile_url()` 拿到 `{z}/{x}/{y}` 占位符 URL，喂给 vjmap。
2. **按自定义 style 出图**  
   先 `create_map_style()` 拿到 `stylename`，把它作为 `layer` 段传给 tile URL，即可得到"按该 style 渲染"的瓦片。  
   后续可用 `cmd_update_style()` 原地修改；用 `cmd_switch_layers()` 临时切换可见图层；用 `slice_layer()` 触发预切片缓存；用 `get_slice_cache_zoom()` 查询当前已缓存到几级；用 `delete_style()` 清理。

WMS 出图走的是独立管线，用 `wms_tile_url()` 构造即可，单图走 `cmd/wms/{mapid}/{version}`，多图叠加走 `/service/wms`。

---

## 快速索引

| 序号 | SDK 方法 | HTTP | URL |
|----:|---------|------|-----|
| 16 | `raster_tile_url` / `rasterTileUrl` | GET | `/api/v1/map/tile/{mapid}/{version}/{layer}/{z}/{x}/{y}` |
| 17 | `vector_tile_url` / `vectorTileUrl` | GET | `/api/v1/map/tile/{mapid}/{version}/{layer}/{z}/{x}/{y}.mvt` |
| 18 | `wms_tile_url` / `wmsTileUrl`（单图） | GET | `/api/v1/map/cmd/wms/{mapid}/{version}` |
| 19 | `wms_tile_url` / `wmsTileUrl`（多图） | GET | `/api/v1/service/wms` |
| 20 | `create_map_style` / `createMapStyle` | POST | `/api/v1/map/cmd/createMapStyle/{mapid}/{version}` |
| 21 | `cmd_switch_layers` / `cmdSwitchLayers` | POST | `/api/v1/map/cmd/switchlayer/{mapid}/{version}` |
| 22 | `cmd_update_style` / `cmdUpdateStyle` | POST | `/api/v1/map/cmd/updateStyle/{mapid}/{version}` |
| 23 | `slice_layer` / `sliceLayer` | POST | `/api/v1/map/cmd/slicelayer/{mapid}/{version}` |
| 24 | `get_slice_cache_zoom` / `getSliceCacheZoom` | POST | `/api/v1/map/cmd/getSliceCacheZoom/{mapid}/{version}` |
| 25 | `delete_style` / `deleteStyle` | POST | `/api/v1/map/cmd/deletestyle/{mapid}/{version}` |

> **注意**：20/21/22/23/24/25 这 6 个是  `cmd` 路由，**POST 必须带非空 JSON body**（即使是 `{"noop":true}`），否则会返回 `500 param error`。SDK 已经替你处理好这点，直接调方法即可。

---

## 16 · 栅格瓦片 URL

**SDK**：`svc.raster_tile_url(mapid?, version?, layer?, fileid?)`

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `mapid` | string | 可省，默认为当前 `_cur_map.mapid` |
| `version` | string | 可省，默认 `v1` |
| `layer` | string | 可省：取 SDK 当前图的 `layer`（`open_map` 后为响应里的 `layer` 字段，如 `s6d04d896e`），没有则 `_`（图档内置默认 style）。若先调用 `create_map_style`，把返回的 `stylename` 传进来即可 |
| `fileid` | string | 可省。主要用于 tile 缓存破坏。后端不校验，只要 `{mapid,version,layer}` 对，tile 就出得来 |

### 实采示例

```json
"http://127.0.0.1:27660/api/v1/map/tile/sys_zp/v1/_/{z}/{x}/{y}?tag=c47be46a2ad2&token=<jwt>"
```

### Python

```python
from vjmap import Vjmap, MapOpenWay

svc = Vjmap("http://127.0.0.1:27660/api/v1", TOKEN)
svc.open_map(mapid="sys_zp", version="v1", mapopenway=MapOpenWay.GeomRender)
print(svc.raster_tile_url())
```

### Node.js

```javascript
import { Vjmap, MapOpenWay } from './sdk/nodejs/vjmap.js';

const svc = new Vjmap('http://127.0.0.1:27660/api/v1', TOKEN);
await svc.openMap({ mapid: 'sys_zp', version: 'v1', mapopenway: MapOpenWay.GeomRender });
console.log(svc.rasterTileUrl());
```

---

## 17 · 矢量瓦片 URL（MVT）

**SDK**：`svc.vector_tile_url(...)`，参数与 `raster_tile_url` 完全一致，区别是 URL 末尾多了 `.mvt` 后缀。

### 实采示例

```json
"http://127.0.0.1:27660/api/v1/map/tile/sys_zp/v1/main/{z}/{x}/{y}.mvt?tag=c47be46a2ad2&token=<jwt>"
```

`{layer}` 默认是 `main`（内置 MVT 默认名），不是 `_`。

### 用法

MVT 非常适合作为 vjmap `vector` source：

```javascript
map.addSource('vj', {
  type: 'vector',
  tiles: [svc.vectorTileUrl()],
  minzoom: 0,
  maxzoom: 22,
});
map.addLayer({ id: 'vj-line', source: 'vj', 'source-layer': 'default', type: 'line' });
```

---

## 18 / 19 · WMS 瓦片 URL

**SDK**：`svc.wms_tile_url(mapid, version?, *, layer?, width?, height?, bbox?, srs?, background_color?, transparent?, crs?, four_parameter?, mvt?)`

### 参数规则

- `mapid`: 可以是字符串，也可以是 `list[str]`
  - **单图** → 走 `/api/v1/map/cmd/wms/{mapid}/{version}`
  - **多图** → 走 `/api/v1/service/wms?mapid=a_,b_,c&version=v1_,v1_,v1&layers=_,_,&crs=_,_,&fourParameter=_,_,`
- `bbox`: 默认 `{bbox-epsg-3857}` 占位符。如果你直接用 如Leaflet，就保留占位符交给前端去替换。SDK 刻意**不对 `bbox` 做 percent-encode**。后端读 `bbox`，`BBOX`（大写）亦可。
- `srs`/`crs`: 坐标系，默认 `EPSG:3857`。
- `layer`: 可选，指定某个 stylename 渲染。**后端读的参数名是 `layers`（复数）**，SDK 已按此发送。
- `backgroundColor`: 背景色，`"r,g,b"` 或 `"r,g,b,a"` 字符串（**仅 `transparent=false` 时生效**；默认 transparent=true 输出透明 PNG）。⚠️ **仅单图 `cmd/wms` 路径有效**——Go 多图 `/service/wms` 的转发白名单里没有它（`transparent` 会转发），多图请求中会被静默丢弃。
- `mvt=True`: 单图时 version 路径后加 `.mvt` 输出矢量瓦片；**多图 `/service/wms` 不支持**（Go 路由无此变体，SDK 会抛错）。
- ⚠️ 多图 `/service/wms` 由 Go 端逐图转发到**默认工作区**图形进程（`global.ProxyPort` 的 `/cmd/wms/...`）——命名工作区里的图用它取不到；且只读**小写** `bbox`/`width`/`height`（大写 `BBOX`/`WIDTH`/`HEIGHT` 仅单图 `cmd/wms` 认）。
- ⚠️ 多图分隔符是 `_,`（下划线+逗号），不是 `;`；且 `mapid/version/layers/crs/fourParameter` 五个数组**必须等长**，否则后端返回 `paramter error`。SDK 的处理是：标量自动广播到 mapid 长度，**显式数组长度不齐则本地抛 `ValueError("数组参数长度须与 mapid 一致")`**（根本到不了后端；REST 直连不齐才见到后端 `{"code":7,"msg":"paramter error"}`）。

### 实采示例

单图：

```json
"http://127.0.0.1:27660/api/v1/map/cmd/wms/sys_zp/v1?bbox={bbox-epsg-3857}&srs=EPSG%3A3857&width=512&height=512&token=<jwt>"
```

多图（3 张图，空 layers/crs/fourParameter 用 `_,` 占位补齐等长）：

```json
"http://127.0.0.1:27660/api/v1/service/wms?bbox={bbox-epsg-3857}&mapid=sys_zp_,other_,third&version=v1_,v1_,v1&layers=_,_,&crs=_,_,&fourParameter=_,_,&srs=EPSG%3A3857&width=256&height=256&token=<jwt>"
```

### Python

```python
url = svc.wms_tile_url("sys_zp", "v1", width=512, height=512)
url_multi = svc.wms_tile_url(["sys_zp", "other"], ["v1", "v1"])
```

### Node.js

```javascript
const url = svc.wmsTileUrl({ mapid: 'sys_zp', version: 'v1', width: 512, height: 512 });
const urlMulti = svc.wmsTileUrl({ mapid: ['sys_zp', 'other'], version: ['v1', 'v1'] });
```

---

## 20 · 创建地图样式 `createMapStyle`

**URL**：`POST /api/v1/map/cmd/createMapStyle/{mapid}/{version}`  
**SDK**：`svc.create_map_style(mapid, version, *, layeron, layeroff, backcolor, clipbounds, expression, lineweight, is_geom_layer=True, name)`  

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `geom` | bool | `true` 几何渲染（默认）；`false` 栅格渲染。`isGeomLayer:false` 返回的 `stylename` 前缀为 `m`（光栅样式，对应 `deleteStyle` 通配 `m*`）；几何样式前缀 `s` |
| `stylename` | string | style 名字（后端 `MapStyleData::parseVariantMap` 读 `stylename`，**不读 `name`**）；不给则视为系统样式（`issys:true`）。SDK 参数仍叫 `name`，发送时映射为 `stylename` |
| `layeron` | string / 数组 | 可见图层；数组传入时 SDK 会自动转成 `(0,1,2)` 格式 |
| `layeroff` | string / 数组 | 隐藏图层 |
| `backcolor` | int | 背景色（0=黑色，常用于暗色底图） |
| `clipbounds` | string | 裁剪范围 JSON 字符串 |
| `expression` | string | Cesium 风格的颜色表达式 |
| `lineweight` | int[] | 线宽缩放 `[默认,最小,最大]` |

### 响应（实采）

```json
{
  "stylename": "sfbd35705c"
}
```

返回的 `stylename` 就是后续作为瓦片 URL 中 `{layer}` 段的值。

### Python

```python
style = svc.create_map_style("sys_zp", "v1", layeron="*", backcolor=0)
tile_url = svc.raster_tile_url(layer=style["stylename"])
```

### Node.js

```javascript
const style = await svc.createMapStyle({ mapid: 'sys_zp', version: 'v1', layeron: '*', backcolor: 0 });
const tileUrl = svc.rasterTileUrl({ layer: style.stylename });
```

### curl

```bash
curl -X POST "http://127.0.0.1:27660/api/v1/map/cmd/createMapStyle/sys_zp/v1?token=$TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"geom":true,"layeron":"*","backcolor":0}'
```

---

## 21 · 切换可见图层 `switchlayer`

**URL**：`POST /api/v1/map/cmd/switchlayer/{mapid}/{version}`  
**SDK**：`svc.cmd_switch_layers(visible_layers, *, expression?, dark_mode?, mapid?, version?)`

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `visibleLayer` | string[] | 要可见的图层 id 列表 |
| `expression` | string | 颜色表达式（与 createMapStyle 同）。⚠️ 本参数会把请求转给样式工作进程，当前版本表达式求值会**打崩该进程**——返回裸 TCP 错误串（如 `read tcp [::1]:port: wsarecv: An existing connection was forcibly closed`），且崩后紧跟的样式命令（switchlayer/createMapStyle/deleteStyle 等）短暂全部 `connectex: No connection`；worker 惰性重启，12s 内未自愈。生产环境勿传 |
| `darkMode` | bool | 是否暗色（可在 openMap 时也指定） |

### 响应（实采）

```json
{
  "layerid": "s24d720029"
}
```

与 `createMapStyle` 的 `stylename` 相似，`layerid` 指向新的内部 style（后端按参数 hash 复用，不会无限增长）。同参数重复调用返回相同 `layerid`；`darkMode` 变化产生新 `layerid`。`layerid`/`stylename` 可直接作为 `rasterTileUrl({layer})` 的 `layer` 段实拉瓦片。

### Python

```python
res = svc.cmd_switch_layers(["0"])
print(res["layerid"])
```

### Node.js

```javascript
const res = await svc.cmdSwitchLayers(["0"]);
console.log(res.layerid);
```

---

## 22 · 原地修改样式 `updateStyle`

**URL**：`POST /api/v1/map/cmd/updateStyle/{mapid}/{version}`  
**SDK**：`svc.cmd_update_style(*, name, layeron, layeroff, backcolor, clipbounds, expression, lineweight, mapid?, version?)`

与 `createMapStyle` 最大的区别：**它可以修改已有 style（传同名 name）**，而不是每次新建。

### 响应（实采）

```json
{
  "bounds": "[587227463.27, 3103509765.63, 588006538.39, 3104288840.75]",
  "boundsChange": false,
  "layerid": "m765111cb4"
}
```

`boundsChange` = false 表示裁剪范围没变；若 `clipbounds` 修改了，`bounds` 与 `boundsChange` 会反映出来。

### Python

```python
svc.cmd_update_style(
    mapid="sys_zp", version="v1",
    name="nb_style_1",
    layeron="*",
    backcolor=0,
)
```

### Node.js

```javascript
await svc.cmdUpdateStyle({
  mapid: 'sys_zp', version: 'v1',
  name: 'nb_style_1',
  layeron: '*',
  backcolor: 0,
});
```

---

## 23 · 触发预切片缓存 `slicelayer`

**URL**：`POST /api/v1/map/cmd/slicelayer/{mapid}/{version}`  
**SDK**：`svc.slice_layer(*, layer, zoom, ismvt, iscancel, is_all_cancel, batch_num?, idle_batch_sleep_ms?, busy_batch_sleep_ms?, mapid?, version?)`

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `layer` | string | 要预切的 layer id（即 stylename） |
| `zoom` | int / int[] / string | 要切的 zoom 层级。单个或数组 |
| `ismvt` | bool | true 切 MVT，false 切栅格 |
| `iscancel` | bool | 取消本次任务 |
| `isAllCancel` | bool | 取消该地图所有正在进行的切片任务 |
| `batchnum` | int | 批处理大小 |
| `idle_batch_sleepms` | int | 空闲批间隔毫秒 |
| `busy_batch_sleepms` | int | 忙时批间隔毫秒 |

### 响应（实采）

取消切片：

```json
{}
```

即空对象。真实触发切片时后端会流式推进度（`{progress:0.5,total:...}`），这里我们的 SDK 只取第一响应。

### Python

```python
svc.slice_layer(layer="s24d720029", zoom=[0, 1, 2, 3], ismvt=False)
```

### Node.js

```javascript
await svc.sliceLayer({ layer: 's24d720029', zoom: [0,1,2,3], ismvt: false });
```

---

## 24 · 查询已缓存的切片级别 `getSliceCacheZoom`

**URL**：`POST /api/v1/map/cmd/getSliceCacheZoom/{mapid}/{version}`  
**SDK**：`svc.get_slice_cache_zoom(*, layer, ismvt, mapid?, version?)`

### 响应（实采）

```json
{
  "sliceCacheZoom": 0
}
```

`sliceCacheZoom` = 当前已稳定缓存到的最大 zoom。若 = 0 通常表示还没跑过预切片。

### Python / Node

```python
print(svc.get_slice_cache_zoom(layer="s24d720029", ismvt=False))
```

```javascript
console.log(await svc.getSliceCacheZoom({ layer: 's24d720029', ismvt: false }));
```

---

## 25 · 删除自定义样式 `deletestyle`

**URL**：`POST /api/v1/map/cmd/deletestyle/{mapid}/{version}`  
**SDK**：`svc.delete_style(stylename, *, mapid?, version?)`

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `styleid` | string | 要删除的 layername 或 style name；支持通配 `*`（全部）/ `a*`（全部系统样式）/ `m*`（全部非几何样式）/ `s*`（全部几何样式）。SDK 的 `delete_style(stylename)` 会代为放入 `styleid` 键 |
| `onlycleardata` | bool | 只清样式渲染数据、保留样式记录（默认 false） |

### 响应（实采）

成功返回被删样式名列表；`styleid` 不匹配任何 `layername`/`name` 时 `deletestyles` 为空串：

```json
{ "deletestyles": "sfbd35705c", "__asyncstyle__": true }
```

### Python / Node

```python
svc.delete_style("sfbd35705c", mapid="sys_zp", version="v1")
```

```javascript
await svc.deleteStyle('sfbd35705c', { mapid: 'sys_zp', version: 'v1' });
```

---

## 组合示例：从开图到看地图

```python
from vjmap import Vjmap, MapOpenWay

svc = Vjmap("http://127.0.0.1:27660/api/v1", TOKEN)
svc.open_map(mapid="sys_zp", version="v1", mapopenway=MapOpenWay.GeomRender)

# 1. 自定义一个黑底白线风格
style = svc.create_map_style(
    "sys_zp", "v1",
    layeron="*",
    backcolor=0,
    expression="gOutColorRed:=255;gOutColorGreen:=255;gOutColorBlue:=255;gOutColorAlpha:=255;",
)

# 2. 拿 tile URL 给 vjmap
raster = svc.raster_tile_url(layer=style["stylename"])

# 3. 预切前 4 级，加快首屏
svc.slice_layer(layer=style["stylename"], zoom=[0,1,2,3], ismvt=False)

# 4. 不用了，清掉
svc.delete_style(style["stylename"], mapid="sys_zp", version="v1")
```
