# 地图管理（Map Management）

> 分类 1 / 13 · 接口 15 个 · 

**快速目录**

| # | 接口 | 方法 | URL |
|---|------|------|-----|
| 1 | [秒传检测](#1-秒传检测-check_map_file) | GET | `/api/v1/map/mapfile` |
| 2 | [上传源文件](#2-上传源文件-upload_map) | POST | `/api/v1/map/uploads` |
| 3 | [打开/创建地图](#3-打开创建地图-open_map) | POST | `/api/v1/map/openmap/{mapid}` |
| 4 | [修改地图](#4-修改地图-update_map) | POST | `/api/v1/map/updatemap/{mapid}` |
| 5 | [关闭地图](#5-关闭地图-close_map) | GET | `/api/v1/map/cmd/closemap/{mapid}/{version}` |
| 6 | [删除地图](#6-删除地图-delete_map) | GET | `/api/v1/map/cmd/deletemap/{mapid}/{version}` |
| 7 | [重命名地图](#7-重命名地图-rename_map) | GET | `/api/v1/map/cmd/renamemap/{old}/_` |
| 8 | [删除源文件](#8-删除源文件-delete_map_file) | GET | `/api/v1/map/cmd/deletemapfile/_/v1` |
| 9 | [列出地图](#9-列出地图-list_maps) | GET | `/api/v1/map/cmd/listmaps/_/v1` |
| 10 | [读元数据](#10-读元数据-metadata) | GET | `/api/v1/map/cmd/metadata/{mapid}/{version}` |
| 11 | [修改元数据](#11-修改元数据-update_metadata) | POST | `/api/v1/map/cmd/updateMetadata/{mapid}/{version}` |
| 12 | [缩略图](#12-缩略图-thumbnail) | GET | `/api/v1/map/cmd/thumbnail/{mapid}/{version}` |
| 13 | [支持的格式](#13-支持的格式-support_format) | GET | `/api/v1/map/cmd/supportFormat/_/v1` |
| 14 | [系统常量](#14-系统常量-const_data) | GET | `/api/v1/map/cmd/constData/_/v1` |
| 15 | [数据范围](#15-数据范围-get_data_bounds) | GET | `/api/v1/map/cmd/getDataBounds/{mapid}/{version}` |

**共性约定（重要）**

- 所有 `cmd/*` 路径：`GET` **不需要 body**，`POST` **要求 body 至少一个字段**，否则后端回 500 `param error`。SDK 对无 body 的 cmd 统一走 GET。
- `openmap` 通过 **POST body** 传 `mapid/version/fileid/uploadname/geom`；`fileid ` 必须来自 `upload_map` 的返回，不要自己拼 md5。打开方式只看布尔 `geom`——后端**不读 `mapopenway`**（源码零消费点），SDK 把 `mapopenway` 换算成 `geom`。
- 所有路径都带 `?token=<JWT>`（SDK 自动加）。

---

## 1. 秒传检测 `check_map_file`

**URL**：`GET /api/v1/map/mapfile?md5={md5}`  
**用途**：客户端上传前先算文件 md5，若 `result:true` 则复用已有文件，跳过上传。

### 入参（Query）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `md5` | string(32) | 是 | 源文件 MD5（小写 hex） |
| `token` | string | 是 | JWT |

### 出参

```json
{"result": false}
```

- `result=true`：已有同 md5 文件；可直接跳过上传。注意 `fileid` 是 `c + md5[:11]`（CAD 文件）或 `t + md5[:11]`（图片等其他文件），**不要自己拼**，应从 `mapfile`/`uploads` 响应里取
- `result=false`：需要走 `upload_map`

### 示例

```bash
curl -s "http://127.0.0.1:27660/api/v1/map/mapfile?token=$T&md5=caf27de8a2259b2c4e04f2afde2f5c42"
```

```python
svc.check_map_file("caf27de8a2259b2c4e04f2afde2f5c42")
```

```javascript
await svc.checkMapFile('caf27de8a2259b2c4e04f2afde2f5c42');
```

---

## 2. 上传源文件 `upload_map`

**URL**：`POST /api/v1/map/uploads`  
**Content-Type**：`multipart/form-data`  

### 入参（Form）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `file` | binary | 是 | 源文件（后端只取第一个文件部分，字段名不限） |
| `md5` | string(32) | 否 | 后端**不读**（只取文件部分，按内容自算 md5）；两个 SDK 仍会带上，无害 |

> ⚠️ 表单里的其它字段（包括 `secretKey`）后端一律不读——**上传时无法设密**。加密只能在该图 v1 **首次 openmap** 时带 ≥10 位 `secretKey`（见 [authentication.md §2.1](../authentication.md#21-设密只在-v1-首次-openmap-时)）。

### 出参

```json
{"fileid":"caf27de8a225","mapid":"caf27de8a225","uploadname":"caf27de8a225.dwg"}
```

- 同内容文件已被某张图使用时，`mapid` 是那张图的 mapid，另带 `version` / `geom`；图片文件另带 `maptype:"image"`；失败返回 `{uploadname, error}`（如 `not support format xxx`）
- `fileid`：生成的 **12 位标识**（`c`+md5 前 11 位为 CAD 文件，`t`+md5 前 11 位为图片等其他文件；恰好都以 md5 首字符起头，容易误认为 md5[:12]）。✅ `.dwg` 上传 → `fileid==="c"+md5.slice(0,11)`、`.png` → `"t"+md5.slice(0,11)` 精确吻合
- `mapid`：默认等于 `fileid`；后续 `open_map` 可以给另一个业务名
- `uploadname`：⚠️ 实际是**回显上传时的原文件名**，**不是**落盘名——服务端实际存为 `fileid+扩展名`（如 `cc4d08f98148.dwg`）。证据：上传后 `delete_map_file(uploadname)` 返回 `notFound`，而 `delete_map_file(fileid+".dwg")` 成功删除。清理上传产物要用 fileid 拼名

### 示例

```bash
curl -s -X POST "http://127.0.0.1:27660/api/v1/map/uploads?token=$T" \
  -F "file=@./my.dwg" \
  -F "md5=$(md5sum my.dwg | awk '{print $1}')"
```

```python
up = svc.upload_map("./my.dwg")
fileid, uploadname = up["fileid"], up["uploadname"]

# 也接受 (文件名, bytes) 元组——服务端照样按内容自算 md5 命名
up = svc.upload_map(("probe.dwg", dwg_bytes))
```

```javascript
const up = await svc.uploadMap('./my.dwg');
const { fileid, uploadname } = up;
```

---

## 3. 打开/创建地图 `open_map`

**URL**：`POST /api/v1/map/openmap/{mapid}?version={version}` （SDK 默认走 POST）  

> ⚠️ **SDK 坑位**：Node SDK 的 `openMap({useMethod:'GET'})` **必崩**——它把 `jsonBody` 传给 fetch，undici 禁止 GET/HEAD 带 body，抛 `Request with GET/HEAD method cannot have body`（包装成 VjmapError）。HTTP 层面的 GET 等价性在 SDK 里不可达，只用默认 POST。

这是整套 API 的**中心接口**。它同时承担：

- 首次创建：`mapid` 未存在时结合 `fileid + uploadname` 新建
- 再次打开：`mapid` 已存在时忽略 `fileid/uploadname`
- 打开方式：body 布尔 `geom`（后端缺省 `false`）——`true` = GeomRender（几何渲染），`false`/**缺省** = Memory（内存打开）。后端不读 `mapopenway`；SDK 的 `mapopenway` 参数（默认 `GeomRender`）按官方 JS SDK 的做法换算为 `geom = mapopenway !== 'Memory'`

### 入参（JSON body）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `mapid` | string | 是 | 业务 ID |
| `version` | string | 是 | 版本号，默认 `v1` |
| `fileid` | string(12) | **首次建图必填** | `upload_map` 返回的 fileid |
| `uploadname` | string | **首次建图必填** | `upload_map` 返回的 uploadname |
| `geom` | bool | 否 | 打开方式，**后端缺省 false（Memory）**。SDK 参数是 `mapopenway: 'Memory' \| 'GeomRender'`（默认 GeomRender → `geom:true`） |
| `style` | object | 否 | 预设样式；详情见 § 2 `create_map_style` |
| `secretKey` | string | 否 | 图级密钥：仅在该图 v1 **首次**打开且 ≥10 位时设密（原样存）；之后用于校验（见 [authentication.md](../authentication.md)） |
| `accessKey` | string | 否 | 该图的 18 位 `ak` 开头 accessKey（仅浏览），或实例 super key |
| `mapbounds` | array / string | 否 | 限制的图面范围 |

> ⚠️ cmd 默认按该图版本在 mapinfos ini 里的 `geom` 标记路由：**从未以 `geom:true` 打开过**的图走内存进程，`queryFeatures` 的 `condition` 查询会回 `not support query type!`（见 [03-query](03-query.md)）。旧版 SDK 因发送 `mapopenway` 而非 `geom`，实际**一律按 Memory 打开**——用它新建的图都是这种状态，用 GeomRender 重开一次即可。

#### GeomRender 首次打开是异步的

新图（或几何库尚未生成的图）以 `geom:true` 打开时立即返回 `{"mapid","version","status":"working","error":""}`，后台子进程生成几何库。需轮询 `GET cmd/listmaps/{mapid}/{version}` 直到该版本 `status=="finish"`（`error` / `runerror` 为失败，原因在 `errmsg`），然后**再 openmap 一次**才拿到完整参数（bounds / layer / layers…）。官方 JS SDK `openMap(param, isWaitFinish=true)` 就是这么做的。

```python
r = svc.open_map(mapid="demo_dwg", fileid=up["fileid"], uploadname=up["uploadname"])
if r.get("status") == "working":
    w = svc.wait_map_open_finish("demo_dwg", r["version"])   # {"status":"finish"|"error", ...}
    r = svc.open_map(mapid="demo_dwg", version=w.get("version") or r["version"])
```

```javascript
let r = await svc.openMap({ mapid: 'demo_dwg', fileid: up.fileid, uploadname: up.uploadname });
if (r.status === 'working') {
  const w = await svc.waitMapOpenFinish('demo_dwg', r.version);
  r = await svc.openMap({ mapid: 'demo_dwg', version: w.version || r.version });
}
```

### 出参（节选，完整见 snapshot）

| 字段 | 含义 |
|------|------|
| `mapid / version / fileid / uploadname / filename` | 基本定位 |
| `bounds / dbBounds / drawBounds / initBounds / viewBounds` | 各种范围 |
| `layers` *(JSON string)* | 图层数组 |
| `styles` *(JSON string)* | 预置样式数组 |
| `format` | 瓦片格式（典型 `png`） |
| `geom` | 实际打开方式，字符串 `"true"`（GeomRender）/ `"false"`（Memory） |
| `layer` | 当前样式图层名（如 `s6d04d896e`）= 瓦片 URL 的 `{layer}` 段；SDK 存入 `currentMapParam().layer` |
| `status` | GeomRender 首次打开时为 `"working"`（此时只有 mapid/version/status/error 四键，见上） |
| `mapopenway` | 后端不读也不返回（下方片段来自早期版本；旧 SDK 发送它时本版响应里也没有） |
| `pid` | 子进程 PID（⚠️ **Memory 方式也返回**——两种 openway 同值，非 GeomRender 专属） |
| `renderAccuracy` | 渲染精度 |
| `isnewmap` | ⚠️ 实际是**字符串 `"true"` 且恒真**——对已存在图重复 openMap/updatemap 也回 `"true"`，**不能当"刚创建"判据** |

#### 真实响应片段

> 注：片段来自早期版本实采。本版响应**没有 `mapopenway`**（实际打开方式看 `geom` 字段），`isnewmap` 是字符串 `"true"` 且恒真、`pid` 在 Memory 下也返回。

```json
{
  "mapid": "nb_roundtrip_1776829368",
  "version": "v1",
  "fileid": "caf27de8a225",
  "uploadname": "caf27de8a225.dwg",
  "filename": "caf27de8a225.dwg",
  "mapopenway": "Memory",
  "bounds": "[67108.08576259,-10253.62403246,73355.58576721,-4006.12402784]",
  "isnewmap": "true",
  "format": "png",
  "layers": "[{\"color\":\"#ffffff\",\"index\":0,...}]",
  "styles": "[{\"backcolor\":0,\"isgeom\":false,...}]"
}
```

### 示例

```python
svc.open_map(
    mapid="demo_dwg", version="v1",
    mapopenway="Memory",
    fileid=up["fileid"], uploadname=up["uploadname"],
)
```

```javascript
await svc.openMap({
  mapid: 'demo_dwg', version: 'v1',
  mapopenway: 'Memory',
  fileid: up.fileid, uploadname: up.uploadname,
});
```

---

## 4. 修改地图 `update_map`

**URL**：`POST /api/v1/map/updatemap/{mapid}?version={version}`  

用法：给**已存在**的 mapid 换图档出新版本。服务端语义：

- body 的 `fileid` 与当前最新版本的图档**不同** → 新建版本 **vN+1**（内部打 `updatemap:true`），此时 `deleteOldVersion:true` 会在新版本打开后删除旧版本（只在这条路径生效）
- `fileid` 为空或与当前相同 → 不出新版本，按当前版本原样重开
- 打开方式与 openmap 相同，由 `geom` 决定（SDK 未给 `geom` 时按 `mapopenway` 换算，缺省 GeomRender）；新版本同样走异步生成，需 `waitMapOpenFinish`

### 入参（JSON body）

| 字段 | 必填 | 说明 |
|------|------|------|
| `mapid` | 否（实采） | 要更新的图；路径里的 `{mapid}` 已生效，body 里可省 |
| `fileid` | 出新版本时必填 | 新上传图档的 fileid；为空/与当前相同 = 原样重开 |
| `uploadname` | 否 | 上传原文件名 |
| `geom` | 否 | 打开方式；SDK 参数 `mapopenway` 自动换算 |
| `deleteOldVersion` | 否 | `true` 时出新版本后删除旧版本 |
| …… | | 其余字段参考 vjmap 发布包 d.ts 里的 `IUpdateMapParam` |

> 📌 实采（2026-09-20）：`POST updatemap/{mapid}?version=v1` 带**空 body** 也成功，返回该图当前参数快照（等价"原样重开"）。

### 出参

结构与 `open_map` 返回一致。

### 示例

```python
up2 = svc.upload_map("./demo_rev2.dwg")          # 新图档
r = svc.update_map("demo_dwg", "v1", body={
    "fileid": up2["fileid"],
    "uploadname": up2["uploadname"],
    "deleteOldVersion": True,                      # 可选：出 v2 后删 v1
})
# r["version"] == "v2"；GeomRender 下 r["status"] 可能是 "working"，同 open_map 等待
```

---

## 5. 关闭地图 `close_map`

**URL**：`GET /api/v1/map/cmd/closemap/{mapid}/{version}`  

仅断开 图形服务 中的内存/渲染引用；不删除数据库文件，也不删图。

### 出参

```json
{"status": true}
```

### 注意：POST 无 body 会 500

```bash
# ✗ 500 param error
curl -X POST ".../closemap/sys_zp/v1?token=$T"
# ✓ 200
curl ".../closemap/sys_zp/v1?token=$T"
# ✓ 200（POST 可以，但 body 必须有至少一个字段）
curl -X POST ".../closemap/sys_zp/v1?token=$T" -d '{"fileid":"caf27de8a225"}' -H 'content-type: application/json'
```

SDK 内部统一使用 GET 规避该陷阱。

---

## 6. 删除地图 `delete_map`

**URL**：`GET /api/v1/map/cmd/deletemap/{mapid}/{version}?deleteDwgFile={bool}`  

| 参数 | 默认 | 说明 |
|------|------|------|
| `deleteDwgFile` | `false` | 后端实际读取的参数：`true` 时强制删除源文件 |
| ~~`keepSource`~~ | - | openapi/旧 SDK 记载的参数，**后端没有消费点**，传了无效 |
| `retainVersionMaxCount` | `0` | 配合 `version=*` 删除全部版本时可保留最新 N 个版本 |

> ⚠️ 后端行为：删除源文件的判断是 `deleteDwgFile==true || !noAutoDeleteDwgFile`（`noAutoDeleteDwgFile` 是服务端 config.json 配置项）。`keepSource` 本身在后端没有消费点——要可靠保留源文件，部署方需把 `noAutoDeleteDwgFile` 设为 `true`，否则默认行为是连源文件一起删。

### 出参

```json
{"status": true}
```

> ⚠️ mapid 不存在时仍返回 `{"status": true}`——**该接口对"删除不存在的图"静默成功**，不能靠返回值判断是否真删了图。

### 示例

```python
svc.delete_map("demo_dwg", "v1", delete_dwg_file=False)
```

---

## 7. 重命名地图 `rename_map`

**URL**：`GET /api/v1/map/cmd/renamemap/{old}/_?newMapId={new}`  

> ⚠️ 查询键是**驼峰** `newMapId`（小写 `newmapid` 后端读不到，会报 `mapid is empty`）；且要求对该图有 admin 权限（`isAdminPermInMap`），否则报 `no admin permisson`。

### 响应样例（权限/参数不满足时）

```json
{
  "status": false,
  "error": true,
  "reason": "no admin permisson to delete this map, please use password or superKey"
}
```

> ⚠️ mapid 不存在时返回 `{"status": false, "error": true, "reason": "mapid ... is not exist"}`——**HTTP 仍是 200**，SDK 只在 HTTP 层非 2xx 时抛 `VjmapError`，业务错误要调用方自查 `status`/`error`/`reason` 字段。
>
> ⚠️ 该接口（及同族 cmd 接口）的响应体会**回显完整 `token` 字段**——把响应写日志/上报前务必剔除，否则会泄露凭据。

---

## 8. 删除源文件 `delete_map_file`

**URL**：`GET /api/v1/map/cmd/deletemapfile/_/v1?files=a.dwg;b.dwg`  

删除上传到 `data/mapfiles/` 目录下的物理文件。后端**不检查**是否有图在引用（找到即删），删前自己确认；不带扩展名时自动补 `.dwg` 再找。

### 出参

```json
{
  "deleted": [],
  "deletedCount": 0,
  "notFound": ["__nb_nonexistent__.dwg"],
  "status": false
}
```

### 示例

```python
svc.delete_map_file(["caf27de8a225.dwg"])
# 或字符串
svc.delete_map_file("a.dwg;b.dwg")
```

---

## 9. 列出地图 `list_maps`

**URL**：`GET /api/v1/map/cmd/listmaps/_/v1`  

### 入参

| 参数 | 说明 |
|-------|------|
| `curPage` | 页码（1 起，配合 `pageCount` 生效；不传/为 0 则返回全部） |
| `pageCount` | 每页条数 |
| `mapIds` | 限定只返回这些 mapid（字符串列表） |

> ⚠️ `mapIds` 由后端 `toStringList()` 读取——GET query 里的字符串不会拆分，必须用 **POST + JSON body** `{"mapIds":["a","b"]}`（官方 JS SDK 也是这样发的）。`curPage`/`pageCount` 走 query 或 body 均可。
>
> ⚠️ openapi/旧文档写的 `keyword`、`page`、`pageSize` 在本版后端**没有消费点**，传了也不会过滤。另外：`mapid` 路径段写具体图名时返回 `{mapid:[该图各版本]}`（不走分页；`version` 写 `_` = 最新版）；非 root 用户默认只隐藏 `ns_` 前缀的图（服务端 `map_list_exclude_prefix` 可配，`;` 分隔，`*` 全隐藏），root 只隐藏 `ns_temp_`。`temp_` 前缀的图**不隐藏**，只是会被登记进临时图自动清理队列。

### 出参

```json
{
  "sys_zp": [
    {
      "db": "c47be46a2ad2.db",
      "filename": "c47be46a2ad2.dwg",
      "geom": true,
      "mtime": "2026-04-10 11:31:00",
      "status": "finish",
      "uploadname": "sys_zp.dwg",
      "version": "v1"
    }
  ]
}
```

- 顶层 key 是 `mapid`，value 是该图所有版本的数组
- `geom:true` 表示该图已有切片缓存
- `status:"finish"` 表示打开流程完成
- ⚠️ **响应形态随参数分叉**：不传分页参数 → 返回 `{mapid: [版本…]}` 字典；带 `curPage`/`pageCount`（都 >0）→ 返回**分页信封** `{count, curPage, pageCount, mapIds:[本页 mapid…], mapInfos:{mapid:[版本…]}, allMapIds:[…]}`——`mapIds` 是**数组**，版本详情在 `mapInfos` 对象里；`count` 是过滤后总数；`allMapIds` 是实例**全部** mapid（未做前缀隐藏过滤）；带 `mapIds` 过滤（POST body）→ 仍回 `{mapid:[…]}` 形态但只含命中图

### 示例

```python
maps = svc.list_maps()
for mapid, versions in maps.items():
    print(mapid, [v["version"] for v in versions])
```

```javascript
const maps = await svc.listMaps();
```

---

## 10. 读元数据 `metadata`

**URL**：`GET /api/v1/map/cmd/metadata/{mapid}/{version}`  

图必须先 `open_map` 打开才能读到完整元数据。

### 入参（Query）

本接口在本版后端不读业务参数（只解析路径里的 `mapid`/`version`，`version="_"` 自动取最新版本）。官方 openapi 记载的 `enableEditMeta`、`fileid` 在后端没有消费点。

### 出参（节选）

字段非常多，以下是二次开发最常用的子集：

| 字段 | 类型 | 含义 |
|------|------|------|
| `mapid / name / version / fileid / uploadname / filename` | string | 定位 |
| `bounds` | string("[minx,miny,maxx,maxy]") | 整图范围 |
| `dbBounds` / `drawBounds` / `initBounds` / `viewBounds` | string | 各阶段范围 |
| `createTime` / `time` / `mtime` | string | 时间戳 |
| `layers` | JSON 字符串（数组） | 图层列表 |
| `styles` | JSON 字符串（数组） | 样式列表 |
| `layouts` | string，逗号分隔 | 布局名 |
| `findFonts` / `preferableFonts` | JSON/字符串 | 字体字典 |
| `geomRecordCount` | string | 几何记录总数（估值） |
| `ucsorg` | string | UCS 原点矩阵 |
| `pid` | string |  进程 PID（仅 GeomRender） |

> ⚠ 注意：`layers` / `styles` / `findFonts` 等被序列化成**字符串**存在顶层；SDK 不会自动解嵌套 JSON，请按需 `json.loads()`。

### 示例

```python
svc.open_map("sys_zp", "v1", mapopenway="Memory",
             fileid="c47be46a2ad2", uploadname="sys_zp.dwg")
meta = svc.metadata("sys_zp", "v1")
layers = json.loads(meta["layers"])
```

---

## 11. 修改元数据 `update_metadata`

**URL**：`POST /api/v1/map/cmd/updateMetadata/{mapid}/{version}`  

通过 POST body 修改图的元数据字段（如 description、业务标签等）。

### 入参（JSON body）

> ⚠️ 后端行为：body 的**顶层键会被逐个直接合并进元数据**（`meta[key]=value`），不是 `{fields:[{name,value}]}` 数组形式——传 `fields` 数组会原样写进一个名为 `fields` 的元数据键。

```json
{
  "description": "我是新的描述",
  "tags": "building,sample"
}
```

image 类型图另有特判：同时传 `imageLeft`/`imageTop`/`imageResolution` 时会重算 `bounds`/`dbBounds`/`drawBounds`。

### 出参

> ⚠️ 返回 `{}` 空对象，**不是**文档早期声称的"合并后完整元数据"；且写入的 `description`/自定义键在随后 `metadata()` 回读中**不可见**（疑似仅对 image 类型图的 `imageLeft/imageTop/imageResolution` 特判路径或受打开方式约束）。另注意：这是 cmd 路由，**空 JSON body `{}` 直接 HTTP 500**（SDK 不做 noop 兜底），必须至少带一个键。

### 示例

```python
svc.update_metadata("demo_dwg", "v1", body={
    "description": "hello"
})
```

---

## 12. 缩略图 `thumbnail`

**URL**：`GET /api/v1/map/cmd/thumbnail/{mapid}/{version}?width=W&height=H`  

返回 PNG 图片字节流。后端行为：返回的是该图**切片目录下预生成的 `.png` 缓存**，缩放后输出——不是实时渲染，也不支持 `layers`/`backcolor`（这两个参数没有消费点）。

### 常用参数

| Query | 说明 |
|-------|------|
| `width` / `height` | 缩略图像素大小（按此缩放缓存图） |
| `darkTheme` | `true` 时把近黑像素反白，适配深色主题 |

### SDK

```python
svc.open_map("sys_zp", "v1", mapopenway="GeomRender",
             fileid="c47be46a2ad2", uploadname="sys_zp.dwg")
png = svc.thumbnail_bytes("sys_zp", "v1", width=256, height=160)
open("thumb.png","wb").write(png)
```

```javascript
const buf = await svc.thumbnailBytes('sys_zp', 'v1', { width: 256, height: 160 });
fs.writeFileSync('thumb.png', buf);
```

也可以只构造 URL 直接嵌到 `<img src>`：

```python
url = svc.thumbnail_url("sys_zp", "v1", width=128, height=96)
# => http://127.0.0.1:27660/api/v1/map/cmd/thumbnail/sys_zp/v1?width=128&height=96&token=...
```

---

## 13. 支持的格式 `support_format`

**URL**：`GET /api/v1/map/cmd/supportFormat/_/v1`  

### 响应示例

```json
{
  "cad": "dwg;dxf",
  "image": "webp;heic;zip;png;pdf;jpg;jpe;jpc;heif;exif;avif;bmp;dib;tiff;hif;hdr;jfif;gif;tif;jpeg"
}
```

---

## 14. 系统常量 `const_data`

**URL**：`GET /api/v1/map/cmd/constData/_/v1`  

返回系统级字典，最常用的是 **CAD 实体类型号 → AcDb 类名** 的映射。

### 响应示例（节选）

```json
{
  "entTypeIdMap": {
    "1":  "AcDbLine",
    "2":  "AcDbPolyline",
    "5":  "AcDbSpline",
    "6":  "AcDbArc",
    "7":  "AcDbCircle",
    "10": "AcDbBlockReference",
    "11": "AcDbHatch",
    "12": "AcDbMText",
    "13": "AcDbText",
    "34": "AcDb3dSolid"
  }
}
```

搭配 `query_features` 的 `entType` 过滤参数使用。

---

## 15. 数据范围 `get_data_bounds`

**URL**：`GET /api/v1/map/cmd/getDataBounds/{mapid}/{version}`  

返回图上所有 **几何实体**的紧凑包围盒，比 `metadata.bounds`（可能含空图框）更贴合可见数据。

### 响应示例

```json
{
  "bounds": [
    587464241.0047014,
    3103794303.1750364,
    587769759.9828022,
    3104004303.209077
  ],
  "status": true
}
```

### 示例

```python
b = svc.get_data_bounds("sys_zp", "v1")["bounds"]
# 用来设置 vjmap 初始 fitBounds
```

---

- 

> 下一分类：[`02-tiles-styles.md`](02-tiles-styles.md)
