<template>
  <div id="controlPanelBox">
    <div>
      <div class="sceneNameBox">
        <div class="">
          场景名称：
          <el-cascader
            :disabled="$route.query.sceneId != undefined"
            v-model="SceneValue"
            placeholder="试试搜索：距离"
            :options="SceneList"
            :props="{ expandTrigger: 'hover' }"
            @change="handleChange"
            filterable
          ></el-cascader>
          <el-tooltip
            v-if="SceneValue && dmsServerItem.functionalDefinition"
            :content="dmsServerItem.functionalDefinition"
            placement="bottom"
            :popper-style="{ maxWidth: '300px' }"
          >
            <el-icon
              style="margin-left: 1rem; width: 1rem; height: 1rem"
              v-show="SceneValue && dmsServerItem.functionalDefinition"
              ><QuestionFilled
            /></el-icon>
          </el-tooltip>
        </div>
        <div>
          <el-button type="primary" @click="sendGeometriesToBackend"
            >发送<i class="el-icon-s-promotion el-icon--right"></i
          ></el-button>
        </div>
      </div>
      <el-divider></el-divider>
      <div>元素个数：{{ SceneValue && dmsServerItem ? dmsServerItem.numberOf : "0" }}</div>
      <div>参数类型：{{ SceneValue && dmsServerItem ? dmsServerItem.elementTypes : "[]" }}</div>
      <div>接口地址：{{ SceneValue && dmsServerItem ? dmsServerItem.apiUrl : "/" }}</div>
    </div>
    <el-divider></el-divider>
    <div>
      <!-- 元素文本渲染和操作区域 -->
      <el-tabs tab-position="left" style="height: calc(100vh - 250px)" class="demo-tabs">
        <el-tab-pane label="入参">
          <div v-if="SceneValue && dmsServerItem && ifHasType('file')">
            <el-upload
              ref="sceneFileUpload"
              v-model:file-list="fileList"
              class="upload-demo"
              action=""
              accept=".geojson,.csv,.xlsx,.zip"
              :limit="1"
              :on-change="handleFileChange"
              :on-exceed="handleExceed"
              :on-remove="uploadRemove"
              :auto-upload="false"
            >
              <el-button type="primary">上传文件</el-button>
            </el-upload>
          </div>
          <div v-if="SceneValue && dmsServerItem">
            <div v-for="item in dmsServerItem.elementTypes" :key="item">
              <div
                v-if="SceneValue && dmsServerItem && selectOptions[item]"
                style="margin: 0.5rem 0"
              >
                {{ item }}：
                <el-input
                  v-if="item === 'geocodeColumnKeyword' && isGeocodeImportZip"
                  v-model="params[item]"
                  placeholder="zip 内含多个表格时请手动输入列名"
                  clearable
                  style="width: 240px"
                  @input="handleSelectChange(item, $event)"
                />
                <el-select
                  v-else-if="selectOptions[item]"
                  @change="handleSelectChange(item, $event)"
                  v-model="params[item]"
                  :placeholder="
                    item === 'geocodeColumnKeyword' && (!selectOptions[item] || !selectOptions[item].length)
                      ? '请先上传 csv / xlsx 解析列名'
                      : '请选择' + item
                  "
                  style="width: 240px"
                  :disabled="
                    item === 'geocodeColumnKeyword' &&
                    !isGeocodeImportZip &&
                    (!selectOptions[item] || !selectOptions[item].length)
                  "
                  filterable
                  clearable
                >
                  <el-option
                    v-for="item2 in selectOptions[item]"
                    :key="item2.value"
                    :label="item2.label"
                    :value="item2.value"
                  />
                </el-select>
              </div>
              <div
                v-if="
                  ['lon', 'lat', 'lonKey', 'latKey', 'filePath', 'EPSILON', 'distance'].includes(
                    item
                  )
                "
              >
                {{ item }}:
                <el-input
                  v-model="params[item]"
                  @input="handleSelectChange(item, $event)"
                  style="width: 240px"
                  :placeholder="'请输入' + item"
                />
              </div>
            </div>
          </div>
          <div
            class="vueJsonEditor_box"
            v-show="
              SceneValue &&
              dmsServerItem &&
              (ifHasType('point') || ifHasType('polyline') || ifHasType('polygon'))
            "
          >
            <div class="vueJsonEditor_tools">
              <span
                v-if="jsonData && (jsonData.features || jsonData.geometry)"
                @click="showToMap(jsonData, 'input')"
                >渲染到地图中</span
              >
              <el-tooltip content="定位到当前入参渲染要素" placement="top">
                <span v-if="renderStatus.input" @click="locateRenderedGeojson('input')">
                  定位
                </span>
              </el-tooltip>
              <span @click="copyJsonData(jsonData)">copy</span>
            </div>
            <vue-json-editor
              :key="'json-input-editor-' + inputEditorKey"
              v-model="jsonData"
              :value="jsonData"
              :show-btns="false"
              :mode="'code'"
              :lang="'zh'"
              @json-change="handleJsonChange"
            >
            </vue-json-editor>
          </div>
        </el-tab-pane>
        <el-tab-pane label="返回">
          <div
            v-if="backData.message || backData.error"
            :style="{
              backgroundColor: backData.code === 200 ? '#67C23A' : '#F56C6C',
              color: '#fff',
              padding: '5px 10px',
              fontSize: '14px',
            }"
          >
            {{ backData.message || backData.error }}
          </div>

          <div class="vueJsonEditor_box" v-show="SceneValue && dmsServerItem">
            <div class="vueJsonEditor_tools">
              <span
                v-if="backData.content && (backData.content.features || backData.content.geometry)"
                @click="showToMap(backData.content, 'output')"
                >渲染到地图中</span
              >
              <el-tooltip content="定位到当前返回渲染要素" placement="top">
                <span v-if="renderStatus.output" @click="locateRenderedGeojson('output')">
                  定位
                </span>
              </el-tooltip>
              <span @click="copyJsonData(backData.content)">copy</span>
            </div>
            <vue-json-editor
              v-if="backData.content"
              :key="'json-output-editor-' + outputEditorKey"
              v-model="backData.content"
              :value="backData.content"
              @json-change="handleJsonChange2"
              :show-btns="false"
              :mode="'code'"
              :lang="'zh'"
            >
            </vue-json-editor></div
        ></el-tab-pane>
      </el-tabs>
    </div>
    <el-dialog
      v-model="propertyDialog.visible"
      title="要素属性"
      width="420px"
      draggable
      :modal="false"
      append-to-body
      class="feature-property-dialog"
    >
      <div class="feature-property-content">
        <div class="feature-property-tip">
          支持直接编辑属性值，失焦后自动同步 JSON。选中地图上的点/线/面后，可按住顶点拖动以实时修改坐标（与右侧入参 JSON 同步）。
        </div>
        <div
          class="feature-property-item"
          v-for="item in propertyDialog.list"
          :key="'feature-property-' + item.id"
        >
          <div class="feature-property-row">
            <el-input
              v-model="item.editKey"
              class="feature-property-input feature-property-key-input"
              placeholder="属性名"
              @change="handlePropertyKeyChange(item)"
            />
            <el-button
              type="danger"
              plain
              size="small"
              class="feature-property-delete-btn"
              @click="deleteProperty(item)"
            >
              删除
            </el-button>
          </div>
          <el-input
            v-model="item.editValue"
            class="feature-property-input"
            placeholder="属性值"
            @change="handlePropertyValueChange(item)"
          />
        </div>
        <el-empty
          v-if="!propertyDialog.list.length"
          description="当前要素无属性信息"
          :image-size="80"
        />
      </div>
      <template #footer>
        <div class="feature-property-footer">
          <el-button type="primary" plain @click="addProperty">+ 添加字段</el-button>
          <div class="feature-property-actions">
            <el-button link type="primary" @click="startEditGeometry">更改绘制</el-button>
            <el-button link type="danger" @click="deleteSelectedFeature">删除</el-button>
          </div>
        </div>
      </template>
    </el-dialog>
    <div
      v-if="hoverHint.visible"
      class="map-edit-hover-hint"
      :style="{ left: hoverHint.left + 'px', top: hoverHint.top + 'px' }"
    >
      {{ hoverHint.text }}
    </div>
    <!-- 绘制工具栏 -->
    <div
      class="toolbar"
      v-if="
        SceneValue &&
        dmsServerItem &&
        (ifHasType('point') || ifHasType('polyline') || ifHasType('polygon'))
      "
    >
      <div
        v-if="ifHasType('point')"
        class="tool-item"
        @click="activateDraw('point')"
        :class="{ active: currentTool === 'point' }"
      >
        绘制点
        <el-tooltip
          content="绘制开关，蓝色状态代表开启，再次点击结束绘制，地图左键点击选点，"
          placement="right"
        >
          <el-icon style="width: 1rem; height: 1rem"><QuestionFilled /></el-icon>
        </el-tooltip>
      </div>
      <div
        v-if="ifHasType('polyline')"
        class="tool-item"
        @click="activateDraw('polyline')"
        :class="{ active: currentTool === 'polyline' }"
      >
        绘制线<el-tooltip
          content="绘制线最少需要两个点，鼠标左键点击选点，点之间自动连线，鼠标右键结束绘制"
          placement="right"
        >
          <el-icon style="width: 1rem; height: 1rem"><QuestionFilled /></el-icon>
        </el-tooltip>
      </div>
      <div
        v-if="ifHasType('polygon')"
        class="tool-item"
        @click="activateDraw('polygon')"
        :class="{ active: currentTool === 'polygon' }"
      >
        绘制面<el-tooltip
          content="绘制面最少需要三个点，鼠标左键点击选点，面自动必合，鼠标右键结束绘制"
          placement="right"
        >
          <el-icon style="width: 1rem; height: 1rem"><QuestionFilled /></el-icon>
        </el-tooltip>
      </div>
      <!-- <div class="tool-item" @click="startHoleDrawing" :class="{ active: isDrawingHole }">
        绘制镂空
      </div> -->
      <div class="tool-item" @click="clearAll">清除所有</div>
    </div>
  </div>
</template>

<script>
import * as XLSX from "xlsx";
import wgn from "../../api/wgn";
import { getFile } from "../../utils/request";
import { WGN_SCENE_LIST } from "@/data/wgnSceneList";
// 控制面板
export default {
  name: "ControlPanel",
  // 2. 接收父组件传递的 props（单向数据流，子组件不能直接修改）
  props: {
    btnText: {
      type: String, // 类型限制
      default: "默认按钮", // 默认值
      required: false, // 是否必传
    },
    showCount: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      currentFile: null, // 当前选中的文件
      fileList: [],
      // 其他参数
      params: {
        unit: "",
        outFileType: "",
        outPrj: "",
        inPrj: "",
        compressionRatio: "",
        lon: "",
        lat: "",
        // 非空转空时，类型字段下拉选择[地址\村居\街镇\区县\要素ID]
        keyType: "",
        // 非空转空时，上传文件解析csv或xlsx文件后，渲染列名到下拉选择框中，指定某列作为类型字段
        geocodeColumnKeyword: "",
      },
      // 请求参数
      jsonData: {},
      // 返回参数
      backData: {},
      // 当前场景值
      SceneValue: "",
      // 场景列表（与微功能中心侧栏同源）
      SceneList: WGN_SCENE_LIST,
      dmsServerItem: {},
      selectOptions: {
        // 单位
        unit: [
          {
            value: "METER",
            label: "米",
          },
          {
            value: "KILOMETER",
            label: "千米",
          },
        ],
        inPrj: [
          {
            value: "WGS84",
            label: "WGS84",
          },
          {
            value: "SH2000",
            label: "SH2000",
          },
          {
            value: "UTM",
            label: "UTM",
          },
          {
            value: "WEB_Mercator",
            label: "WEB_Mercator",
          },
        ],
        compressionRatio: [
          {
            value: "100%",
            label: "100%",
          },
          {
            value: "80%",
            label: "80%",
          },
          {
            value: "50%",
            label: "50%",
          },
          {
            value: "25%",
            label: "25%",
          },
        ],
        outPrj: [
          {
            value: "WGS84",
            label: "WGS84",
          },
          {
            value: "SH2000",
            label: "SH2000",
          },
          {
            value: "UTM",
            label: "UTM",
          },
          {
            value: "WEB_Mercator",
            label: "WEB_Mercator",
          },
        ],
        outFileType: [
          {
            value: "geoJson",
            label: "geoJson",
          },
          {
            value: "shp",
            label: "shp",
          },
          {
            value: "csv",
            label: "csv",
          },
          {
            value: "xlsx",
            label: "xlsx",
          },
        ],
        keyType: [
          { value: "地址", label: "地址" },
          { value: "村居", label: "村居" },
          { value: "街镇", label: "街镇" },
          { value: "区县", label: "区县" },
          // { value: "要素ID", label: "要素ID" },
        ],
        geocodeColumnKeyword: [],
      },
      currentTool: null,
      drawingMode: null,
      handler: null,
      drawnEntities: [],
      geometries: [], // 存储绘制的几何对象
      // 绘制状态相关
      currentEntity: null, // 当前正在绘制的实体
      currentPositions: [], // 当前正在绘制的位置数组
      isDrawingHole: false, // 是否正在绘制镂空
      currentPolygonEntity: null, // 当前要添加镂空的多边形实体
      currentPolygonGeometry: null, // 当前要添加镂空的多边形几何对象
      tempEntity: null, // 临时预览实体
      // 地图渲染状态（入参/返回）
      renderStatus: {
        input: false,
        output: false,
      },
      // 缓存两侧最近一次成功渲染的geojson，用于定位时恢复
      renderedGeojsonCache: {
        input: null,
        output: null,
      },
      currentRenderedSource: "",
      // 地图点击属性弹窗
      featurePickHandler: null,
      inputEditorKey: 0,
      outputEditorKey: 0,
      propertyIdSeed: 1,
      propertyDialog: {
        visible: false,
        list: [],
        source: "",
        propertiesRef: null,
        featureIndex: -1,
      },
      /** 地图编辑选中：高亮要素 + 顶点柄 */
      mapEditSelection: null,
      vertexHandleEntities: [],
      hoveredVertexIndex: -1,
      hoveredHandleEntity: null,
      hoveredFeatureEntity: null,
      geometryEditMode: false,
      pickedFeatureRef: null,
      hoverHint: {
        visible: false,
        text: "",
        left: 0,
        top: 0,
      },
      /** 拖动/绘制时临时关闭 requestRenderMode，避免面片异步剖分导致与顶点不同步 */
      _interactiveRenderDepth: 0,
      _savedSceneRenderMode: null,
    };
  },
  computed: {
    /** zip 多表场景下列名无法预解析，改用手动输入 */
    isGeocodeImportZip() {
      const f = this.currentFile;
      const name = f ? f.name || (f.raw && f.raw.name) || "" : "";
      return String(name).toLowerCase().endsWith(".zip");
    },
  },
  mounted() {
    this.SceneValue = this.$route.query.sceneId ? this.$route.query.sceneId : "";
  },

  beforeUnmount() {
    // 组件卸载前清理资源
    this.deactivateDraw();
    if (this.handler) {
      this.handler.destroy();
    }
    if (this.featurePickHandler) {
      this.featurePickHandler.destroy();
      this.featurePickHandler = null;
    }
    this.resetPickDragState();
    this.setScreenSpaceCameraForDrag(true);
    while (this._interactiveRenderDepth > 0) {
      this.exitInteractiveSceneRender();
    }
  },
  watch: {
    SceneValue(newVal, oldVal) {
      if (newVal && newVal != oldVal) {
        // 请求DMS
        this.searchServerList(this.SceneValue);
      }
    },
    dmsServerItem: {
      handler() {
        this.$nextTick(() => this.syncDrawToolWithElementTypes());
      },
      deep: true,
    },
  },
  methods: {
    /** 与 DMS 入参规则 c_input_parameter_rules 解析后的 elementTypes 对齐，支持少量别名 */
    expandParameterTypeAliases(type) {
      const aliases = [type, type + "Z"];
      if (type === "polyline") {
        aliases.push("line", "lineZ", "LineString", "LineStringZ");
      }
      if (type === "polygon") {
        aliases.push("multipolygon", "MultiPolygon", "MultiPolygonZ");
      }
      return aliases;
    },
    ifHasType(type) {
      if (
        !this.dmsServerItem ||
        !this.dmsServerItem.elementTypes ||
        this.dmsServerItem.elementTypes.length === 0
      ) {
        return false;
      }
      const declared = this.dmsServerItem.elementTypes;
      const candidates = this.expandParameterTypeAliases(type);
      return declared.some((t) => candidates.includes(t));
    },
    /** DMS 声明的单个参数是否属于点/线/面几何槽位，返回规范名 point | polyline | polygon */
    geometrySlotCanonical(declaredType) {
      if (declaredType == null || declaredType === "") {
        return null;
      }
      const d = String(declaredType);
      for (const canon of ["point", "polyline", "polygon"]) {
        const aliases = this.expandParameterTypeAliases(canon);
        const hit = aliases.some(
          (a) => String(a) === d || String(a).toLowerCase() === d.toLowerCase()
        );
        if (hit) {
          return canon;
        }
      }
      return null;
    },
    /** GeoJSON geometry.type -> 与 geometrySlotCanonical 同一套规范名 */
    featureGeometryCanonical(geoJsonType) {
      if (!geoJsonType) {
        return null;
      }
      const t = String(geoJsonType);
      if (t === "Point") {
        return "point";
      }
      if (t === "LineString" || t === "MultiLineString") {
        return "polyline";
      }
      if (t === "Polygon" || t === "MultiPolygon") {
        return "polygon";
      }
      return null;
    },
    /**
     * 发送前校验：当「元素个数」numberOf > 0 时，与入参 FeatureCollection.features 数量对齐；
     * numberOf 为 0 时表示几何要素个数不固定（支持多个），不拦截数量。
     * 「参数类型」elementTypes 中的 point/polyline/polygon 仅表示允许的几何类型（可多选），
     * 不要求每种类型各来一个，也不要求与声明顺序一一对应（忽略 unit/file 等非几何项）。
     */
    validateSendGeometryParams() {
      if (!this.dmsServerItem || !Array.isArray(this.dmsServerItem.elementTypes)) {
        return { ok: true };
      }
      const types = this.dmsServerItem.elementTypes;
      const geoSlots = types
        .map((declared) => ({
          declared,
          canon: this.geometrySlotCanonical(declared),
        }))
        .filter((s) => s.canon != null);

      if (geoSlots.length === 0) {
        return { ok: true };
      }

      const allowedCanonSet = new Set(geoSlots.map((s) => s.canon));

      const rawN = this.dmsServerItem.numberOf;
      let expectedGeomCount = null;
      if (rawN !== undefined && rawN !== null && rawN !== "") {
        const parsedN = typeof rawN === "number" ? rawN : Number(rawN);
        // 0 表示不固定个数、支持多个要素，不做「元素个数」与 features 数量对齐拦截
        if (Number.isFinite(parsedN) && parsedN > 0) {
          expectedGeomCount = parsedN;
        }
      }

      const fc = this.jsonData;
      const features = fc && Array.isArray(fc.features) ? fc.features : [];

      if (expectedGeomCount !== null) {
        if (features.length > expectedGeomCount) {
          return {
            ok: false,
            message: `入参几何要素过多：当前 ${features.length} 个，场景要求 ${expectedGeomCount} 个（元素个数）`,
          };
        }
        if (features.length < expectedGeomCount) {
          return {
            ok: false,
            message: `入参几何要素不足：当前 ${features.length} 个，场景要求 ${expectedGeomCount} 个（元素个数），请绘制或渲染足够要素`,
          };
        }
      }

      for (let i = 0; i < features.length; i++) {
        const f = features[i];
        const g = f && f.geometry;
        if (!g || !g.type) {
          return {
            ok: false,
            message: `第 ${i + 1} 个几何要素缺少有效的 geometry`,
          };
        }
        const fk = this.featureGeometryCanonical(g.type);
        if (!fk) {
          return {
            ok: false,
            message: `第 ${i + 1} 个几何要素类型「${g.type}」不符合场景要求，请使用点、线或面`,
          };
        }
        if (!allowedCanonSet.has(fk)) {
          const allowedLabel = [...allowedCanonSet].join("、");
          return {
            ok: false,
            message: `第 ${i + 1} 个几何要素类型「${g.type}」不在场景允许的类型内，允许：${allowedLabel}`,
          };
        }
      }

      return { ok: true };
    },
    /** 场景切换后若当前绘制类型不在参数类型中，则关闭绘制避免“隐形”仍在绘制 */
    syncDrawToolWithElementTypes() {
      if (!this.currentTool) {
        return;
      }
      const t = this.currentTool;
      if (t === "point" && !this.ifHasType("point")) {
        this.deactivateDraw();
      } else if (t === "polyline" && !this.ifHasType("polyline")) {
        this.deactivateDraw();
      } else if (t === "polygon" && !this.ifHasType("polygon")) {
        this.deactivateDraw();
      }
    },
    handleJsonChange(jsonStr) {
      this.jsonData = jsonStr;
    },
    handleJsonChange2(jsonStr) {
      this.backData.content = jsonStr;
    },
    // 搜索微功能服务
    searchServerList() {
      let requestParams = {
        columnId: systemConfig.columnIds[5],
        states: 0,
        pageSize: 999,
        page: 0,
      };
      if (this.SceneValue) {
        requestParams.search = JSON.stringify([
          {
            field: "c_scene_name",
            searchType: 1,
            content: { value: this.SceneValue },
          },
        ]);
        // 获取微功能服务列表
        wgn
          .getDmsData(requestParams)
          .then((res) => {
            if (res.code === 200 && res.content.data[0]) {
              let dmsServiceData = res.content.data[0];
              this.dmsServerItem = {
                // 功能描述
                functionalDefinition: dmsServiceData.content,
                // 元素类型
                elementTypes: JSON.parse(dmsServiceData.c_input_parameter_rules),
                // 元素个数
                numberOf: JSON.parse(dmsServiceData.c_number_of_elements),
                // 后台接口路径
                apiUrl: dmsServiceData.c_url,
              };
            } else {
              this.$message({
                message: "搜索微功能服务失败,请检查场景名称是否正确",
                type: "warning",
              });
            }
          })
          .catch((e) => {
            this.$message({
              message: "搜索微功能服务失败" + e,
              type: "error",
            });
          });
      }
    },
    getSourceGeojsonData(source) {
      return source === "output" ? this.backData.content : this.jsonData;
    },
    syncGeometriesFromRenderedSource() {
      const src = this.currentRenderedSource || "input";
      const geojson = this.getSourceGeojsonData(src);
      if (!geojson) {
        return;
      }
      const list = [];
      if (geojson.features && Array.isArray(geojson.features)) {
        geojson.features.forEach((f) => {
          if (f && f.geometry && f.geometry.type && f.geometry.coordinates) {
            list.push({
              type: f.geometry.type,
              coordinates: JSON.parse(JSON.stringify(f.geometry.coordinates)),
            });
          }
        });
      } else if (geojson.geometry && geojson.geometry.type && geojson.geometry.coordinates) {
        list.push({
          type: geojson.geometry.type,
          coordinates: JSON.parse(JSON.stringify(geojson.geometry.coordinates)),
        });
      }
      if (list.length) {
        this.geometries = list;
      }
    },
    // 将用户输入或后台返回的geojson渲染到地图中
    showToMap(geojson, source = "input") {
      if (!geojson || (!geojson.features && !geojson.geometry)) {
        this.$message({
          message: "当前JSON缺少有效几何信息，无法渲染",
          type: "warning",
        });
        return;
      }
      this.currentRenderedSource = source;
      // 1. 清除所有地图中的元素
      this.clearAllMap();
      // 2. 将geojson添加到地图中
      this.addToMap(geojson);
      this.currentRenderedSource = source;
      this.renderStatus[source] = true;
      // 仅缓存纯数据，避免响应式对象带来的引用副作用
      this.renderedGeojsonCache[source] = JSON.parse(JSON.stringify(geojson));
      this.flyToGeojson(geojson);
    },
    // 定位到已渲染要素
    locateRenderedGeojson(source) {
      if (!this.renderStatus[source]) {
        return;
      }
      // 当前地图不是该来源数据时，优先恢复对应渲染结果，再飞行定位
      if (this.currentRenderedSource !== source && this.renderedGeojsonCache[source]) {
        this.showToMap(this.renderedGeojsonCache[source], source);
        return;
      }
      const sourceGeojson = this.getSourceGeojsonData(source) || this.renderedGeojsonCache[source];
      this.flyToGeojson(sourceGeojson);
    },
    collectGeometryCoordinates(geometry) {
      if (!geometry || !geometry.coordinates) {
        return [];
      }
      const { type, coordinates } = geometry;
      const points = [];
      if (type === "Point") {
        points.push([coordinates[0], coordinates[1]]);
      } else if (type === "LineString") {
        coordinates.forEach((coord) => points.push([coord[0], coord[1]]));
      } else if (type === "Polygon") {
        coordinates.forEach((ring) => {
          ring.forEach((coord) => points.push([coord[0], coord[1]]));
        });
      } else if (type === "MultiPolygon") {
        coordinates.forEach((polygon) => {
          polygon.forEach((ring) => {
            ring.forEach((coord) => points.push([coord[0], coord[1]]));
          });
        });
      }
      return points;
    },
    collectGeojsonCoordinates(geojson) {
      if (!geojson) {
        return [];
      }
      if (geojson.features && Array.isArray(geojson.features)) {
        return geojson.features.flatMap((feature) =>
          this.collectGeometryCoordinates(feature.geometry)
        );
      }
      if (geojson.geometry) {
        return this.collectGeometryCoordinates(geojson.geometry);
      }
      return [];
    },
    // 飞行定位到geojson范围
    flyToGeojson(geojson) {
      if (!viewer || !geojson) {
        return;
      }
      const points = this.collectGeojsonCoordinates(geojson);
      if (!points.length) {
        return;
      }
      if (points.length === 1) {
        viewer.camera.flyTo({
          destination: SkyScenery.Cartesian3.fromDegrees(points[0][0], points[0][1], 1200),
          duration: 1.1,
          orientation: {
            heading: viewer.camera.heading,
            pitch: SkyScenery.Math.toRadians(-65),
            roll: 0,
          },
        });
        viewer.scene.requestRender();
        return;
      }
      let minLon = Infinity;
      let maxLon = -Infinity;
      let minLat = Infinity;
      let maxLat = -Infinity;
      points.forEach((point) => {
        minLon = Math.min(minLon, point[0]);
        maxLon = Math.max(maxLon, point[0]);
        minLat = Math.min(minLat, point[1]);
        maxLat = Math.max(maxLat, point[1]);
      });
      const lonPad = Math.max((maxLon - minLon) * 0.25, 0.0008);
      const latPad = Math.max((maxLat - minLat) * 0.25, 0.0008);
      viewer.camera.flyTo({
        destination: SkyScenery.Rectangle.fromDegrees(
          minLon - lonPad,
          minLat - latPad,
          maxLon + lonPad,
          maxLat + latPad
        ),
        duration: 1.1,
      });
      viewer.scene.requestRender();
    },
    // 把属性对象格式化为可编辑列表
    formatFeatureProperties(properties) {
      if (!properties || typeof properties !== "object") {
        return [];
      }
      return Object.keys(properties).map((key) => {
        const rawValue = properties[key];
        const editValue =
          rawValue === null || rawValue === undefined
            ? ""
            : typeof rawValue === "object"
              ? JSON.stringify(rawValue)
              : String(rawValue);
        return {
          id: this.propertyIdSeed++,
          originalKey: key,
          editKey: key,
          editValue,
        };
      });
    },
    parsePropertyValue(inputValue) {
      if (typeof inputValue !== "string") {
        return inputValue;
      }
      const value = inputValue.trim();
      if (value === "") {
        return "";
      }
      if (value === "true") {
        return true;
      }
      if (value === "false") {
        return false;
      }
      if (value === "null") {
        return null;
      }
      if (!isNaN(Number(value))) {
        return Number(value);
      }
      if (
        (value.startsWith("{") && value.endsWith("}")) ||
        (value.startsWith("[") && value.endsWith("]"))
      ) {
        try {
          return JSON.parse(value);
        } catch (e) {
          return inputValue;
        }
      }
      return inputValue;
    },
    syncJsonEditorData(source) {
      if (source === "output") {
        if (!this.backData.content) {
          return;
        }
        this.backData = {
          ...this.backData,
          content: JSON.parse(JSON.stringify(this.backData.content)),
        };
      } else {
        this.jsonData = JSON.parse(JSON.stringify(this.jsonData));
      }
      if (source === "output") {
        this.outputEditorKey += 1;
      } else {
        this.inputEditorKey += 1;
      }
      this.renderedGeojsonCache[source] = JSON.parse(
        JSON.stringify(this.getSourceGeojsonData(source))
      );
      const latestGeojson = this.getSourceGeojsonData(source);
      if (latestGeojson && this.propertyDialog.visible && this.propertyDialog.source === source) {
        if (
          this.propertyDialog.featureIndex >= 0 &&
          latestGeojson.features &&
          latestGeojson.features[this.propertyDialog.featureIndex]
        ) {
          if (!latestGeojson.features[this.propertyDialog.featureIndex].properties) {
            latestGeojson.features[this.propertyDialog.featureIndex].properties = {};
          }
          this.propertyDialog.propertiesRef =
            latestGeojson.features[this.propertyDialog.featureIndex].properties;
        } else {
          if (!latestGeojson.properties) {
            latestGeojson.properties = {};
          }
          this.propertyDialog.propertiesRef = latestGeojson.properties;
        }
      }
    },
    handlePropertyValueChange(item) {
      if (!this.propertyDialog.propertiesRef || !item) {
        return;
      }
      const currentKey = (item.editKey || "").trim();
      if (!currentKey) {
        this.$message({
          message: "属性名不能为空",
          type: "warning",
        });
        item.editKey = item.originalKey || "";
        return;
      }
      this.propertyDialog.propertiesRef[currentKey] = this.parsePropertyValue(item.editValue);
      item.originalKey = currentKey;
      item.editKey = currentKey;
      this.syncPropertyToEntity(this.propertyDialog.propertiesRef);
      this.syncJsonEditorData(this.propertyDialog.source || this.currentRenderedSource || "input");
    },
    handlePropertyKeyChange(item) {
      if (!this.propertyDialog.propertiesRef || !item) {
        return;
      }
      const oldKey = item.originalKey;
      const newKey = (item.editKey || "").trim();
      if (!newKey) {
        this.$message({
          message: "属性名不能为空",
          type: "warning",
        });
        item.editKey = oldKey;
        return;
      }
      if (
        newKey !== oldKey &&
        Object.prototype.hasOwnProperty.call(this.propertyDialog.propertiesRef, newKey)
      ) {
        this.$message({
          message: "属性名已存在，请更换",
          type: "warning",
        });
        item.editKey = oldKey;
        return;
      }
      const oldValue = this.propertyDialog.propertiesRef[oldKey];
      if (newKey !== oldKey) {
        delete this.propertyDialog.propertiesRef[oldKey];
        this.propertyDialog.propertiesRef[newKey] = oldValue;
      }
      item.originalKey = newKey;
      item.editKey = newKey;
      this.syncPropertyToEntity(this.propertyDialog.propertiesRef);
      this.syncJsonEditorData(this.propertyDialog.source || this.currentRenderedSource || "input");
    },
    deleteProperty(item) {
      if (!this.propertyDialog.propertiesRef || !item) {
        return;
      }
      const key = (item.originalKey || "").trim();
      if (key && Object.prototype.hasOwnProperty.call(this.propertyDialog.propertiesRef, key)) {
        delete this.propertyDialog.propertiesRef[key];
      }
      this.propertyDialog.list = this.propertyDialog.list.filter(
        (property) => property.id !== item.id
      );
      this.syncPropertyToEntity(this.propertyDialog.propertiesRef);
      this.syncJsonEditorData(this.propertyDialog.source || this.currentRenderedSource || "input");
    },
    addProperty() {
      if (!this.propertyDialog.propertiesRef) {
        this.$message({
          message: "请先点击地图中的已渲染要素",
          type: "warning",
        });
        return;
      }
      let index = 1;
      let newKey = "newKey";
      while (Object.prototype.hasOwnProperty.call(this.propertyDialog.propertiesRef, newKey)) {
        newKey = `newKey${index}`;
        index += 1;
      }
      this.propertyDialog.propertiesRef[newKey] = "";
      this.propertyDialog.list.push({
        id: this.propertyIdSeed++,
        originalKey: newKey,
        editKey: newKey,
        editValue: "",
      });
      this.syncPropertyToEntity(this.propertyDialog.propertiesRef);
      this.syncJsonEditorData(this.propertyDialog.source || this.currentRenderedSource || "input");
    },
    startEditGeometry() {
      const picked = this.pickedFeatureRef;
      if (!picked || !picked.entity) {
        this.$message({
          message: "请先选择要素",
          type: "warning",
        });
        return;
      }
      this.geometryEditMode = true;
      this.deactivateDraw();
      this.applyMapEditSelection(picked.entity);
      this.propertyDialog.visible = false;
      this.$message({
        message: "已进入节点编辑模式，可拖拽顶点或中点调整绘制",
        type: "success",
      });
    },
    reindexEntityFeatureRefs(source, deletedIndex) {
      this.drawnEntities.forEach((ent) => {
        if (!ent || !ent.__featureRef) return;
        if (
          ent.__featureRef.source === source &&
          typeof ent.__featureRef.featureIndex === "number" &&
          ent.__featureRef.featureIndex > deletedIndex
        ) {
          ent.__featureRef.featureIndex -= 1;
        }
      });
    },
    deleteSelectedFeature() {
      const sel = this.mapEditSelection || this.pickedFeatureRef;
      if (!sel || !sel.entity || sel.featureIndex < 0) {
        this.$message({
          message: "请先选择要素",
          type: "warning",
        });
        return;
      }
      const source = sel.source || this.currentRenderedSource || "input";
      const geojson = this.getSourceGeojsonData(source);
      if (!geojson || !geojson.features || !Array.isArray(geojson.features)) {
        this.$message({
          message: "当前数据不是 FeatureCollection，无法删除",
          type: "warning",
        });
        return;
      }
      const idx = sel.featureIndex;
      if (idx < 0 || idx >= geojson.features.length) {
        this.$message({
          message: "要素索引无效，删除失败",
          type: "error",
        });
        return;
      }
      geojson.features.splice(idx, 1);
      const toRemove = this.drawnEntities.filter(
        (ent) =>
          ent &&
          ent.__featureRef &&
          ent.__featureRef.source === source &&
          ent.__featureRef.featureIndex === idx
      );
      toRemove.forEach((ent) => {
        try {
          viewer.entities.remove(ent);
        } catch (e) {
          /* ignore */
        }
      });
      this.drawnEntities = this.drawnEntities.filter((ent) => !toRemove.includes(ent));
      this.reindexEntityFeatureRefs(source, idx);
      this.propertyDialog.visible = false;
      this.propertyDialog.list = [];
      this.propertyDialog.source = "";
      this.propertyDialog.propertiesRef = null;
      this.propertyDialog.featureIndex = -1;
      this.clearMapEditSelection();
      this.syncJsonEditorData(source);
      this.$message({
        message: "删除成功",
        type: "success",
      });
    },
    syncPropertyToEntity(propertiesRef) {
      if (!propertiesRef || !viewer || !this.currentRenderedSource) {
        return;
      }
      const targetSource = this.propertyDialog.source || this.currentRenderedSource;
      const targetIndex = this.propertyDialog.featureIndex;
      this.drawnEntities.forEach((entity) => {
        if (!entity || !entity.__featureRef) {
          return;
        }
        if (
          entity.__featureRef.source === targetSource &&
          entity.__featureRef.featureIndex === targetIndex
        ) {
          entity.__featureProperties = propertiesRef;
        }
      });
    },
    // 将geojson添加到地图中
    addToMap(geojson) {
      const source = this.currentRenderedSource || "input";
      if (!geojson.features && geojson.geometry) {
        const { type, coordinates } = geojson.geometry;
        const featureProperties = geojson.properties || {};
        const featureRefInfo = {
          source,
          featureIndex: -1,
        };
        switch (type) {
          case "Point":
            // 点
            this.addPoint(coordinates, featureProperties, featureRefInfo);
            break;
          case "LineString":
            // 线
            this.addLine(coordinates, featureProperties, featureRefInfo);
            break;
          case "Polygon":
          case "MultiPolygon":
            // 面
            this.addPolygon(coordinates, featureProperties, featureRefInfo);
            break;
          default:
            break;
        }
      } else if (geojson.features) {
        const features = geojson.features;
        // 2. 遍历features，根据type添加到地图中
        console.log("features", features);
        features.forEach((feature, featureIndex) => {
          const { type, coordinates } = feature.geometry;
          const featureProperties = feature.properties || {};
          const featureRefInfo = {
            source,
            featureIndex,
          };
          switch (type) {
            case "Point":
              // 点
              this.addPoint(coordinates, featureProperties, featureRefInfo);
              break;
            case "LineString":
              // 线
              this.addLine(coordinates, featureProperties, featureRefInfo);
              break;
            case "Polygon":
            case "MultiPolygon":
              // 面
              this.addPolygon(coordinates, featureProperties, featureRefInfo);
              break;
            default:
              break;
          }
        });
      }
      setTimeout(() => {
        viewer.scene.requestRender();
      });
    },
    // 添加点到地图中
    addPoint(coordinates, featureProperties = null, featureRefInfo = null) {
      // 1. 解析点的坐标
      console.log("addPoint coordinates", coordinates);
      // 2. 创建点实体
      const pointEntity = viewer.entities.add({
        name: "point",
        position:
          coordinates.length > 2
            ? SkyScenery.Cartesian3.fromDegrees(coordinates[0], coordinates[1], coordinates[2])
            : SkyScenery.Cartesian3.fromDegrees(coordinates[0], coordinates[1]),
        point: {
          show: true,
          color: SkyScenery.Color.RED,
          pixelSize: 10,
          outlineColor: SkyScenery.Color.WHITE,
          outlineWidth: 2,
        },
      });
      pointEntity.__featureProperties = featureProperties;
      pointEntity.__featureRef = featureRefInfo;
      // 3. 将点实体添加到drawnEntities中
      this.drawnEntities.push(pointEntity);
    },
    // 添加线到地图中
    addLine(coordinates, featureProperties = null, featureRefInfo = null) {
      // 1. 解析线的坐标
      console.log("addLine coordinates", coordinates);
      // 2. 处理坐标格式：如果是二维数组则取第一个元素，否则直接使用
      const lineCoordinates =
        Array.isArray(coordinates[0]) && Array.isArray(coordinates[0][0])
          ? coordinates[0]
          : coordinates;

      // 3. 检测坐标是否包含Z值
      const hasZValues = lineCoordinates.some(
        (coord) => coord.length > 2 && typeof coord[2] === "number"
      );

      // 4. 根据是否有Z值选择合适的坐标转换方法
      let positions;
      if (hasZValues) {
        // 包含Z值，使用带高度的坐标转换方法
        const flatCoordinatesWithHeights = [];
        lineCoordinates.forEach((coord) => {
          flatCoordinatesWithHeights.push(coord[0], coord[1], coord[2] || 0);
        });
        positions = SkyScenery.Cartesian3.fromDegreesArrayHeights(flatCoordinatesWithHeights);
      } else {
        // 不包含Z值，使用普通坐标转换方法
        const flatCoordinates = [];
        lineCoordinates.forEach((coord) => {
          flatCoordinates.push(coord[0], coord[1]);
        });
        positions = SkyScenery.Cartesian3.fromDegreesArray(flatCoordinates);
      }

      // 5. 创建线实体
      const lineEntity = viewer.entities.add({
        name: "line",
        polyline: {
          show: true,
          positions: positions,
          material: SkyScenery.Color.BLUE.withAlpha(0.8),
          width: 3,
        },
      });
      lineEntity.__featureProperties = featureProperties;
      lineEntity.__featureRef = featureRefInfo;

      // 6. 将线实体添加到drawnEntities中
      this.drawnEntities.push(lineEntity);
    },
    // 添加面到地图中
    addPolygon(coordinates, featureProperties = null, featureRefInfo = null) {
      // 检测是否为MultiPolygon类型（MultiPolygon的坐标是三维数组）
      if (
        Array.isArray(coordinates[0]) &&
        Array.isArray(coordinates[0][0]) &&
        Array.isArray(coordinates[0][0][0])
      ) {
        console.log("MultiPolygon coordinates", coordinates);
        // 是MultiPolygon类型，遍历每个Polygon
        coordinates.forEach((polygonCoordinates) => {
          this.renderSinglePolygon(polygonCoordinates, featureProperties, featureRefInfo);
        });
      } else {
        // 是单个Polygon类型
        this.renderSinglePolygon(coordinates, featureProperties, featureRefInfo);
      }
    },
    // 渲染单个Polygon
    renderSinglePolygon(coordinates, featureProperties = null, featureRefInfo = null) {
      // 1. 处理坐标格式：确保获取外部环坐标
      const outerRingCoordinates =
        Array.isArray(coordinates[0]) && Array.isArray(coordinates[0][0])
          ? coordinates[0]
          : coordinates;

      // 2. 检测坐标是否包含Z值
      const hasZValues = outerRingCoordinates.some(
        (coord) => coord.length > 2 && typeof coord[2] === "number"
      );

      // 3. 根据是否有Z值选择合适的坐标转换方法
      let positions;
      let baseHeight = 0;
      let extrudedHeight = 0; // 默认挤压高度

      if (hasZValues) {
        // 包含Z值，使用带高度的坐标转换方法
        const flatCoordinatesWithHeights = [];
        let minZ = Infinity;
        let maxZ = -Infinity;

        // 计算Z值的范围
        outerRingCoordinates.forEach((coord) => {
          const z = coord[2] || 0;
          minZ = Math.min(minZ, z);
          maxZ = Math.max(maxZ, z);
          flatCoordinatesWithHeights.push(coord[0], coord[1], minZ); // 使用最小Z值作为基础高度
        });

        positions = SkyScenery.Cartesian3.fromDegreesArrayHeights(flatCoordinatesWithHeights);
        baseHeight = minZ;
        extrudedHeight = maxZ;
      } else {
        // 不包含Z值，使用普通坐标转换方法
        const flatCoordinates = [];
        outerRingCoordinates.forEach((coord) => {
          flatCoordinates.push(coord[0], coord[1]);
        });
        positions = SkyScenery.Cartesian3.fromDegreesArray(flatCoordinates);
      }

      // 4. 创建面实体
      const polygonEntity = viewer.entities.add({
        name: "polygon",
        polygon: {
          hierarchy: {
            positions: positions,
          },
          height: baseHeight, // 多边形底部高度
          extrudedHeight: extrudedHeight, // 多边形顶部高度，实现3D挤压效果
          heightReference: hasZValues
            ? SkyScenery.HeightReference.NONE
            : SkyScenery.HeightReference.CLAMP_TO_GROUND,
          material: SkyScenery.Color.GREEN.withAlpha(0.5),
          outline: true,
          outlineColor: SkyScenery.Color.WHITE,
          outlineWidth: 2,
          // 设置侧面材质
          extrudedMaterial: SkyScenery.Color.GREEN.withAlpha(0.8),
        },
      });
      polygonEntity.__featureProperties = featureProperties;
      polygonEntity.__featureRef = featureRefInfo;

      // 5. 将面实体添加到drawnEntities中
      this.drawnEntities.push(polygonEntity);
    },
    handleSelectChange(item, value) {
      this.jsonData[item] = value;
    },
    // 复制json数据
    copyJsonData(data) {
      let textarea = document.createElement("textarea");
      try {
        textarea.value = JSON.stringify(data, null, 2);
        document.body.appendChild(textarea);
        // 3. 选中文本（兼容移动端）
        textarea.select();
        // 兼容移动端：设置选择范围覆盖全部文本
        textarea.setSelectionRange(0, textarea.value.length);
        // 4. 执行复制命令
        const isSuccess = document.execCommand("copy");
        if (isSuccess) {
          this.$message({
            message: "复制成功",
            type: "success",
          });
        }
      } catch (err) {
        this.$message({
          message: err,
          type: "error",
        });
      } finally {
        document.body.removeChild(textarea);
      }
    },
    handleExceed(file) {
      this.$message({
        message: "最多只能上传一个文件",
        type: "warning",
      });
    },
    /** 时空格式转换等场景：上传文件仅允许 geojson / csv / xlsx / zip */
    isAllowedSceneUploadFile(file) {
      const name = (file && file.name) || (file.raw && file.raw.name) || "";
      const lower = String(name).toLowerCase();
      return (
        lower.endsWith(".geojson") ||
        lower.endsWith(".csv") ||
        lower.endsWith(".xlsx") ||
        lower.endsWith(".zip")
      );
    },
    handleFileChange(file, fileList) {
      if (!this.isAllowedSceneUploadFile(file)) {
        this.$message.error("仅支持上传 .geojson、.csv、.xlsx、.zip 格式的文件，请重新选择");
        this.currentFile = null;
        this.fileList = [];
        this.$nextTick(() => {
          if (this.$refs.sceneFileUpload && this.$refs.sceneFileUpload.clearFiles) {
            this.$refs.sceneFileUpload.clearFiles();
          }
        });
        return;
      }
      this.currentFile = file;
      this.refreshImportColumnOptions(file);
    },
    uploadRemove() {
      this.currentFile = null;
      this.params.geocodeColumnKeyword = "";
      this.selectOptions.geocodeColumnKeyword = [];
    },
    parseCsvHeaderLine(line) {
      const out = [];
      let cur = "";
      let inq = false;
      for (let i = 0; i < line.length; i++) {
        const c = line[i];
        if (c === '"') {
          inq = !inq;
          continue;
        }
        if (!inq && c === ",") {
          out.push(cur.trim());
          cur = "";
          continue;
        }
        cur += c;
      }
      out.push(cur.trim());
      return out.filter((h) => h.length > 0);
    },
    async refreshImportColumnOptions(file) {
      this.params.geocodeColumnKeyword = "";
      this.selectOptions.geocodeColumnKeyword = [];
      const raw = file && (file.raw || file);
      if (!raw || !raw.name) {
        return;
      }
      const name = String(raw.name).toLowerCase();
      if (name.endsWith(".zip")) {
        this.$message.info("zip 包请在列名输入框中填写与各 xlsx 表头一致的列名");
        return;
      }
      try {
        if (name.endsWith(".csv")) {
          const text = await raw.text();
          const firstLine = (text.split(/\r?\n/).find((l) => l.trim().length > 0) || "").trim();
          if (!firstLine) {
            return;
          }
          const headers = this.parseCsvHeaderLine(firstLine);
          this.selectOptions.geocodeColumnKeyword = headers.map((h) => ({
            value: h,
            label: h,
          }));
          this.$message.success("已解析 CSV 表头，共 " + headers.length + " 列");
        } else if (name.endsWith(".xlsx") || name.endsWith(".xls")) {
          const buf = await raw.arrayBuffer();
          const wb = XLSX.read(buf, { type: "array" });
          const sheetName = wb.SheetNames[0];
          const sheet = wb.Sheets[sheetName];
          const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: "" });
          const headers = (rows[0] || []).map((c) => String(c).trim()).filter((c) => c.length > 0);
          this.selectOptions.geocodeColumnKeyword = headers.map((h) => ({
            value: h,
            label: h,
          }));
          this.$message.success("已解析 Excel 首行表头，共 " + headers.length + " 列");
        }
      } catch (e) {
        console.error(e);
        this.$message.warning("解析表头失败，可手动在「列名」中输入与表格一致的列名");
      }
    },
    handleChange(emit) {
      this.params.unit = "";
      this.params.keyType = "";
      this.params.geocodeColumnKeyword = "";
      this.selectOptions.geocodeColumnKeyword = [];
      this.fileList = [];
      this.currentFile = null;
      // 清除所有地图中的元素
      this.clearAll();
      this.jsonData = {};
      this.backData = {};
      this.renderStatus = {
        input: false,
        output: false,
      };
      this.renderedGeojsonCache = {
        input: null,
        output: null,
      };
      this.currentRenderedSource = "";
      this.propertyDialog.visible = false;
      this.propertyDialog.list = [];
      this.propertyDialog.source = "";
      this.propertyDialog.propertiesRef = null;
      this.propertyDialog.featureIndex = -1;
      this.SceneValue = emit[emit.length - 1];
    },
    // 初始化绘制处理器
    initDrawHandler() {
      // 创建绘制处理器
      if (!this.handler) {
        this.handler = new SkyScenery.ScreenSpaceEventHandler(viewer.canvas);
      }
      this.initFeaturePickHandler();
    },
    resetPickDragState() {
      this._pickDrag = null;
    },
    /** @param {boolean} exitEditMode 为 false 时仅清除选中与高亮，用于在「节点编辑」内切换要素 */
    clearMapEditSelection(exitEditMode = true) {
      this.removeVertexHandleEntities();
      const prevEntity = this.mapEditSelection && this.mapEditSelection.entity;
      this.mapEditSelection = null;
      this.hoveredVertexIndex = -1;
      this.hoveredHandleEntity = null;
      this.clearHoveredFeatureStyle();
      if (exitEditMode) {
        this.geometryEditMode = false;
        this.pickedFeatureRef = null;
      }
      this.hideHoverHint();
      this.restoreEntityHighlight(prevEntity);
      this.setScreenSpaceCameraForDrag(true);
      if (viewer) {
        viewer.scene.requestRender();
      }
    },
    removeVertexHandleEntities() {
      if (!viewer || !this.vertexHandleEntities.length) {
        this.vertexHandleEntities = [];
        return;
      }
      this.vertexHandleEntities.forEach((e) => {
        try {
          viewer.entities.remove(e);
        } catch (err) {
          /* ignore */
        }
      });
      this.vertexHandleEntities = [];
    },
    restoreEntityHighlight(entIn) {
      const ent = entIn || (this.mapEditSelection && this.mapEditSelection.entity);
      if (!ent || !ent.__editBackup || !viewer) {
        return;
      }
      const b = ent.__editBackup;
      if (ent.point && b.point) {
        ent.point.pixelSize = b.point.pixelSize;
        ent.point.color = b.point.color;
        ent.point.outlineColor = b.point.outlineColor;
        ent.point.outlineWidth = b.point.outlineWidth;
      }
      if (ent.polyline && b.polyline) {
        ent.polyline.width = b.polyline.width;
        ent.polyline.material = b.polyline.material;
      }
      if (ent.polygon && b.polygon) {
        if (b.polygon.material != null) {
          ent.polygon.material = b.polygon.material;
        }
        if (b.polygon.outlineWidth != null) {
          ent.polygon.outlineWidth = b.polygon.outlineWidth;
        }
        if (b.polygon.outlineColor != null) {
          ent.polygon.outlineColor = b.polygon.outlineColor;
        }
      }
      delete ent.__editBackup;
    },
    applyEntityHighlight(entity, geometryType) {
      if (!entity || !viewer) {
        return;
      }
      if (!entity.__editBackup) {
        entity.__editBackup = {};
        if (entity.point) {
          const p = entity.point;
          entity.__editBackup.point = {
            pixelSize: p.pixelSize,
            color: p.color,
            outlineColor: p.outlineColor,
            outlineWidth: p.outlineWidth,
          };
        }
        if (entity.polyline) {
          const pl = entity.polyline;
          entity.__editBackup.polyline = {
            width: pl.width,
            material: pl.material,
          };
        }
        if (entity.polygon) {
          const pg = entity.polygon;
          entity.__editBackup.polygon = {
            material: pg.material,
            outlineWidth: pg.outlineWidth,
            outlineColor: pg.outlineColor,
          };
        }
      }
      const C = SkyScenery.Color;
      if (geometryType === "Point" && entity.point) {
        entity.point.pixelSize = 16;
        entity.point.color = C.YELLOW.withAlpha(0.95);
        entity.point.outlineColor = C.DEEPSKYBLUE;
        entity.point.outlineWidth = 3;
      } else if (geometryType === "LineString" && entity.polyline) {
        entity.polyline.width = 6;
        entity.polyline.material = C.CYAN.withAlpha(0.95);
      } else if (geometryType === "Polygon" && entity.polygon) {
        entity.polygon.outlineWidth = 4;
        entity.polygon.outlineColor = C.CYAN;
        entity.polygon.material = C.LIME.withAlpha(0.45);
      }
    },
    getVertexHandleStyle(mode, handleType = "vertex") {
      const C = SkyScenery.Color;
      if (handleType === "midpoint") {
        if (mode === "pressed") {
          return {
            pixelSize: 12,
            color: C.ORANGE,
            outlineColor: C.WHITE,
            outlineWidth: 3,
          };
        }
        if (mode === "hover") {
          return {
            pixelSize: 10,
            color: C.YELLOW,
            outlineColor: C.DARKBLUE,
            outlineWidth: 2,
          };
        }
        return {
          pixelSize: 7,
          color: C.RED.withAlpha(0.9),
          outlineColor: C.WHITE,
          outlineWidth: 1,
        };
      }
      if (mode === "pressed") {
        return {
          pixelSize: 15,
          color: C.ORANGE,
          outlineColor: C.WHITE,
          outlineWidth: 3,
        };
      }
      if (mode === "hover") {
        return {
          pixelSize: 12,
          color: C.YELLOW,
          outlineColor: C.DARKBLUE,
          outlineWidth: 2,
        };
      }
      return {
        pixelSize: 9,
        color: C.WHITE,
        outlineColor: C.DEEPSKYBLUE,
        outlineWidth: 2,
      };
    },
    applyVertexHandleStyles() {
      if (!this.vertexHandleEntities.length) {
        return;
      }
      const dragging = this._pickDrag && this._pickDrag.dragging;
      const dragHandle = dragging ? this._pickDrag.dragHandleEntity || null : null;
      this.vertexHandleEntities.forEach((h) => {
        if (!h || !h.point) {
          return;
        }
        let mode = "default";
        if (dragging && dragHandle && h === dragHandle) {
          mode = "pressed";
        } else if (this.hoveredHandleEntity && h === this.hoveredHandleEntity) {
          mode = "hover";
        }
        const s = this.getVertexHandleStyle(mode, h.__handleType || "vertex");
        h.point.pixelSize = s.pixelSize;
        h.point.color = s.color;
        h.point.outlineColor = s.outlineColor;
        h.point.outlineWidth = s.outlineWidth;
      });
    },
    applyPointSelectionVisualByHoverPress() {
      const sel = this.mapEditSelection;
      if (!sel || sel.geometryType !== "Point" || !sel.entity || !sel.entity.point) {
        return;
      }
      const ent = sel.entity;
      const dragging = this._pickDrag && this._pickDrag.dragging && this._pickDrag.geometryType === "Point";
      const C = SkyScenery.Color;
      if (dragging) {
        ent.point.pixelSize = 18;
        ent.point.color = C.ORANGE.withAlpha(0.98);
        ent.point.outlineColor = C.WHITE;
        ent.point.outlineWidth = 4;
      } else if (this.hoveredVertexIndex === 0) {
        ent.point.pixelSize = 17;
        ent.point.color = C.LIME.withAlpha(0.95);
        ent.point.outlineColor = C.DEEPSKYBLUE;
        ent.point.outlineWidth = 3;
      } else {
        this.applyEntityHighlight(ent, "Point");
      }
    },
    refreshVertexHandlesFromPositions(positions) {
      if (!positions || !this.vertexHandleEntities.length) {
        return;
      }
      this.vertexHandleEntities.forEach((h) => {
        const i = h.__vertexIndex;
        if (h.__handleType === "vertex" && i >= 0 && i < positions.length) {
          h.position = SkyScenery.Cartesian3.clone(positions[i], new SkyScenery.Cartesian3());
        } else if (h.__handleType === "midpoint") {
          const a = h.__insertAfter;
          const b = a + 1;
          if (a >= 0 && b < positions.length) {
            h.position = SkyScenery.Cartesian3.midpoint(
              positions[a],
              positions[b],
              new SkyScenery.Cartesian3()
            );
          }
        }
      });
      if (viewer) {
        viewer.scene.requestRender();
      }
    },
    hideHoverHint() {
      this.hoverHint.visible = false;
      this.hoverHint.text = "";
    },
    clearHoveredFeatureStyle() {
      const ent = this.hoveredFeatureEntity;
      if (!ent || !ent.__hoverBackup) {
        this.hoveredFeatureEntity = null;
        return;
      }
      const b = ent.__hoverBackup;
      if (ent.point && b.point) {
        ent.point.pixelSize = b.point.pixelSize;
        ent.point.color = b.point.color;
        ent.point.outlineColor = b.point.outlineColor;
      }
      if (ent.polyline && b.polyline) {
        ent.polyline.width = b.polyline.width;
        ent.polyline.material = b.polyline.material;
      }
      if (ent.polygon && b.polygon) {
        ent.polygon.outlineWidth = b.polygon.outlineWidth;
        ent.polygon.outlineColor = b.polygon.outlineColor;
      }
      delete ent.__hoverBackup;
      this.hoveredFeatureEntity = null;
    },
    applyHoveredFeatureStyle(entity) {
      if (!entity || (this.mapEditSelection && this.mapEditSelection.entity === entity)) {
        this.clearHoveredFeatureStyle();
        return;
      }
      if (this.hoveredFeatureEntity === entity) {
        return;
      }
      this.clearHoveredFeatureStyle();
      entity.__hoverBackup = {};
      const C = SkyScenery.Color;
      if (entity.point) {
        entity.__hoverBackup.point = {
          pixelSize: entity.point.pixelSize,
          color: entity.point.color,
          outlineColor: entity.point.outlineColor,
        };
        entity.point.pixelSize = 13;
        entity.point.color = C.CYAN.withAlpha(0.95);
        entity.point.outlineColor = C.WHITE;
      } else if (entity.polyline) {
        entity.__hoverBackup.polyline = {
          width: entity.polyline.width,
          material: entity.polyline.material,
        };
        entity.polyline.width = 5;
        entity.polyline.material = C.CYAN.withAlpha(0.92);
      } else if (entity.polygon) {
        entity.__hoverBackup.polygon = {
          outlineWidth: entity.polygon.outlineWidth,
          outlineColor: entity.polygon.outlineColor,
        };
        entity.polygon.outlineWidth = 3;
        entity.polygon.outlineColor = C.CYAN;
      }
      this.hoveredFeatureEntity = entity;
    },
    findNearestHandleByScreen(screenPosition, maxDistPx = 18) {
      if (!this.vertexHandleEntities || !this.vertexHandleEntities.length) {
        return null;
      }
      let best = null;
      let bestD = maxDistPx;
      this.vertexHandleEntities.forEach((h) => {
        if (!h || !h.position) return;
        const p = h.position.getValue ? h.position.getValue(viewer.clock.currentTime) : h.position;
        const d = this.screenDistanceToCartesian(screenPosition, p);
        if (d < bestD) {
          bestD = d;
          best = h;
        }
      });
      return best;
    },
    showHoverHint(screenPosition, text) {
      if (!screenPosition || !text) {
        this.hideHoverHint();
        return;
      }
      this.hoverHint.visible = true;
      this.hoverHint.text = text;
      this.hoverHint.left = screenPosition.x + 14;
      this.hoverHint.top = screenPosition.y + 16;
    },
    createVertexHandlesForSelection(entity, geometryType, positionsArray) {
      this.removeVertexHandleEntities();
      if (!viewer || !entity || !positionsArray || !positionsArray.length) {
        return;
      }
      const featureRef = entity.__featureRef;
      const C = SkyScenery.Color;
      let indices = positionsArray.map((_, i) => i);
      const n = positionsArray.length;
      if (
        geometryType === "Polygon" &&
        n > 2 &&
        SkyScenery.Cartesian3.distance(positionsArray[0], positionsArray[n - 1]) < 0.5
      ) {
        indices = indices.slice(0, -1);
      }
      indices.forEach((vertexIndex) => {
        const pos = positionsArray[vertexIndex];
        const h = viewer.entities.add({
          position: SkyScenery.Cartesian3.clone(pos, new SkyScenery.Cartesian3()),
          point: {
            show: true,
            color: C.WHITE,
            pixelSize: 9,
            outlineColor: C.DEEPSKYBLUE,
            outlineWidth: 2,
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
          },
        });
        h.__vertexHandle = true;
        h.__handleType = "vertex";
        h.__vertexIndex = vertexIndex;
        h.__parentEntity = entity;
        h.__featureRef = featureRef;
        this.vertexHandleEntities.push(h);
      });
      // 在相邻顶点中间增加可插入顶点的中点柄
      const segCount = geometryType === "Polygon" ? indices.length : Math.max(0, indices.length - 1);
      for (let i = 0; i < segCount; i++) {
        const aIdx = indices[i];
        const bIdx = geometryType === "Polygon" ? indices[(i + 1) % indices.length] : indices[i + 1];
        if (aIdx == null || bIdx == null) continue;
        const a = positionsArray[aIdx];
        const b = positionsArray[bIdx];
        if (!a || !b) continue;
        const mid = SkyScenery.Cartesian3.midpoint(a, b, new SkyScenery.Cartesian3());
        const m = viewer.entities.add({
          position: mid,
          point: {
            show: true,
            color: C.RED.withAlpha(0.9),
            pixelSize: 7,
            outlineColor: C.WHITE,
            outlineWidth: 1,
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
          },
        });
        m.__vertexHandle = true;
        m.__handleType = "midpoint";
        m.__insertAfter = aIdx;
        m.__vertexIndex = -1;
        m.__parentEntity = entity;
        m.__featureRef = featureRef;
        this.vertexHandleEntities.push(m);
      }
      this.applyVertexHandleStyles();
    },
    applyMapEditSelection(entity) {
      if (!this.geometryEditMode) {
        return;
      }
      if (!entity || !viewer || !entity.__featureRef) {
        return;
      }
      const g =
        this.getSourceGeojsonData(entity.__featureRef.source || "input") ||
        this.renderedGeojsonCache[entity.__featureRef.source];
      const fi = entity.__featureRef.featureIndex;
      if (!g || !g.features || fi < 0 || !g.features[fi] || !g.features[fi].geometry) {
        return;
      }
      const geom = g.features[fi].geometry;
      const gt = geom.type === "MultiPolygon" ? null : geom.type;
      if (!gt || gt === "MultiPolygon") {
        return;
      }
      if (this.mapEditSelection && this.mapEditSelection.entity === entity) {
        const now = viewer.clock.currentTime;
        const gType = this.mapEditSelection.geometryType;
        this.removeVertexHandleEntities();
        if (gType === "LineString" && entity.polyline && entity.polyline.positions) {
          const arr = entity.polyline.positions.getValue(now);
          if (arr && arr.length) {
            this.createVertexHandlesForSelection(entity, "LineString", arr);
          }
        } else if (gType === "Polygon" && entity.polygon && entity.polygon.hierarchy) {
          const hProp = entity.polygon.hierarchy;
          const hier =
            hProp && typeof hProp.getValue === "function" ? hProp.getValue(now) : hProp;
          const arr = hier && hier.positions;
          if (arr && arr.length) {
            this.createVertexHandlesForSelection(entity, "Polygon", arr);
          }
        }
        this.applyVertexHandleStyles();
        this.applyPointSelectionVisualByHoverPress();
        return;
      }
      this.clearMapEditSelection(false);
      this.mapEditSelection = {
        entity,
        geometryType: gt,
        source: entity.__featureRef.source || "input",
        featureIndex: fi,
      };
      this.applyEntityHighlight(entity, gt === "LineString" ? "LineString" : gt === "Polygon" ? "Polygon" : "Point");
      const now = viewer.clock.currentTime;
      if (gt === "LineString" && entity.polyline && entity.polyline.positions) {
        const arr = entity.polyline.positions.getValue(now);
        if (arr && arr.length) {
          this.createVertexHandlesForSelection(entity, "LineString", arr);
        }
      } else if (gt === "Polygon" && entity.polygon && entity.polygon.hierarchy) {
        const hProp = entity.polygon.hierarchy;
        const hier = hProp && typeof hProp.getValue === "function" ? hProp.getValue(now) : hProp;
        const arr = hier && hier.positions;
        if (arr && arr.length) {
          this.createVertexHandlesForSelection(entity, "Polygon", arr);
        }
      }
      this.hoveredVertexIndex = -1;
      this.applyVertexHandleStyles();
      this.applyPointSelectionVisualByHoverPress();
      viewer.scene.requestRender();
    },
    updateVertexHoverFromPick(screenPosition, precomputedNearHandle = null) {
      if (!this.mapEditSelection || !this.mapEditSelection.entity) {
        this.hoveredVertexIndex = -1;
        this.hoveredHandleEntity = null;
        this.hideHoverHint();
        return;
      }
      if (this._pickDrag && this._pickDrag.dragging) {
        this.hoveredVertexIndex = this._pickDrag.vertexIndex;
        this.hoveredHandleEntity = this._pickDrag.dragHandleEntity || null;
        this.applyVertexHandleStyles();
        this.applyPointSelectionVisualByHoverPress();
        this.showHoverHint(screenPosition, "可拖拽调整节点位置");
        return;
      }
      // 与按下逻辑一致：优先按屏幕距离命中顶点/中点柄。纯 pick 常会先拾取到面/线填充而非小点实体。
      let e =
        precomputedNearHandle ||
        (this.vertexHandleEntities && this.vertexHandleEntities.length
          ? this.findNearestHandleByScreen(screenPosition, 32)
          : null);
      if (!e) {
        const pickedObject = viewer.scene.pick(screenPosition);
        e = pickedObject && pickedObject.id && pickedObject.id.__vertexHandle ? pickedObject.id : null;
      }
      if (e && e.__vertexHandle) {
        this.hoveredHandleEntity = e;
        this.hoveredVertexIndex = typeof e.__vertexIndex === "number" ? e.__vertexIndex : -1;
        if (e.__handleType === "midpoint") {
          this.showHoverHint(screenPosition, "拖动可新增节点");
        } else {
          this.showHoverHint(screenPosition, "可拖拽调整节点位置");
        }
      } else if (this.mapEditSelection.geometryType === "Point") {
        const pickedPoint = viewer.scene.pick(screenPosition);
        const pe = pickedPoint && pickedPoint.id;
        if (pe === this.mapEditSelection.entity) {
          this.hoveredVertexIndex = 0;
          this.hoveredHandleEntity = null;
          this.showHoverHint(screenPosition, "可拖拽调整节点位置");
        } else {
          this.hoveredVertexIndex = -1;
          this.hoveredHandleEntity = null;
          this.hideHoverHint();
        }
      } else {
        this.hoveredVertexIndex = -1;
        this.hoveredHandleEntity = null;
        this.hideHoverHint();
      }
      this.applyVertexHandleStyles();
      this.applyPointSelectionVisualByHoverPress();
      if (viewer) {
        viewer.scene.requestRender();
      }
    },
    setScreenSpaceCameraForDrag(enable) {
      if (!viewer || !viewer.scene || !viewer.scene.screenSpaceCameraController) {
        return;
      }
      const c = viewer.scene.screenSpaceCameraController;
      c.enableInputs = enable;
      c.enableRotate = enable;
      c.enableTranslate = enable;
      c.enableZoom = enable;
      c.enableTilt = enable;
      c.enableLook = enable;
    },
    /** 拖动顶点/线/面或折线面绘制时启用连续渲染，结束后恢复 Example 中的 requestRenderMode */
    enterInteractiveSceneRender() {
      if (!viewer || !viewer.scene) {
        return;
      }
      if (this._interactiveRenderDepth === 0) {
        this._savedSceneRenderMode = {
          requestRenderMode: viewer.scene.requestRenderMode,
          maximumRenderTimeChange: viewer.scene.maximumRenderTimeChange,
        };
        viewer.scene.requestRenderMode = false;
        viewer.scene.maximumRenderTimeChange = 0;
      }
      this._interactiveRenderDepth++;
    },
    exitInteractiveSceneRender() {
      if (!viewer || !viewer.scene || this._interactiveRenderDepth <= 0) {
        return;
      }
      this._interactiveRenderDepth--;
      if (this._interactiveRenderDepth === 0 && this._savedSceneRenderMode) {
        viewer.scene.requestRenderMode = this._savedSceneRenderMode.requestRenderMode;
        viewer.scene.maximumRenderTimeChange = this._savedSceneRenderMode.maximumRenderTimeChange;
        this._savedSceneRenderMode = null;
      }
    },
    /** globe.pick 在快速移动时易失败；补充 pickPosition / 椭球拾取，保证几何与柄同步 */
    pickSurfaceCartesian(screenPosition) {
      if (!viewer || !viewer.scene || !viewer.camera || !screenPosition) {
        return null;
      }
      const ray = viewer.camera.getPickRay(screenPosition);
      if (!ray) {
        return null;
      }
      let cart = viewer.scene.globe.pick(ray, viewer.scene);
      if (cart) {
        return cart;
      }
      if (typeof viewer.scene.pickPosition === "function") {
        cart = viewer.scene.pickPosition(screenPosition);
        if (cart) {
          return cart;
        }
      }
      const ellipsoid = viewer.scene.globe && viewer.scene.globe.ellipsoid;
      if (ellipsoid && typeof viewer.camera.pickEllipsoid === "function") {
        return viewer.camera.pickEllipsoid(screenPosition, ellipsoid);
      }
      return null;
    },
    wgs84PointToScreen(cartesian) {
      if (!viewer || !cartesian) {
        return null;
      }
      const ST = SkyScenery.SceneTransforms;
      if (!ST || typeof ST.wgs84ToWindowCoordinates !== "function") {
        return null;
      }
      const win = new SkyScenery.Cartesian2();
      if (!ST.wgs84ToWindowCoordinates(viewer.scene, cartesian, win)) {
        return null;
      }
      return win;
    },
    screenDistanceToCartesian(screenPos, cartesian) {
      const win = this.wgs84PointToScreen(cartesian);
      if (!win) {
        return Infinity;
      }
      const dx = win.x - screenPos.x;
      const dy = win.y - screenPos.y;
      return Math.sqrt(dx * dx + dy * dy);
    },
    findClosestVertexIndexScreen(positions, screenPos, maxDistPx) {
      if (!positions || !positions.length) {
        return -1;
      }
      let best = -1;
      let bestD = maxDistPx;
      for (let i = 0; i < positions.length; i++) {
        const d = this.screenDistanceToCartesian(screenPos, positions[i]);
        if (d < bestD) {
          bestD = d;
          best = i;
        }
      }
      return best;
    },
    cloneCartesian3Array(arr) {
      return arr.map((p) => SkyScenery.Cartesian3.clone(p, new SkyScenery.Cartesian3()));
    },
    openFeaturePropertyDialogForEntity(entity) {
      const featureIndex =
        entity.__featureRef && typeof entity.__featureRef.featureIndex === "number"
          ? entity.__featureRef.featureIndex
          : -1;
      const propertySource =
        (entity.__featureRef && entity.__featureRef.source) || this.currentRenderedSource || "input";
      const geojsonData =
        this.getSourceGeojsonData(propertySource) || this.renderedGeojsonCache[propertySource];
      let propertiesRef = null;
      if (geojsonData) {
        if (
          entity.__featureRef &&
          typeof entity.__featureRef.featureIndex === "number" &&
          entity.__featureRef.featureIndex >= 0 &&
          geojsonData.features &&
          geojsonData.features[entity.__featureRef.featureIndex]
        ) {
          if (!geojsonData.features[entity.__featureRef.featureIndex].properties) {
            geojsonData.features[entity.__featureRef.featureIndex].properties = {};
          }
          propertiesRef = geojsonData.features[entity.__featureRef.featureIndex].properties;
        } else {
          if (!geojsonData.properties) {
            geojsonData.properties = {};
          }
          propertiesRef = geojsonData.properties;
        }
      }
      const propertyList = this.formatFeatureProperties(
        propertiesRef || entity.__featureProperties || {}
      );
      if (
        propertiesRef ||
        (entity.__featureProperties && typeof entity.__featureProperties === "object")
      ) {
        this.pickedFeatureRef = {
          entity,
          source: propertySource,
          featureIndex,
        };
        this.propertyDialog.source = propertySource;
        this.propertyDialog.propertiesRef = propertiesRef || entity.__featureProperties || {};
        this.propertyDialog.featureIndex = featureIndex;
        this.propertyDialog.list = propertyList;
        this.propertyDialog.visible = true;
      } else {
        this.pickedFeatureRef = {
          entity,
          source: propertySource,
          featureIndex,
        };
        this.propertyDialog.visible = false;
        this.propertyDialog.list = [];
        this.propertyDialog.source = "";
        this.propertyDialog.propertiesRef = null;
        this.propertyDialog.featureIndex = -1;
      }
    },
    commitDraggedGeometryToGeojson(st) {
      if (!st || !st.entity || st.featureIndex < 0) {
        return;
      }
      const g = this.getSourceGeojsonData(st.source);
      if (!g || !g.features || !g.features[st.featureIndex] || !g.features[st.featureIndex].geometry) {
        return;
      }
      const geom = g.features[st.featureIndex].geometry;
      const now = viewer.clock.currentTime;
      const toLonLat = (c3, origCoord) => {
        const c = SkyScenery.Cartographic.fromCartesian(c3);
        const lon = SkyScenery.Math.toDegrees(c.longitude);
        const lat = SkyScenery.Math.toDegrees(c.latitude);
        if (origCoord && origCoord.length > 2 && typeof origCoord[2] === "number") {
          return [lon, lat, origCoord[2]];
        }
        return [lon, lat];
      };
      if (geom.type === "Point" && st.entity.position) {
        const p = st.entity.position.getValue(now);
        if (!p) {
          return;
        }
        const orig = geom.coordinates;
        geom.coordinates = toLonLat(p, orig);
        return;
      }
      if (geom.type === "LineString" && st.workingPositions) {
        geom.coordinates = st.workingPositions.map((c3, i) => toLonLat(c3, geom.coordinates[i]));
        return;
      }
      if (geom.type === "Polygon" && st.workingPositions) {
        const ring = st.workingPositions.map((c3, i) => {
          const origRing = (geom.coordinates && geom.coordinates[0]) || [];
          return toLonLat(c3, origRing[i]);
        });
        if (ring.length > 1) {
          const a = ring[0];
          const b = ring[ring.length - 1];
          if (a[0] !== b[0] || a[1] !== b[1] || (a[2] !== undefined && b[2] !== undefined && a[2] !== b[2])) {
            ring.push([...a]);
          }
        }
        if (!geom.coordinates) {
          geom.coordinates = [];
        }
        geom.coordinates[0] = ring;
      }
    },
    syncPolygonEntityHierarchy(entity, positions) {
      if (!entity || !entity.polygon) {
        return;
      }
      const n = positions.length;
      if (n > 1) {
        const a = positions[0];
        const b = positions[n - 1];
        if (SkyScenery.Cartesian3.distance(a, b) < 0.5) {
          positions[n - 1] = SkyScenery.Cartesian3.clone(a, new SkyScenery.Cartesian3());
        }
      }
      entity.polygon.hierarchy = new SkyScenery.PolygonHierarchy(this.cloneCartesian3Array(positions));
    },
    tryBeginPickDrag(event) {
      if (this.currentTool || this.isDrawingHole) {
        return;
      }
      const nearHandle = this.geometryEditMode ? this.findNearestHandleByScreen(event.position, 32) : null;
      const pickedObject = nearHandle ? { id: nearHandle } : viewer.scene.pick(event.position);
      if (!SkyScenery.defined(pickedObject) || !SkyScenery.defined(pickedObject.id)) {
        this.propertyDialog.visible = false;
        this.propertyDialog.list = [];
        this.propertyDialog.source = "";
        this.propertyDialog.propertiesRef = null;
        this.propertyDialog.featureIndex = -1;
        this.resetPickDragState();
        this.clearMapEditSelection();
        return;
      }
      const entity = pickedObject.id;
      if (!this.geometryEditMode) {
        if (!entity.__featureRef || typeof entity.__featureRef.featureIndex !== "number") {
          this.resetPickDragState();
          return;
        }
        this._pickDrag = {
          entity,
          source: entity.__featureRef.source || this.currentRenderedSource || "input",
          featureIndex: entity.__featureRef.featureIndex,
          selectOnly: true,
          noDrag: true,
          startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
          dragging: false,
        };
        return;
      }
      if (
        (entity && entity.__vertexHandle && entity.__parentEntity) ||
        (nearHandle && nearHandle.__vertexHandle && nearHandle.__parentEntity)
      ) {
        const handleEntity = nearHandle || entity;
        const parent = handleEntity.__parentEntity;
        if (!parent.__featureRef || typeof parent.__featureRef.featureIndex !== "number") {
          this.resetPickDragState();
          return;
        }
        const featureIndex = parent.__featureRef.featureIndex;
        const source = parent.__featureRef.source || this.currentRenderedSource || "input";
        const g = this.getSourceGeojsonData(source) || this.renderedGeojsonCache[source];
        if (!g || !g.features || featureIndex < 0 || !g.features[featureIndex]) {
          this.resetPickDragState();
          return;
        }
        const geom = g.features[featureIndex].geometry;
        if (!geom || geom.type === "MultiPolygon") {
          this.resetPickDragState();
          return;
        }
        // 避免每次点柄都 remove/recreate 顶点实体，防止与当前按下目标不同步、pick/hover 失效
        if (!this.mapEditSelection || this.mapEditSelection.entity !== parent) {
          this.applyMapEditSelection(parent);
        }
        const now = viewer.clock.currentTime;
        const vi = handleEntity.__vertexIndex;
        if (geom.type === "LineString" && parent.polyline && parent.polyline.positions) {
          const positions = parent.polyline.positions.getValue(now);
          if (!positions || !positions.length) {
            this.resetPickDragState();
            return;
          }
          let working = this.cloneCartesian3Array(positions);
          let dragIndex = vi;
          if (handleEntity.__handleType === "midpoint") {
            const after = handleEntity.__insertAfter;
            const insertAt = after + 1;
            const midPos =
              handleEntity.position && handleEntity.position.getValue
                ? handleEntity.position.getValue(now)
                : null;
            if (after < 0 || insertAt > working.length || !midPos) {
              this.resetPickDragState();
              return;
            }
            working.splice(insertAt, 0, SkyScenery.Cartesian3.clone(midPos, new SkyScenery.Cartesian3()));
            parent.polyline.positions = this.cloneCartesian3Array(working);
            this.refreshVertexHandlesFromPositions(working);
            dragIndex = insertAt;
          }
          if (dragIndex < 0 || dragIndex >= working.length) {
            this.resetPickDragState();
            return;
          }
          this._pickDrag = {
            entity: parent,
            source,
            featureIndex,
            geometryType: "LineString",
            vertexIndex: dragIndex,
            dragHandleEntity: handleEntity,
            workingPositions: working,
            startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
            cameraLocked: true,
            dragging: false,
          };
          this.setScreenSpaceCameraForDrag(false);
          this.applyVertexHandleStyles();
          this.applyPointSelectionVisualByHoverPress();
          return;
        }
        if (geom.type === "Polygon" && parent.polygon && parent.polygon.hierarchy) {
          const hProp = parent.polygon.hierarchy;
          const hier =
            hProp && typeof hProp.getValue === "function" ? hProp.getValue(now) : hProp;
          const positions = hier && hier.positions;
          if (!positions || !positions.length) {
            this.resetPickDragState();
            return;
          }
          let working = this.cloneCartesian3Array(positions);
          let dragIndex = vi;
          if (handleEntity.__handleType === "midpoint") {
            const after = handleEntity.__insertAfter;
            const insertAt = after + 1;
            const midPos =
              handleEntity.position && handleEntity.position.getValue
                ? handleEntity.position.getValue(now)
                : null;
            // insertAt === length 表示在末尾追加（闭合边上 v_last→v0 之间插点）；用 >= 会误拒开环多边形
            if (after < 0 || insertAt > working.length || !midPos) {
              this.resetPickDragState();
              return;
            }
            working.splice(insertAt, 0, SkyScenery.Cartesian3.clone(midPos, new SkyScenery.Cartesian3()));
            this.syncPolygonEntityHierarchy(parent, working);
            this.refreshVertexHandlesFromPositions(working);
            dragIndex = insertAt;
          }
          if (dragIndex < 0 || dragIndex >= working.length) {
            this.resetPickDragState();
            return;
          }
          this._pickDrag = {
            entity: parent,
            source,
            featureIndex,
            geometryType: "Polygon",
            vertexIndex: dragIndex,
            dragHandleEntity: handleEntity,
            workingPositions: working,
            startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
            cameraLocked: true,
            dragging: false,
          };
          this.setScreenSpaceCameraForDrag(false);
          this.applyVertexHandleStyles();
          this.applyPointSelectionVisualByHoverPress();
          return;
        }
        this.resetPickDragState();
        return;
      }
      if (!entity.__featureRef || typeof entity.__featureRef.featureIndex !== "number") {
        this.resetPickDragState();
        return;
      }
      const featureIndex = entity.__featureRef.featureIndex;
      const source = entity.__featureRef.source || this.currentRenderedSource || "input";
      const g = this.getSourceGeojsonData(source) || this.renderedGeojsonCache[source];
      if (!g || !g.features || featureIndex < 0 || !g.features[featureIndex]) {
        this.resetPickDragState();
        return;
      }
      const geom = g.features[featureIndex].geometry;
      if (!geom || geom.type === "MultiPolygon") {
        this.resetPickDragState();
        return;
      }
      const now = viewer.clock.currentTime;
      const maxPx = 28;
      if (entity.point && entity.position) {
        const pos = entity.position.getValue(now);
        if (!pos) {
          this.resetPickDragState();
          return;
        }
        if (this.screenDistanceToCartesian(event.position, pos) > maxPx) {
          this.resetPickDragState();
          return;
        }
        this.applyMapEditSelection(entity);
        this._pickDrag = {
          entity,
          source,
          featureIndex,
          geometryType: "Point",
          vertexIndex: 0,
          dragHandleEntity: null,
          startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
          cameraLocked: true,
          dragging: false,
        };
        this.setScreenSpaceCameraForDrag(false);
        this.applyPointSelectionVisualByHoverPress();
        return;
      }
      if (entity.polyline && entity.polyline.positions) {
        const positions = entity.polyline.positions.getValue(now);
        if (!positions || !positions.length) {
          this.resetPickDragState();
          return;
        }
        const vi = this.findClosestVertexIndexScreen(positions, event.position, maxPx);
        if (vi < 0) {
          // 点在折线段上但未贴近任一顶点：仍选中并高亮，便于查看属性
          this.applyMapEditSelection(entity);
          this._pickDrag = {
            entity,
            source,
            featureIndex,
            geometryType: "LineString",
            vertexIndex: -1,
            selectOnly: true,
            startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
            dragging: false,
          };
          return;
        }
        this.applyMapEditSelection(entity);
        this._pickDrag = {
          entity,
          source,
          featureIndex,
          geometryType: "LineString",
          vertexIndex: vi,
          dragHandleEntity: this.vertexHandleEntities.find(
            (h) => h.__handleType === "vertex" && h.__vertexIndex === vi
          ),
          workingPositions: this.cloneCartesian3Array(positions),
          startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
          cameraLocked: true,
          dragging: false,
        };
        this.setScreenSpaceCameraForDrag(false);
        this.applyVertexHandleStyles();
        return;
      }
      if (entity.polygon && entity.polygon.hierarchy) {
        const hProp = entity.polygon.hierarchy;
        const hier =
          hProp && typeof hProp.getValue === "function" ? hProp.getValue(now) : hProp;
        const positions = hier && hier.positions;
        if (!positions || !positions.length) {
          this.resetPickDragState();
          return;
        }
        const vi = this.findClosestVertexIndexScreen(positions, event.position, maxPx);
        if (vi < 0) {
          // 点在面填充/边线上但未贴近顶点：仍选中高亮并显示全部顶点柄
          this.applyMapEditSelection(entity);
          this._pickDrag = {
            entity,
            source,
            featureIndex,
            geometryType: "Polygon",
            vertexIndex: -1,
            selectOnly: true,
            startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
            dragging: false,
          };
          return;
        }
        this.applyMapEditSelection(entity);
        this._pickDrag = {
          entity,
          source,
          featureIndex,
          geometryType: "Polygon",
          vertexIndex: vi,
          dragHandleEntity: this.vertexHandleEntities.find(
            (h) => h.__handleType === "vertex" && h.__vertexIndex === vi
          ),
          workingPositions: this.cloneCartesian3Array(positions),
          startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
          cameraLocked: true,
          dragging: false,
        };
        this.setScreenSpaceCameraForDrag(false);
        this.applyVertexHandleStyles();
        return;
      }
      this.resetPickDragState();
    },
    onPickDragMove(event) {
      if (!this.geometryEditMode) {
        const po = viewer.scene.pick(event.endPosition);
        const ent = po && po.id && po.id.__featureRef ? po.id : null;
        if (ent) {
          this.applyHoveredFeatureStyle(ent);
        } else {
          this.clearHoveredFeatureStyle();
        }
        this.hideHoverHint();
        return;
      }
      this.clearHoveredFeatureStyle();
      const st = this._pickDrag;
      const hoverNearHandle =
        this.vertexHandleEntities && this.vertexHandleEntities.length
          ? this.findNearestHandleByScreen(event.endPosition, 32)
          : null;
      if (st && st.entity && !st.selectOnly) {
        const dx = event.endPosition.x - st.startScreen.x;
        const dy = event.endPosition.y - st.startScreen.y;
        const movedPx = Math.sqrt(dx * dx + dy * dy);
        if (!st.dragging && movedPx > 1) {
          st.dragging = true;
          this.setScreenSpaceCameraForDrag(false);
          this.enterInteractiveSceneRender();
        }
        if (st.dragging) {
          const cart = this.pickSurfaceCartesian(event.endPosition);
          if (cart) {
            if (st.geometryType === "Point" && st.entity.position) {
              st.entity.position = cart;
            } else if (st.geometryType === "LineString" && st.workingPositions) {
              const prev = st.workingPositions[st.vertexIndex];
              const cPrev = SkyScenery.Cartographic.fromCartesian(prev);
              const cNew = SkyScenery.Cartographic.fromCartesian(cart);
              cNew.height = cPrev.height;
              st.workingPositions[st.vertexIndex] = SkyScenery.Cartesian3.fromRadians(
                cNew.longitude,
                cNew.latitude,
                cNew.height
              );
              st.entity.polyline.positions = st.workingPositions;
              this.refreshVertexHandlesFromPositions(st.workingPositions);
            } else if (st.geometryType === "Polygon" && st.workingPositions) {
              const prev = st.workingPositions[st.vertexIndex];
              const cPrev = SkyScenery.Cartographic.fromCartesian(prev);
              const cNew = SkyScenery.Cartographic.fromCartesian(cart);
              cNew.height = cPrev.height;
              st.workingPositions[st.vertexIndex] = SkyScenery.Cartesian3.fromRadians(
                cNew.longitude,
                cNew.latitude,
                cNew.height
              );
              const n = st.workingPositions.length;
              // 仅当 positions 已带「首尾重合」闭合点时才同步；开环多边形首尾是不同角点，不能互拷否则会吞掉顶点
              const ringClosedDuplicate =
                n > 2 &&
                SkyScenery.Cartesian3.distance(st.workingPositions[0], st.workingPositions[n - 1]) <
                  0.5;
              if (ringClosedDuplicate) {
                if (st.vertexIndex === 0) {
                  st.workingPositions[n - 1] = SkyScenery.Cartesian3.clone(
                    st.workingPositions[0],
                    st.workingPositions[n - 1]
                  );
                } else if (st.vertexIndex === n - 1) {
                  st.workingPositions[0] = SkyScenery.Cartesian3.clone(
                    st.workingPositions[n - 1],
                    st.workingPositions[0]
                  );
                }
              }
              this.syncPolygonEntityHierarchy(st.entity, st.workingPositions);
              this.refreshVertexHandlesFromPositions(st.workingPositions);
            }
            viewer.scene.requestRender();
          }
        }
      }
      // 相机默认先于自定义 handler 响应拖拽；在靠近顶点/中点或正在拖动节点时关闭相机输入，避免“拖地图”抢事件
      const vertexInteraction =
        !!(st && st.dragging) ||
        !!(st && st.cameraLocked && st.entity && !st.selectOnly) ||
        !!hoverNearHandle;
      this.setScreenSpaceCameraForDrag(!vertexInteraction);
      if (!(st && st.dragging)) {
        this.updateVertexHoverFromPick(event.endPosition, hoverNearHandle);
      } else {
        this.applyVertexHandleStyles();
        this.applyPointSelectionVisualByHoverPress();
      }
    },
    onPickDragUp(event) {
      const st = this._pickDrag;
      if (!st || !st.entity) {
        this.resetPickDragState();
        return;
      }
      const wasDraggingGeometry = st.dragging;
      const dx = event.position.x - st.startScreen.x;
      const dy = event.position.y - st.startScreen.y;
      const movedPx = Math.sqrt(dx * dx + dy * dy);
      if (st.dragging) {
        this.commitDraggedGeometryToGeojson(st);
        this.syncJsonEditorData(st.source);
        this.applyMapEditSelection(st.entity);
      } else if (movedPx <= 5) {
        this.openFeaturePropertyDialogForEntity(st.entity);
      }
      if (st.cameraLocked) {
        this.setScreenSpaceCameraForDrag(true);
      }
      if (wasDraggingGeometry) {
        this.exitInteractiveSceneRender();
      }
      this.resetPickDragState();
    },
    // 初始化地图要素拾取：单击打开属性；按住拖动顶点/点实时改坐标并同步 JSON
    initFeaturePickHandler() {
      if (this.featurePickHandler || !viewer || !viewer.canvas) {
        return;
      }
      const h = new SkyScenery.ScreenSpaceEventHandler(viewer.canvas);
      const SS = SkyScenery.ScreenSpaceEventType;
      h.setInputAction((e) => this.tryBeginPickDrag(e), SS.LEFT_DOWN);
      h.setInputAction((e) => this.onPickDragMove(e), SS.MOUSE_MOVE);
      h.setInputAction((e) => this.onPickDragUp(e), SS.LEFT_UP);
      this.featurePickHandler = h;
    },

    // 激活绘制工具
    activateDraw(type) {
      // 进入绘制工具时退出“更改绘制”模式，避免交互冲突
      if (this.geometryEditMode) {
        this.clearMapEditSelection();
      }
      // 取消镂空绘制模式
      this.isDrawingHole = false;
      // 如果已经是当前激活的工具，则取消激活
      if (this.currentTool === type) {
        this.deactivateDraw();
        return;
      }
      // 先取消之前的绘制模式
      this.deactivateDraw();
      // 设置当前工具
      this.currentTool = type;
      this.drawingMode = type;
      // 从当前渲染/编辑器数据同步几何，避免新绘制覆盖已有要素
      this.syncGeometriesFromRenderedSource();
      // 重置当前绘制状态
      this.resetDrawingState();
      switch (type) {
        case "point":
          this.drawPoint();
          break;
        case "polyline":
          this.drawPolyline();
          break;
        case "polygon":
          this.drawPolygon();
          break;
      }
      if (type === "polyline" || type === "polygon") {
        this.enterInteractiveSceneRender();
      }
    },

    // 重置绘制状态
    resetDrawingState() {
      this.currentPositions = [];
      this.currentEntity = null;
      // 移除临时预览实体
      if (this.tempEntity) {
        viewer.entities.remove(this.tempEntity);
        viewer.scene.requestRender();
        this.tempEntity = null;
      }
    },

    // 取消绘制模式
    deactivateDraw() {
      this.exitInteractiveSceneRender();
      if (this.handler) {
        // 移除所有事件监听器
        this.handler.removeInputAction(SkyScenery.ScreenSpaceEventType.LEFT_CLICK);
        this.handler.removeInputAction(SkyScenery.ScreenSpaceEventType.MOUSE_MOVE);
        this.handler.removeInputAction(SkyScenery.ScreenSpaceEventType.RIGHT_CLICK);
      }
      // 重置状态
      this.resetDrawingState();
      this.currentTool = null;
      this.drawingMode = null;
      this.isDrawingHole = false;
    },

    // 绘制点
    drawPoint() {
      const that = this;
      this.handler.setInputAction(function (event) {
        const position = that.pickSurfaceCartesian(event.position);
        if (position) {
          const cartographic = SkyScenery.Cartographic.fromCartesian(position);
          const longitude = SkyScenery.Math.toDegrees(cartographic.longitude);
          const latitude = SkyScenery.Math.toDegrees(cartographic.latitude);
          // 创建点实体（实时渲染）
          const entity = viewer.entities.add({
            position: position,
            point: {
              show: true,
              color: SkyScenery.Color.RED,
              pixelSize: 10,
              outlineColor: SkyScenery.Color.WHITE,
              outlineWidth: 2,
            },
          });
          viewer.scene.requestRender();
          that.drawnEntities.push(entity);
          // 转换为geometry格式并保存
          const geometry = {
            type: "Point",
            coordinates: [longitude, latitude],
          };
          that.geometries.push(geometry);
          that.changeGeometries();
          const source = "input";
          const featureIndex = that.geometries.length - 1;
          entity.__featureRef = { source, featureIndex };
          entity.__featureProperties = {};
          console.log("绘制了点:", geometry);
        }
      }, SkyScenery.ScreenSpaceEventType.LEFT_CLICK);
    },

    // 绘制线
    drawPolyline() {
      const that = this;
      // 鼠标移动时更新临时线（实时渲染预览）
      this.handler.setInputAction(function (event) {
        if (that.currentPositions.length > 0) {
          const endPosition = that.pickSurfaceCartesian(event.endPosition);

          if (endPosition) {
            const last = that.currentPositions[that.currentPositions.length - 1];
            const pendingSeg = [last, endPosition];
            // 待落点使用虚线预览
            if (!that.tempEntity) {
              that.tempEntity = viewer.entities.add({
                polyline: {
                  show: true,
                  positions: pendingSeg,
                  material: new SkyScenery.PolylineDashMaterialProperty({
                    color: SkyScenery.Color.RED.withAlpha(0.9),
                    dashLength: 10,
                  }),
                  width: 3,
                  clampToGround: true,
                },
              });
              viewer.scene.requestRender();
            } else {
              that.tempEntity.polyline.positions = pendingSeg;
              viewer.scene.requestRender();
            }
          }
        }
      }, SkyScenery.ScreenSpaceEventType.MOUSE_MOVE);

      // 左键点击添加点
      this.handler.setInputAction(function (event) {
        const position = that.pickSurfaceCartesian(event.position);

        if (position) {
          that.currentPositions.push(position.clone());

          // 已落点使用实线
          if (that.currentPositions.length > 1) {
            if (!that.currentEntity) {
              that.currentEntity = viewer.entities.add({
                polyline: {
                  show: true,
                  positions: [...that.currentPositions],
                  material: SkyScenery.Color.RED,
                  width: 3,
                  clampToGround: true,
                },
              });
            } else {
              that.currentEntity.polyline.positions = [...that.currentPositions];
            }
            viewer.scene.requestRender();
          }
        }
      }, SkyScenery.ScreenSpaceEventType.LEFT_CLICK);

      // 右键点击完成绘制
      this.handler.setInputAction(function () {
        if (that.currentPositions.length > 1) {
          // 移除临时预览实体
          if (that.tempEntity) {
            viewer.entities.remove(that.tempEntity);
            that.tempEntity = null;
          }

          // 否则创建线
          // 确保当前实体存在
          if (!that.currentEntity) {
            that.currentEntity = viewer.entities.add({
              polyline: {
                show: true,
                positions: [...that.currentPositions],
                material: SkyScenery.Color.RED,
                width: 3,
                  clampToGround: true,
              },
            });
          }

          that.drawnEntities.push(that.currentEntity);
          viewer.scene.requestRender();

          // 转换为geometry格式并保存
          const coordinates = [];
          that.currentPositions.forEach((pos) => {
            const cartographic = SkyScenery.Cartographic.fromCartesian(pos);
            coordinates.push([
              SkyScenery.Math.toDegrees(cartographic.longitude),
              SkyScenery.Math.toDegrees(cartographic.latitude),
            ]);
          });

          const geometry = {
            type: "LineString",
            coordinates: coordinates,
          };
          that.geometries.push(geometry);
          that.changeGeometries();
          const source = "input";
          const featureIndex = that.geometries.length - 1;
          that.currentEntity.__featureRef = { source, featureIndex };
          that.currentEntity.__featureProperties = {};
          console.log("绘制了线:", geometry);
          // 取消当前绘制模式
          that.deactivateDraw();
        }
      }, SkyScenery.ScreenSpaceEventType.RIGHT_CLICK);
    },

    // 绘制面
    drawPolygon() {
      const that = this;

      // 鼠标移动时更新临时面（实时渲染预览）
      this.handler.setInputAction(function (event) {
        if (that.currentPositions.length > 0) {
          const endPosition = that.pickSurfaceCartesian(event.endPosition);

          if (endPosition) {
            const last = that.currentPositions[that.currentPositions.length - 1];
            const first = that.currentPositions[0];
            const pending = [last, endPosition, first];
            // 待落点边使用虚线
            if (!that.tempEntity) {
              that.tempEntity = viewer.entities.add({
                polyline: {
                  show: true,
                  positions: pending,
                  material: new SkyScenery.PolylineDashMaterialProperty({
                    color: SkyScenery.Color.RED.withAlpha(0.95),
                    dashLength: 10,
                  }),
                  width: 3,
                  clampToGround: true,
                },
              });
              viewer.scene.requestRender();
            } else {
              that.tempEntity.polyline.positions = pending;
              viewer.scene.requestRender();
            }
          }
        }
      }, SkyScenery.ScreenSpaceEventType.MOUSE_MOVE);

      // 左键点击添加点
      this.handler.setInputAction(function (event) {
        const position = that.pickSurfaceCartesian(event.position);

        if (position) {
          // 检查是否点击了第一个点附近（自动闭合）
          if (
            that.currentPositions.length > 2 &&
            that.isPositionNearFirst(position, that.currentPositions[0])
          ) {
            // 完成多边形绘制
            that.finalizePolygonDrawing();
            that.deactivateDraw();
            return;
          }

          that.currentPositions.push(position.clone());

          // 至少两个点时先显示已落点实线（第三个点后切换为实面+实边）
          if (that.currentPositions.length === 2) {
            const linePositions = [...that.currentPositions];
            if (!that.currentEntity || !that.currentEntity.polyline) {
              if (that.currentEntity) {
                viewer.entities.remove(that.currentEntity);
              }
              that.currentEntity = viewer.entities.add({
                polyline: {
                  show: true,
                  positions: linePositions,
                  material: SkyScenery.Color.RED,
                  width: 3,
                  clampToGround: true,
                },
              });
            } else {
              that.currentEntity.polyline.positions = linePositions;
            }
            viewer.scene.requestRender();
          } else if (that.currentPositions.length > 2) {
            // 闭合多边形
            const closedPositions = [...that.currentPositions, that.currentPositions[0]];
            if (!that.currentEntity) {
              that.currentEntity = viewer.entities.add({
                polygon: {
                  show: true,
                  hierarchy: new SkyScenery.PolygonHierarchy(closedPositions),
                  material: SkyScenery.Color.RED.withAlpha(0.35),
                  outline: true,
                  outlineColor: SkyScenery.Color.RED,
                  outlineWidth: 2,
                },
              });
            } else if (that.currentEntity.polygon) {
              that.currentEntity.polygon.hierarchy = new SkyScenery.PolygonHierarchy(closedPositions);
            } else {
              // 第二点阶段是线实体，第三点开始改为面实体
              viewer.entities.remove(that.currentEntity);
              that.currentEntity = viewer.entities.add({
                polygon: {
                  show: true,
                  hierarchy: new SkyScenery.PolygonHierarchy(closedPositions),
                  material: SkyScenery.Color.RED.withAlpha(0.35),
                  outline: true,
                  outlineColor: SkyScenery.Color.RED,
                  outlineWidth: 2,
                },
              });
            }
            viewer.scene.requestRender();
          }
        }
      }, SkyScenery.ScreenSpaceEventType.LEFT_CLICK);

      // 右键点击完成绘制
      this.handler.setInputAction(function () {
        if (that.currentPositions.length > 2) {
          that.finalizePolygonDrawing();
          // 取消当前绘制模式
          that.deactivateDraw();
        }
      }, SkyScenery.ScreenSpaceEventType.RIGHT_CLICK);
    },

    // 完成多边形绘制
    finalizePolygonDrawing() {
      // 移除临时预览实体
      if (this.tempEntity) {
        viewer.entities.remove(this.tempEntity);
        this.tempEntity = null;
      }

      // 确保多边形是闭合的
      let closedPositions = [...this.currentPositions];
      // 创建最终的多边形实体
      if (this.currentEntity) {
        viewer.entities.remove(this.currentEntity);
      }

      this.currentEntity = viewer.entities.add({
        polygon: {
          show: true,
          hierarchy: new SkyScenery.PolygonHierarchy(closedPositions),
          material: SkyScenery.Color.RED.withAlpha(0.5),
          outline: true,
          outlineColor: SkyScenery.Color.BLUE,
          outlineWidth: 2,
        },
      });
      viewer.scene.requestRender();

      this.drawnEntities.push(this.currentEntity);

      // 转换为geometry格式并保存（不包含重复的最后一个点）
      const coordinates = [];
      const mainRing = [];
      for (let i = 0; i < closedPositions.length; i++) {
        const cartographic = SkyScenery.Cartographic.fromCartesian(closedPositions[i]);
        mainRing.push([
          SkyScenery.Math.toDegrees(cartographic.longitude),
          SkyScenery.Math.toDegrees(cartographic.latitude),
        ]);
      }
      const cartographic = SkyScenery.Cartographic.fromCartesian(closedPositions[0]);
      mainRing.push([
        SkyScenery.Math.toDegrees(cartographic.longitude),
        SkyScenery.Math.toDegrees(cartographic.latitude),
      ]);
      coordinates.push(mainRing);

      const geometry = {
        type: "Polygon",
        coordinates: coordinates, // GeoJSON格式
        holes: [], // 用于存储镂空区域
      };

      // 保存实体和几何对象的关联
      this.currentEntity.geometryRef = geometry;
      this.geometries.push(geometry);
      this.changeGeometries();
      const source = "input";
      const featureIndex = this.geometries.length - 1;
      this.currentEntity.__featureRef = { source, featureIndex };
      this.currentEntity.__featureProperties = {};
      console.log("绘制了面:", geometry);
    },

    // 开始绘制镂空
    startHoleDrawing() {
      // 如果已经在绘制镂空，则取消
      if (this.isDrawingHole) {
        this.isDrawingHole = false;
        this.deactivateDraw();
        return;
      }

      // 取消其他绘制模式
      this.deactivateDraw();

      // 设置为镂空绘制模式
      this.isDrawingHole = true;

      console.log("请点击一个多边形以添加镂空");

      // 设置点击事件选择多边形
      const that = this;
      this.handler.setInputAction(function (event) {
        // 拾取实体
        const pickedObject = viewer.scene.pick(event.position);
        if (pickedObject && pickedObject.id && pickedObject.id.polygon) {
          const polygonEntity = pickedObject.id;

          // 检查是否有对应的几何对象
          if (polygonEntity.geometryRef && polygonEntity.geometryRef.type === "Polygon") {
            that.currentPolygonEntity = polygonEntity;
            that.currentPolygonGeometry = polygonEntity.geometryRef;

            console.log("已选择多边形，现在绘制镂空区域");

            // 开始绘制镂空区域（使用多边形绘制逻辑，但最后添加为镂空）
            that.resetDrawingState();
            that.drawHole();
          } else {
            console.log("选择的不是有效的多边形");
          }
        } else {
          console.log("未选中任何多边形");
        }
      }, SkyScenery.ScreenSpaceEventType.LEFT_CLICK);
    },

    // 绘制镂空区域
    drawHole() {
      const that = this;
      this.enterInteractiveSceneRender();

      // 清除之前的事件
      this.handler.removeInputAction(SkyScenery.ScreenSpaceEventType.LEFT_CLICK);

      // 鼠标移动时更新临时镂空区域
      this.handler.setInputAction(function (event) {
        if (that.currentPositions.length > 0) {
          const endPosition = that.pickSurfaceCartesian(event.endPosition);

          if (endPosition) {
            // 为了预览效果，临时闭合多边形
            const tempPositions = [...that.currentPositions, endPosition, that.currentPositions[0]];

            // 更新临时预览实体
            if (!that.tempEntity) {
              that.tempEntity = viewer.entities.add({
                polygon: {
                  show: true,
                  hierarchy: new SkyScenery.PolygonHierarchy(tempPositions),
                  material: new SkyScenery.ColorMaterialProperty(
                    new SkyScenery.Color(1, 0, 0, 0.3)
                  ),
                  outline: true,
                  outlineColor: SkyScenery.Color.RED,
                  outlineWidth: 2,
                },
              });
              viewer.scene.requestRender();
            } else {
              that.tempEntity.polygon.hierarchy = new SkyScenery.PolygonHierarchy(tempPositions);
            }
          }
        }
      }, SkyScenery.ScreenSpaceEventType.MOUSE_MOVE);

      // 左键点击添加点
      this.handler.setInputAction(function (event) {
        const position = that.pickSurfaceCartesian(event.position);

        if (position) {
          // 检查是否点击了第一个点附近（自动闭合）
          if (
            that.currentPositions.length > 2 &&
            that.isPositionNearFirst(position, that.currentPositions[0])
          ) {
            // 完成镂空绘制
            that.finalizeHoleDrawing();
            return;
          }

          that.currentPositions.push(position.clone());
        }
      }, SkyScenery.ScreenSpaceEventType.LEFT_CLICK);

      // 右键点击完成绘制
      this.handler.setInputAction(function () {
        if (that.currentPositions.length > 2) {
          that.finalizeHoleDrawing();
        }
      }, SkyScenery.ScreenSpaceEventType.RIGHT_CLICK);
    },

    // 完成镂空绘制
    finalizeHoleDrawing() {
      // 移除临时预览实体
      if (this.tempEntity) {
        viewer.entities.remove(this.tempEntity);
        viewer.scene.requestRender();
        this.tempEntity = null;
      }

      // 确保镂空区域是闭合的
      let closedPositions = [...this.currentPositions];

      // 转换为几何坐标
      const holeCoordinates = [];
      for (let i = 0; i < closedPositions.length; i++) {
        const cartographic = SkyScenery.Cartographic.fromCartesian(closedPositions[i]);
        holeCoordinates.push([
          SkyScenery.Math.toDegrees(cartographic.longitude),
          SkyScenery.Math.toDegrees(cartographic.latitude),
        ]);
      }

      const cartographic = SkyScenery.Cartographic.fromCartesian(closedPositions[0]);
      holeCoordinates.push([
        SkyScenery.Math.toDegrees(cartographic.longitude),
        SkyScenery.Math.toDegrees(cartographic.latitude),
      ]);
      // 添加到几何对象的镂空数组
      if (!this.currentPolygonGeometry.holes) {
        this.currentPolygonGeometry.holes = [];
      }
      this.currentPolygonGeometry.holes.push(holeCoordinates);

      // 更新多边形的层级结构以包含镂空
      const polygonHierarchy = this.buildPolygonHierarchyWithHoles(
        this.currentPolygonGeometry.coordinates[0],
        this.currentPolygonGeometry.holes
      );

      // 更新实体显示
      this.currentPolygonEntity.polygon.hierarchy = polygonHierarchy;

      console.log("添加了镂空区域:", holeCoordinates);
      console.log("更新后的多边形几何:", this.currentPolygonGeometry);

      // 重置状态
      this.resetDrawingState();
      this.isDrawingHole = false;
      this.currentPolygonEntity = null;
      this.currentPolygonGeometry = null;

      // 取消绘制模式
      this.deactivateDraw();
    },

    // 构建包含镂空的多边形层级结构
    buildPolygonHierarchyWithHoles(outerRing, holes) {
      // 将外部环转换为Cartesian3数组
      const outerRingPositions = [];
      outerRing.forEach((coord) => {
        const cartesian = SkyScenery.Cartesian3.fromDegrees(coord[0], coord[1]);
        outerRingPositions.push(cartesian);
      });
      // 闭合外部环
      outerRingPositions.push(outerRingPositions[0]);

      // 创建层级结构
      const hierarchy = new SkyScenery.PolygonHierarchy(outerRingPositions);

      // 添加镂空
      if (holes && holes.length > 0) {
        hierarchy.holes = holes.map((hole) => {
          const holePositions = [];
          hole.forEach((coord) => {
            const cartesian = SkyScenery.Cartesian3.fromDegrees(coord[0], coord[1]);
            holePositions.push(cartesian);
          });
          // 闭合镂空环
          holePositions.push(holePositions[0]);
          return new SkyScenery.PolygonHierarchy(holePositions);
        });
      }
      return hierarchy;
    },

    // 检查点是否靠近第一个点
    isPositionNearFirst(position, firstPosition, tolerance = 10) {
      // 单位：米
      const distance = SkyScenery.Cartesian3.distance(position, firstPosition);
      return distance < tolerance;
    },
    changeGeometries() {
      let requestData = {};
      const geometriesToSend = this.geometries;
      const baseFeatures =
        this.jsonData && Array.isArray(this.jsonData.features) ? this.jsonData.features : [];
      let FeatureCollectionFeatures = [];
      geometriesToSend.forEach((item, idx) => {
        const base = baseFeatures[idx] || {};
        FeatureCollectionFeatures.push({
          type: "Feature",
          properties: base.properties ? JSON.parse(JSON.stringify(base.properties)) : {},
          geometry: {
            type: item.type,
            coordinates: item.coordinates,
          },
        });
      });
      // 构造请求数据
      requestData = {
        type: "FeatureCollection",
        features: FeatureCollectionFeatures,
        timestamp: new Date().getTime(),
      };
      if (
        this.SceneValue &&
        this.dmsServerItem &&
        this.dmsServerItem.elementTypes &&
        this.ifHasType("unit") &&
        this.params.unit
      ) {
        requestData.unit = this.params.unit;
      }
      if (
        this.SceneValue &&
        this.dmsServerItem &&
        this.dmsServerItem.elementTypes &&
        this.ifHasType("EPSILON") &&
        this.params.EPSILON
      ) {
        requestData.EPSILON = this.params.EPSILON;
      }
      if (
        this.SceneValue &&
        this.dmsServerItem &&
        this.dmsServerItem.elementTypes &&
        this.ifHasType("inPrj") &&
        this.params.inPrj
      ) {
        requestData.inPrj = this.params.inPrj;
      }
      if (
        this.SceneValue &&
        this.dmsServerItem &&
        this.dmsServerItem.elementTypes &&
        this.ifHasType("compressionRatio") &&
        this.params.compressionRatio
      ) {
        requestData.compressionRatio = this.params.compressionRatio;
      }
      if (
        this.SceneValue &&
        this.dmsServerItem &&
        this.dmsServerItem.elementTypes &&
        this.ifHasType("outPrj") &&
        this.params.outPrj
      ) {
        requestData.outPrj = this.params.outPrj;
      }
      if (
        this.SceneValue &&
        this.dmsServerItem &&
        this.dmsServerItem.elementTypes &&
        this.ifHasType("distance") &&
        this.params.distance
      ) {
        requestData.distance = this.params.distance;
      }
      this.jsonData = requestData;
    },
    // 发送几何数据到后台接口
    sendGeometriesToBackend() {
      // 这里使用axios发送请求，需要确保项目中已安装并导入axios
      if (!this.dmsServerItem || !this.dmsServerItem.apiUrl) {
        this.$message({
          message: "请先选择有效场景并加载接口信息",
          type: "warning",
        });
        return;
      }
      let requestData;
      this.backData = {};
      let requestUrl = this.dmsServerItem.apiUrl;
      if (this.ifHasType("point") || this.ifHasType("polyline") || this.ifHasType("polygon")) {
        const check = this.validateSendGeometryParams();
        if (!check.ok) {
          this.$message({
            message: check.message,
            type: "warning",
          });
          return;
        }
        requestData = this.jsonData;
      } else {
        requestData = new FormData();
        this.dmsServerItem.elementTypes.forEach((key) => {
          if (key == "file") {
            if (!this.currentFile) {
              return this.$message({
                message: "请选择文件",
                type: "error",
              });
            }
            requestData.append("file", this.currentFile.raw);
          } else {
            requestData.append(key, this.params[key]);
          }
        });
        requestData.append("token", localStorage.getItem("token"));
      }
      let that = this;
      // 实际项目中使用以下代码发送请求
      wgn
        .topology(requestUrl, requestData)
        .then((res) => {
          if (requestUrl.indexOf("downloadFile") == -1) {
            that.backData = res;
            if (res.code && res.code == 200) {
              that.$message({
                message: res.message,
                type: "success",
              });
              const downPath = that.pickDownFilePathFromResponse(res);
              if (downPath) {
                that.$nextTick(() => {
                  that.downloadConvertedFile(downPath);
                });
              }
            } else {
              that.$message({
                message: res.content,
                type: "error",
              });
            }
          } else {
            const blob = res; // 响应体是 Blob 类型
            if (!blob) {
              that.$message.error("下载失败：文件流为空");
              reject("文件流为空");
              return;
            }
            let _downloadFile = that.params.filePath + "";
            let fileName = _downloadFile.substring(_downloadFile.indexOf("down_files") + 11);
            // D:\work\backCode\one_map_portal_server\down_files\0378b1c2e92a4c36ab447f552c0c7888.xlsx
            // 替换原代码中创建 url 的逻辑，先加校验
            console.log("blob 类型：", Object.prototype.toString.call(blob)); // 正常应输出 [object Blob]
            console.log("blob 大小：", blob.size); // 正常应大于 0

            if (!(blob instanceof Blob) || blob.size === 0) {
              this.$message.error("文件流解析失败，无法生成下载链接");
              return;
            }
            const url = window.URL.createObjectURL(blob); // 将 Blob 转为临时 URL
            const link = document.createElement("a");
            link.href = url;
            link.download = fileName; // 设置下载文件名
            link.style.display = "none";
            document.body.appendChild(link);
            link.click(); // 触发点击下载
            document.body.removeChild(link);
            window.URL.revokeObjectURL(url); // 销毁临时 URL，避免内存泄漏
          }
        })
        .catch((error) => {
          console.error("后台接口调用失败:", error);
        });
    },

    /**
     * 从接口返回 JSON 中解析 downFilePath（支持 content 为对象或 JSON 字符串、或顶层字段）。
     */
    pickDownFilePathFromResponse(res) {
      if (!res || typeof res !== "object") {
        return null;
      }
      const tryVal = (v) => {
        if (typeof v !== "string") {
          return null;
        }
        const s = v.trim();
        if (!s || s.indexOf("未知文件") !== -1) {
          return null;
        }
        return s;
      };
      let p = tryVal(res.downFilePath);
      if (p) {
        return p;
      }
      const c = res.content;
      if (c && typeof c === "object") {
        p = tryVal(c.downFilePath);
        if (p) {
          return p;
        }
      }
      if (typeof c === "string") {
        try {
          const parsed = JSON.parse(c);
          if (parsed && typeof parsed === "object") {
            p = tryVal(parsed.downFilePath);
            if (p) {
              return p;
            }
          }
        } catch (e) {
          /* ignore */
        }
      }
      return null;
    },

    /** 根据后端返回的 downFilePath，请求 downloadFile 并触发浏览器下载（与「下载文件」场景一致） */
    downloadConvertedFile(serverFilePath) {
      const base =
        typeof systemConfig !== "undefined" && systemConfig.baseServicerPath
          ? systemConfig.baseServicerPath
          : "";
      const url = base + "/spatiotemporal_data_format_conversion/downloadFile";
      getFile(url, { filePath: serverFilePath })
        .then((blob) => {
          if (!(blob instanceof Blob) || blob.size === 0) {
            this.$message.warning(
              "转换文件下载失败（空流），请从返回 JSON 的 downFilePath 手动获取"
            );
            return;
          }
          const name = this.fileNameFromPath(serverFilePath);
          const href = window.URL.createObjectURL(blob);
          const link = document.createElement("a");
          link.href = href;
          link.download = name;
          link.style.display = "none";
          document.body.appendChild(link);
          link.click();
          document.body.removeChild(link);
          window.URL.revokeObjectURL(href);
          this.$message.success("已开始下载转换文件：" + name);
        })
        .catch((e) => {
          console.error(e);
          this.$message.error("转换文件下载失败");
        });
    },
    fileNameFromPath(fullPath) {
      if (!fullPath) {
        return "download";
      }
      const s = String(fullPath).replace(/\\/g, "/");
      const i = s.lastIndexOf("/");
      return i >= 0 ? s.slice(i + 1) : s;
    },

    // 清除所有绘制的元素,并更新geometries
    clearAll() {
      this.clearAllMap();
      this.changeGeometries();
    },
    // 清除所有绘制的元素
    clearAllMap() {
      this.removeVertexHandleEntities();
      this.mapEditSelection = null;
      this.hoveredVertexIndex = -1;
      this.hoveredHandleEntity = null;
      this.clearHoveredFeatureStyle();
      this.geometryEditMode = false;
      this.pickedFeatureRef = null;
      this.hideHoverHint();
      // 移除所有实体
      this.drawnEntities.forEach((entity) => {
        viewer.entities.remove(entity);
      });
      viewer.scene.requestRender();
      // 清空数组
      this.drawnEntities = [];
      this.geometries = [];
      this.currentRenderedSource = "";
      this.propertyDialog.visible = false;
      this.propertyDialog.list = [];
      this.propertyDialog.source = "";
      this.propertyDialog.propertiesRef = null;
      this.propertyDialog.featureIndex = -1;
      this.resetPickDragState();
      this.setScreenSpaceCameraForDrag(true);
      // 取消当前绘制模式
      this.deactivateDraw();
      console.log("已清除所有绘制的元素");
    },
  },
};
</script>

<style lang="less" scoped>
// 操作栏样式
#controlPanelBox {
  width: calc(30vw - 2rem);
  min-width: 500px;
  height: calc(100vh - 2rem);
  display: flex;
  flex-direction: column;
  padding: 1rem;
  position: fixed;
  top: 0px;
  right: 0;
  background: #08224a;
}

// 绘制按钮区域
.toolbar {
  position: absolute;
  top: 10px;
  left: -120px;
  z-index: 1000;
  background: #08224a;
  border-radius: 8px;
  padding: 3px 5px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.tool-item {
  padding: 10px 20px;
  cursor: pointer;
  border-radius: 4px;
  transition: all 0.3s ease;
  user-select: none;
  font-size: 14px;
}

.tool-item:hover {
  background: #ffffff32;
}

.tool-item.active {
  background: #409eff;
  color: white;
}
.sceneNameBox {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.vueJsonEditor_box {
  width: 100%;
  height: 100%;
  position: relative;
}
.vueJsonEditor_tools {
  position: absolute;
  top: 8px;
  right: 10px;
  font-size: 14px;
  z-index: 1;
  color: #fff;
}
.vueJsonEditor_tools span {
  padding: 2px 5px;
  opacity: 0.8;
  cursor: pointer;
  border: 1px solid #ffffff00;
  border-radius: 2px;
  margin-right: 5px;
  &:hover {
    background-color: rgba(255, 255, 255, 0.2);
    border-color: rgba(255, 255, 255, 0.4);
  }
}
.feature-property-content {
  max-height: 360px;
  overflow: auto;
  padding-right: 4px;
}
.feature-property-tip {
  color: #9ed2ff;
  margin-bottom: 12px;
  font-size: 13px;
}
.feature-property-item {
  padding: 10px 12px;
  margin-bottom: 8px;
  border-radius: 8px;
  background: rgba(61, 132, 205, 0.14);
  border: 1px solid rgba(111, 186, 255, 0.35);
}
.feature-property-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.feature-property-delete-btn {
  flex-shrink: 0;
}
.feature-property-footer {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.feature-property-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}
.feature-property-key-input {
  width: 180px;
}
.map-edit-hover-hint {
  position: fixed;
  z-index: 3000;
  pointer-events: none;
  color: #4a4a4a;
  background: #fff;
  border-radius: 4px;
  padding: 7px 10px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
  font-size: 12px;
  line-height: 1.2;
  transform-origin: left top;
  animation: mapHintPop 120ms ease-out;
}
@keyframes mapHintPop {
  from {
    transform: scale(0.92);
    opacity: 0.5;
  }
  to {
    transform: scale(1);
    opacity: 1;
  }
}
:deep(.feature-property-input .el-input__wrapper) {
  background: rgba(8, 34, 74, 0.68);
  box-shadow: inset 0 0 0 1px rgba(130, 198, 255, 0.4);
}
:deep(.feature-property-input .el-input__inner) {
  color: #ffffff;
}
:deep(.feature-property-dialog .el-dialog) {
  background: rgba(7, 24, 48, 0.95);
  border: 1px solid rgba(111, 186, 255, 0.5);
}
:deep(.feature-property-dialog .el-dialog__title) {
  color: #e8f4ff;
  font-weight: 700;
}
:deep(.feature-property-dialog .el-dialog__headerbtn .el-dialog__close) {
  color: #d8ecff;
}
:deep(.feature-property-dialog .el-dialog__body) {
  padding-top: 12px;
}
:deep(.ace_editor) {
  height: 600px !important;
  max-height: calc(100vh - 300px) !important;
}
:deep(.jsoneditor-modes),
:deep(.jsoneditor-poweredBy) {
  display: none;
}
</style>
