# 组合 / 拆分 / 导出

> 分类代号：C.5  本章共 **6 个接口**

把多张 DWG 组合成一张、把一张大图按图框 / 图层拆成多张、把图纸导出为 PDF / DXF 或离线瓦片包——这一章就是"图纸经纪人"。

**共同特点**：这 6 个接口都相对重，后端会真实创建新图档 / 新文件。测试环境请用临时 mapid，不要对生产图做实验。

---

## 快速索引

| 序号 | SDK 方法 | URL |
|----:|---------|-----|
| 36 | `cmd_compose_new_map` / `cmdComposeNewMap` | `POST /api/v1/map/cmd/composeNewMap/_null/v1` |
| 37 | `cmd_split_map` / `cmdSplitMap` | `POST /api/v1/map/cmd/cmdSplitMap/_null/v1` |
| 38 | `cmd_export_layout` / `cmdExportLayout` | `POST /api/v1/map/cmd/exportLayout/_null/v1` |
| 39 | `cmd_export_pdf` / `cmdExportPdf` | `POST /api/v1/map/cmd/exportPdf/{mapid}/{version}` |
| 40 | `cmd_export_dxf` / `cmdExportDxf` | `POST /api/v1/map/cmd/exportDxf/{mapid}/{version}` |
| 41 | `cmd_save_offline_tile_db` / `cmdSaveOfflineTileDb` | `POST /api/v1/map/cmd/saveOfflineTileDb/_null/v1` |

---

## 36 · 组合新地图 `composeNewMap`

把多张已有 DWG（可各自裁剪范围、四参数变换、按图层过滤）拼成一张新图。

> ⚠️ 参数是**后缀编号**风格，不是 `maps` 数组：第 1 张图用无后缀键（`mapid`、`clipBounds`…），第 2 张起加序号（`mapid2`、`clipBounds2`…），总数由 `count` 声明。

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `count` | int | 参与组合的源图数，默认 1 |
| `mapid{i}` / `version{i}` | string | 第 i 张源图（i=1 无后缀，i≥2 后缀 `2`、`3`…） |
| `clipBounds{i}` | string | 可选：裁剪范围 `[x1,y1,x2,y2]` |
| `selByCrossing{i}` | bool | 裁剪时是否含相交实体（默认 false） |
| `layers{i}` | string | 可选：参与的图层名 JSON 数组，如 `["0","TEXT"]` |
| `fourParameter{i}` | string | 可选四参数变换 `[xOff,yOff,scale,rotRad]` |
| `isInverseFourParamter{i}` | string | `"true"` 反向应用四参数（注意后端拼写 `Paramter`） |
| `layerPrefix{i}` / `layerSuffix{i}` | string | 输出图层名加前/后缀 |
| `savefilename{i}` | string | 可选：该源图裁剪中间结果文件名，缺省自动生成 |
| `purge{i}` | bool | 先对源图 purge 再组合（默认 false） |
| `isFullExtent{i}` | bool | 裁剪结果范围取全图（默认 false） |
| `isRenameBlockNameIfExist` | bool | 块名冲突时自动重命名（默认 true） |
| `subfrom` / `subdependencies` | string | 协同图元数据的直接来源/依赖（高级） |

### 响应

成功（实采双图组合）：

```json
{ "status": true, "fileid": "p2de970c8c9a", "mapfrom": "…", "mapdependencies": "…" }
```

- `fileid` 是**组合产物文件**（`{fileid}.dwg`，`deleteMapFile` 可清理）——不是逐图回写数组。
- `mapfrom` **不是** `subfrom` 的回显——是后端合成的源规格串（`"{mapid}&&{ver}&&{clipbounds}&&0&&…&&1&&"` 形态）；`mapdependencies` 也不回显 `subdependencies`（传了仍返空串）。两字段当**只读回执**看，别当参数回显校验。
- `count` 与实际参数不一致（如 `count=2` 缺 `mapid2`）→ **HTTP 200 + body 级错误** `{error:"file is not exist, ", status:false}`，不抛异常——调用方必须自查 `status`。
- `isInverseFourParamter`（后端错拼 `Paramter`）端到端可用：`fourParameter` + `isInverseFourParamter:true` 组合正常出图。

失败（实采：源文件不存在 / count 与实参不符）：

```json
{
  "error": "file is not exist, ",
  "status": false
}
```

### Python

```python
svc.cmd_compose_new_map(
    count=2,
    mapid="part_a", version="v1",
    clipBounds="[0,0,1000,1000]", layers='["0"]',
    mapid2="part_b", version2="v1",
    fourParameter2="[1000,0,1,0]",   # 第二张图平移 1000
)
```

### Node.js

```javascript
await svc.cmdComposeNewMap({
  count: 2,
  mapid: 'part_a', version: 'v1', clipBounds: '[0,0,1000,1000]',
  mapid2: 'part_b', version2: 'v1', fourParameter2: '[1000,0,1,0]',
});
```

---

## 37 · 拆分子图 `cmdSplitMap`

按一组 bbox 把一张大图拆成多张小图。

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `mapid` / `version` | string | 源图（二选一：`fileid` 直接给 dwg 文件名） |
| `fileid` | string | 源 dwg 文件名，省略时用当前打开的图档 |
| `clipBounds` | array | **必填**：bbox 列表 `[["x1","y1","x2","y2"], ...]`，每框出一张子图。元素也接受逗号串形式 `["x1,y1,x2,y2"]` |
| `isFullExtent` | bool | 子图范围取全图（默认 false） |
| `method` | string | `cloneObjects`（默认，逐实体克隆）/ `cloneDb` / `cloneMap` |
| `accessKey` / `secretKey` | string | 访问受保护图时必填 |

### 响应

成功：`{ "fileids": ["a1.dwg", "a2.dwg", ...] }`（每个 clipBounds 一个新 fileid；**成功时无 `status` 字段**，判失败看 `error`/`status:false`）  
失败（实采：未给 clipBounds）：

```json
{ "error": "clipBounds is empty" }
```

> ⚠️ **fileids 可能回含源文件名**（实采 2026-09-20）：clipBounds 覆盖源图数据范围时，拆分出的子图可以直接就是源 dwg——响应为 `{"fileids":["<mapid>.dwg"]}`。**批量 `deleteMapFile(fileids)` 清理会把地图底图 dwg 一起删掉**（`mapfiles/<mapid>.dwg` 消失，之后 compose/exportLayout 等文件型命令全报 `file is not exist`，但 db 驱动的 openMap/query/渲染不受影响）。清理时务必排除源文件名：`fileids.filter(f => f !== mapid + '.dwg')`。

---

## 38 · 导出布局为 DWG `exportLayout`

从 DWG 的**布局空间**（paper space）单独导出一张新 DWG。CAD 里经常一个文件多个布局（A3、A2、首页），这个接口能一次性把某个布局变成独立图档。

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `mapid` / `version` | string | 源图（version 缺省取最新版） |
| `layoutIndex` | int | 布局序号（0 起） |
| `layoutName` | string | 或直接用布局名（`layoutIndex` 优先，二者都给时按 index） |
| `accessKey` / `secretKey` | string | 访问受保护图时必填 |

> 新图 mapid 由后端根据请求参数 hash 自动生成（`p` 前缀），**不接受** `newMapid` / `newVersion` 请求参数。

### 响应（实采：sys_zp/v1, layoutIndex=0）

```json
{
  "fileid": "pf68817b9655",
  "status": true
}
```

`fileid` 指向后端缓存里的新 DWG 文件，可以接着调用 `open_map(mapid=...)` 打开，或用 `exportDxf / exportPdf` 进一步出图。

> ⚠️ **不存在/bogus mapid 是 body 级错误**：`{status:false, error:'File access error: "…/mapfiles/<mapid>"'}`，HTTP 200 不抛异常——调用方须自查 `status`。

### Python

```python
res = svc.cmd_export_layout(mapid="sys_zp", version="v1", layoutIndex=0)
assert res["status"]
print(res["fileid"])  # 'pf68817b9655'
```

---

## 39 · 导出 PDF `exportPdf`

把当前图档通过 ODA PDF 导出器输出为 PDF。参数是 ODA 导出选项，**不是** bbox 截图式参数（不支持 `bounds`/`width`/`height`/`scale`/`outName`）。

### 请求参数

| 字段 | 类型 | 默认 | 说明 |
|------|------|-----:|------|
| `pageWidth` / `pageHeight` | number | — | 纸张尺寸（mm），两者都给出才生效 |
| `pageLeftMargin` / `pageRightMargin` / `pageTopMargin` / `pageBottomMargin` | number | 0 | 页边距（配合 pageWidth/pageHeight） |
| `bZoomToExtents` | bool | true | 内容缩放到纸张范围 |
| `bActiveLayout` | bool | true | 只导出当前活动布局；false 导出全部布局 |
| `bUsePaperFromOptions` | bool | — | 使用图纸自带页面设置 |
| `geomDPI` / `colorImagesDPI` / `bwImagesDPI` | int | — | 几何/彩色图/黑白图 DPI |
| `isBlackWhiteMode` / `isGrayMode` / `isBwModeBlock` / `isUseGrayPolicy` | bool | false | 黑白/灰度输出 |
| `bEmbededTTF` / `bEmbededOptimizedTTF` | bool | false | 嵌入 TTF 字体 |
| `bTTFAsAGeometry` / `bSHXAsAGeometry` | bool | true | 字体转几何 |
| `bSearchableTTF` / `bSearchableSHX` | bool | — | 文本可搜索 |
| `bEnableLayersSupport` / `bIncludeOffLayers` | bool | — | PDF 图层支持 / 含关闭图层 |
| `bUseHiddenLineAlgo` / `bUseSimpleGeomOpt` | bool | — | 隐藏线算法 / 几何优化 |
| `bHyperlinks` / `bLinearized` / `bMeasuring` / `bExtentsBoundingBox` | bool | — | 超链接 / 线性化 / 测量 / extents 边框 |
| `bEncoded` / `bASCIIhexEncoded` | bool | — | 内容流编码 |
| `nSolidHatchExpType` / `bOtherHatchExpTypekBitmap` | int/bool | — | 填充导出类型 |
| `title` / `author` / `creator` / `producer` | string | — | PDF 文档元信息 |
| `pdfPath` | string | 自动 | 输出路径（缺省写入 download 目录、按参数 hash 命名） |
| `debugLog` / `stripProxyOnRetry` | bool | false/true | 调试日志 / 重试时剥离代理实体 |
| `lineWeight` | number | — | 线宽系数 |

> 图纸内容由 URL 中的 `{mapid}/{version}` 选定的已打开图档决定。
>
> ⚠️ 同参数重复调用**返回不同的 `path` 文件名**（`download/<hash1>.pdf` → `download/<hash2>.pdf`），但第二次明显更快（565ms→7ms）——后端内部有加速，但**不要假设 `path` 相同即缓存复用**，要复用文件请存好首次返回的 `path`。

### 响应（实采：sys_zp/v1）

```json
{
  "path": "download/d5f939154c.pdf",
  "status": true
}
```

完整下载 URL 是 `http://127.0.0.1:27660/{path}?token=<jwt>`。

### Python

```python
res = svc.cmd_export_pdf(
    mapid="sys_zp", version="v1",
    pageWidth=297, pageHeight=210,
)
url = svc.base_gateway() + res["path"] + f"?token={svc.token}"
```

### Node.js

```javascript
const res = await svc.cmdExportPdf({
  mapid: 'sys_zp', version: 'v1',
  pageWidth: 297, pageHeight: 210,
});
const url = `${svc.baseGateway()}${res.path}?token=${svc.token}`;
```

---

## 40 · 导出 DXF `exportDxf`

把当前图档**整体**导出为 DXF 文件（不支持裁剪/选层——导出的是整个 database）。

### 请求参数

> ⚠️ cmd 路由对**空 JSON body 的 POST** 回 **HTTP 500 param error**，本接口同样适用。SDK 已处理：除 mapid/version 外没有其它参数时 `cmdExportDxf` / `cmdExportPdf` 改走 GET（GET 不校验），按缺省参数导出；手写 REST 时要么 GET，要么 POST 至少带一个字段（如 `cadVersion:"*"`）。

| 字段 | 类型 | 说明 |
|------|------|------|
| `filename` | string | 输出文件名（自动补 `.dxf`；缺省用源图名 + `_.dxf`） |
| `cadVersion` | string | 目标 CAD 版本：`2000`/`2004`/`2007`/`2010`/`2013`/`2018`；`*` = 沿用源图版本 |

### 响应

成功：`{ "status": true, "fileid": "xxx.dxf" }`（`fileid` 是后端 mapfiles 目录里的文件名）  
失败（实采：当前地图没有可导出图元）：

```json
{
  "error": "Invalid Symbol Table name",
  "status": false
}
```

### Python

```python
res = svc.cmd_export_dxf(
    mapid="sys_zp", version="v1",
    filename="sys_zp_out",   # 实际写 sys_zp_out.dxf
    cadVersion="2018",
)
```

---

## 41 · 离线瓦片包 `saveOfflineTileDb`

把指定地图**已有**的栅格/矢量瓦片库按 zoom 范围抽成一个新的离线 sqlite 包（`offlinetiles/<id>.db`）。注意：它**复制既有瓦片**，不切新瓦片——源图需先生成过瓦片。

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `mapid` / `version` | string | 源图 |
| `layers` | string | **必填**：db_source 中的 style 表名（`s<hex>` 形态，即瓦片 URL `/map/tile/{mapid}/{ver}/{sXXX}/…` 里的那段），逗号分隔 |
| `rasterMinZoom` / `rasterMaxZoom` | int | 栅格瓦片 zoom 区间（`max > min` 才导栅格） |
| `vectorMinZoom` / `vectorMaxZoom` | int | 矢量瓦片 zoom 区间（`max > min` 才导矢量） |

> 输出文件名后端自动生成（`_yyyyMMddhhmmss_NNN`），不接受 `outName`/`bounds`/`ismvt`/`minzoom`/`maxzoom`。

### 响应

成功：`{ "status": true, "dbfilename": "<绝对路径>.db" }`  
失败（实采：缺 layers 或 zoom 区间为空）：

```json
{ "error": "param error" }
```

> ⚠️ `layers` 的值被**原样插进后端 SQL**（`insert into <layers> select * from db_source.<layers> where zoom_level >= …`）。给了不存在的表名时返回的是 **HTTP 200 + body 级** `{ "error": "exec sql error: insert into …" }` 而非 `param error`——调用方要检查 `r.error`，不能只看 `status`/`dbfilename`。文档旧示例里的 `layers="s_default"` 就是一条 SQL 错误；正确值是真实 style 表名（如 `s6d04d896e`）。

### Python

```python
res = svc.cmd_save_offline_tile_db(
    mapid="sys_zp", version="v1",
    layers="s6d04d896e",  # 用瓦片 URL 里的 style 表名（s<hex>），不是 "s_default"
    rasterMinZoom=0, rasterMaxZoom=7,
    vectorMinZoom=0, vectorMaxZoom=7,
)
```

---

## 组合示例：DWG → PDF 一把梭

```python
# 1. 上传并打开 DWG
up = svc.upload_map("project.dwg")
svc.open_map(
    mapid="proj", version="v1",
    fileid=up["fileid"], uploadname=up["uploadname"],
    mapopenway=vjmap.MapOpenWay.GeomRender,
)

# 2. 导出第 0 号布局（新图 mapid 由后端 hash 生成，看返回 fileid）
lay = svc.cmd_export_layout(mapid="proj", version="v1", layoutIndex=0)

# 3. 打开刚生成的布局图并导 PDF（fileid=12 位 hash，uploadname=<fileid>.dwg）
svc.open_map(mapid="proj_layout0", version="v1",
             fileid=lay["fileid"], uploadname=lay["fileid"] + ".dwg",
             mapopenway=vjmap.MapOpenWay.GeomRender)
pdf = svc.cmd_export_pdf(mapid="proj_layout0", version="v1",
                          pageWidth=297, pageHeight=210)
print(svc.base_gateway() + pdf["path"])
```

---
