<template>
  <div ref="containerRef" class="cesium-map">
    <div ref="viewerRef" class="cesium-map__viewer"></div>
  </div>
</template>

<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue"
import polygonClipping from "polygon-clipping"
import { HONGYUAN_BOUNDS } from "@/config/layers"
import { CHINA_GEOJSON_PATH, HONGYUAN_COUNTY_OUTLINE_PATH, getPublicMapResourcePath } from "@/config/mapResources"
import { hongyuanTownshipLabelPoints, hongyuanTownshipsGeoJson } from "@/config/townshipBoundaries"

const NATIONAL_BACKDROP_BOUNDS = {
  west: 100.18,
  south: 30.96,
  east: 105.02,
  north: 34.46,
}
const HONGYUAN_HIGH_RES_MAX_LEVEL = 17
const COUNTY_VIEW_HEADING = -18
const COUNTY_VIEW_PITCH = -50
const COUNTY_VIEW_CENTER_LAT_OFFSET = 0.045
const DETAIL_VIEW_HEADING = -18
const DETAIL_VIEW_PITCH = -58
const COUNTY_MASK_PADDING_LNG = 4.2
const COUNTY_MASK_PADDING_LAT = 3.2
const COUNTY_WALL_MIN_HEIGHT = 500
const COUNTY_WALL_MAX_HEIGHT = 7600
const COUNTY_STAGE_HEIGHT = 900
const COUNTY_BOUNDARY_RENDER_HEIGHT = 4700
const TOWNSHIP_BOUNDARY_RENDER_HEIGHT = 4620
let hongyuanCountyClipMultiPolygon = []
let hongyuanCountyBoundaryPromise = null

const props = defineProps({
  active: {
    type: Boolean,
    default: true,
  },
  activeLayer: {
    type: Object,
    default: null,
  },
  mapRanking: {
    type: Object,
    default: null,
  },
  overlayLayer: {
    type: Object,
    default: null,
  },
  overlayLayers: {
    type: Array,
    default: () => [],
  },
  tileUrlTemplate: {
    type: String,
    default: "https://qkl-map.oss-cn-chengdu.aliyuncs.com/hy-result/{z}/{x}/{y}.png",
  },
  referenceTileUrlTemplate: {
    type: String,
    default: "https://map.shuxitech.com/vt/lyrs=s&hl=zh-CN&x={x}&y={y}&z={z}",
  },
  terrainUrl: {
    type: String,
    default: "/terrain",
  },
  center: {
    type: Array,
    default: () => [102.600005, 32.585593],
  },
  initialZoom: {
    type: Number,
    default: 9,
  },
  minimumZoomDistance: {
    type: Number,
    default: 350,
  },
  countyViewTargetOffset: {
    type: Object,
    default: null,
  },
  autoApplyLayerMapView: {
    type: Boolean,
    default: true,
  },
  maxZoom: {
    type: Number,
    default: 21,
  },
  adminBoundaryLineScale: {
    type: Number,
    default: 1,
  },
  showNationalBackdrop: {
    type: Boolean,
    default: true,
  },
  enableImageryFeaturePick: {
    type: Boolean,
    default: true,
  },
  enableTownPolygonPick: {
    type: Boolean,
    default: true,
  },
  hideTownLabelStats: {
    type: Boolean,
    default: false,
  },
})

const emit = defineEmits([
  "ready",
  "scene-mounted",
  "feature-loading",
  "feature-info",
  "layer-status",
  "tile-status",
  "map-click",
  "draw-boundary",
])

const containerRef = ref(null)
const viewerRef = ref(null)
const canvasMap = shallowRef(null)
const activeLayerStack = computed(() => {
  if (props.activeLayer) {
    return [props.activeLayer, ...(Array.isArray(props.activeLayer.overlayLayers) ? props.activeLayer.overlayLayers : [])].filter(Boolean)
  }
  if (props.overlayLayers.length) return props.overlayLayers.filter(Boolean)
  return props.overlayLayer ? [props.overlayLayer] : []
})

let viewer = null
let clickHandler = null
let resizeObserver = null
let cameraMoveEndRemove = null
let terrainFallbackApplied = false
let layerRevision = 0
let selectedEntity = null
let drawingEnabled = false
let drawingPoints = []
let baseImageryLayer = null
let nationalBackdropImageryLayer = null
let topicImageryLayers = []
let boundaryEntities = []
let topicEntities = []
let rankingEntities = []
let focusEntities = []
let selectedOverlayEntities = []
let selectedOverlayDataSource = null
let drawingEntities = []
let analysisEntities = []
let countyFrameEntities = []
let countyStageEntities = []
const townBadgeImageCache = new Map()

onMounted(() => {
  emit("scene-mounted")
  if (props.active) initializeCesium()
})

onBeforeUnmount(() => destroyMap())

watch(
  activeLayerStack,
  () => refreshTopicLayers(),
  { deep: true },
)

watch(
  () => props.mapRanking,
  () => refreshMapRanking(),
  { deep: true },
)

watch(
  () => props.hideTownLabelStats,
  () => refreshMapRanking(),
)

watch(
  () => props.active,
  async (active) => {
    if (!active) {
      destroyMap()
      return
    }
    await nextTick()
    initializeCesium()
  },
)

function initializeCesium() {
  if (viewer || !viewerRef.value) return Boolean(viewer)
  const Cesium = window.Cesium
  if (!Cesium) {
    emit("layer-status", { type: "error", text: "Cesium 运行资源加载失败" })
    return false
  }

  terrainFallbackApplied = false
  const terrainProvider = createTerrainProvider(Cesium)
  const normalizedTileUrlTemplate = normalizeTileUrlTemplate(props.tileUrlTemplate)
  const normalizedReferenceTileUrlTemplate = normalizeTileUrlTemplate(props.referenceTileUrlTemplate)
  const hasHongyuanHighResolutionTile = isHongyuanHighResolutionTile(normalizedTileUrlTemplate)
  const referenceProvider =
    normalizedReferenceTileUrlTemplate && normalizedReferenceTileUrlTemplate !== normalizedTileUrlTemplate
      ? createImageryProvider(Cesium, normalizedReferenceTileUrlTemplate, {
          maximumLevel: Math.min(props.maxZoom, 19),
        })
      : null
  const baseProvider = normalizedTileUrlTemplate
    ? createImageryProvider(Cesium, normalizedTileUrlTemplate, {
        maximumLevel: hasHongyuanHighResolutionTile
          ? Math.min(props.maxZoom, HONGYUAN_HIGH_RES_MAX_LEVEL)
          : props.maxZoom,
        rectangle: hasHongyuanHighResolutionTile ? getHongyuanImageryRectangle(Cesium) : null,
      })
    : null

  viewer = new Cesium.Viewer(viewerRef.value, {
    animation: false,
    baseLayerPicker: false,
    fullscreenButton: false,
    geocoder: false,
    homeButton: false,
    infoBox: false,
    navigationHelpButton: false,
    sceneModePicker: false,
    selectionIndicator: false,
    timeline: false,
    scene3DOnly: true,
    imageryProvider: false,
    terrainProvider,
    useBrowserRecommendedResolution: false,
    requestRenderMode: true,
    maximumRenderTimeChange: Infinity,
    contextOptions: {
      webgl: {
        alpha: true,
        antialias: true,
      },
    },
  })

  if (referenceProvider) {
    const referenceLayer = viewer.imageryLayers.addImageryProvider(referenceProvider)
    referenceLayer.alpha = 1
    referenceLayer.brightness = 0.68
    referenceLayer.contrast = 1.08
    referenceLayer.saturation = 0.48
    referenceLayer.gamma = 0.9
  }
  if (baseProvider) {
    baseImageryLayer = viewer.imageryLayers.addImageryProvider(baseProvider)
    baseImageryLayer.alpha = hasHongyuanHighResolutionTile ? 0.88 : 1
    baseImageryLayer.brightness = hasHongyuanHighResolutionTile ? 0.96 : 0.9
    baseImageryLayer.contrast = hasHongyuanHighResolutionTile ? 1.12 : 1.1
    baseImageryLayer.saturation = hasHongyuanHighResolutionTile ? 1.08 : 1.05
    baseImageryLayer.gamma = hasHongyuanHighResolutionTile ? 0.98 : 0.96
  }
  viewer.scene.globe.terrainExaggeration = 1.5
  viewer.scene.globe.depthTestAgainstTerrain = false
  viewer.scene.globe.baseColor = Cesium.Color.fromCssColorString("#061821")
  viewer.scene.backgroundColor = Cesium.Color.fromCssColorString("#020b12")
  viewer.scene.skyAtmosphere.show = false
  if (viewer.scene.skyBox) viewer.scene.skyBox.show = false
  viewer.scene.fog.enabled = true
  viewer.scene.fog.density = 0.00018
  viewer.resolutionScale = Math.min(Math.max(window.devicePixelRatio || 1, 1), 2)
  viewer.scene.fxaa = false
  viewer.scene.postProcessStages.fxaa.enabled = false
  viewer.scene.screenSpaceCameraController.enableCollisionDetection = true
  viewer.scene.screenSpaceCameraController.minimumZoomDistance = Math.max(
    1,
    Number(props.minimumZoomDistance) || 1,
  )
  viewer.scene.screenSpaceCameraController.maximumZoomDistance = 560000
  if (viewer.cesiumWidget?.creditContainer) viewer.cesiumWidget.creditContainer.style.display = "none"
  if (viewer._cesiumWidget?._creditContainer) viewer._cesiumWidget._creditContainer.style.display = "none"

  bindTerrainFallback(terrainProvider, Cesium)
  loadCountyBoundary()
    .then(() => {
      if (!viewer || viewer.isDestroyed?.()) return
      addCountyFrame()
      updateBoundaryVisualScale()
    })
    .catch((error) => {
      console.warn("[cesium-map] county outline load failed", error)
      emit("layer-status", { type: "error", text: "红原县边界加载失败" })
    })
  addTownshipBoundaries()
  bindMapClick()
  connectResizeObserver()
  connectBoundaryVisualScale()
  setCountyView(false)
  updateBoundaryVisualScale()
  refreshTopicLayers()
  refreshMapRanking()

  const facade = {
    viewer,
    clearSelectedFeature,
    focusProject,
    focusPasture,
    focusTownByName: focusTown,
    focusYakIndustryPoint,
    focusVectorFeature,
    getVectorFeatureAnchorScreen,
    getVectorFeaturesAnchorScreen,
    getVectorFeatureScreen,
    getYakIndustryPointScreen,
    destroy: destroyMap,
  }
  canvasMap.value = facade

  baseProvider?.errorEvent?.addEventListener?.(() => {
    emit("tile-status", { type: "warning", text: "牧场影像底图部分瓦片加载失败" })
  })
  emit("tile-status", { type: "success", text: "牧场影像底图已加载" })
  window.setTimeout(() => {
    if (viewer && !viewer.isDestroyed?.()) emit("ready")
  }, 0)
  nextTick(refreshView)
  return true
}

function createTerrainProvider(Cesium) {
  if (!props.terrainUrl) return new Cesium.EllipsoidTerrainProvider()
  try {
    return new Cesium.CesiumTerrainProvider({
      url: props.terrainUrl,
      requestVertexNormals: true,
      requestWaterMask: false,
    })
  } catch (error) {
    terrainFallbackApplied = true
    return new Cesium.EllipsoidTerrainProvider()
  }
}

function createImageryProvider(Cesium, url, options = {}) {
  if (!url) return null
  return new Cesium.UrlTemplateImageryProvider({
    url,
    minimumLevel: options.minimumLevel || 3,
    maximumLevel: options.maximumLevel || props.maxZoom,
    rectangle: options.rectangle || undefined,
    tilingScheme: new Cesium.WebMercatorTilingScheme(),
    credit: "",
  })
}

function normalizeTileUrlTemplate(url) {
  return String(url || "").trim()
}

function isHongyuanHighResolutionTile(url) {
  return /hy-result|qkl-map/i.test(String(url || ""))
}

function getHongyuanImageryRectangle(Cesium) {
  const paddingLng = 0.035
  const paddingLat = 0.035
  return Cesium.Rectangle.fromDegrees(
    HONGYUAN_BOUNDS.west - paddingLng,
    HONGYUAN_BOUNDS.south - paddingLat,
    HONGYUAN_BOUNDS.east + paddingLng,
    HONGYUAN_BOUNDS.north + paddingLat,
  )
}

async function addNationalBackdrop(Cesium) {
  const targetViewer = viewer
  try {
    const response = await fetch(getPublicMapResourcePath(CHINA_GEOJSON_PATH), { credentials: "same-origin" })
    if (!response.ok) throw new Error(`HTTP ${response.status}`)
    const canvas = createNationalBackdropCanvas(await response.json())
    if (!canvas || viewer !== targetViewer || targetViewer?.isDestroyed?.()) return
    const provider = new Cesium.SingleTileImageryProvider({
      url: canvas.toDataURL("image/png"),
      rectangle: Cesium.Rectangle.fromDegrees(
        NATIONAL_BACKDROP_BOUNDS.west,
        NATIONAL_BACKDROP_BOUNDS.south,
        NATIONAL_BACKDROP_BOUNDS.east,
        NATIONAL_BACKDROP_BOUNDS.north,
      ),
      credit: "",
    })
    nationalBackdropImageryLayer = targetViewer.imageryLayers.addImageryProvider(provider, 0)
    nationalBackdropImageryLayer.alpha = 0.68
    nationalBackdropImageryLayer.brightness = 0.84
    nationalBackdropImageryLayer.contrast = 1.16
    targetViewer.scene.requestRender()
  } catch (error) {
    console.warn("[cesium-map] national backdrop failed", error)
  }
}

function createNationalBackdropCanvas(data) {
  const features = data?.type === "FeatureCollection" ? data.features : []
  const points = []
  features.forEach((feature) => collectCoordinates(feature?.geometry?.coordinates, points))
  if (!features.length || !points.length) return null

  const canvas = document.createElement("canvas")
  canvas.width = 2400
  canvas.height = 1600
  const context = canvas.getContext("2d")
  if (!context) return null

  const sourceBounds = points.reduce((bounds, point) => ({
    west: Math.min(bounds.west, point[0]),
    south: Math.min(bounds.south, point[1]),
    east: Math.max(bounds.east, point[0]),
    north: Math.max(bounds.north, point[1]),
  }), { west: Infinity, south: Infinity, east: -Infinity, north: -Infinity })
  const padding = 92
  const sourceWidth = Math.max(0.0001, sourceBounds.east - sourceBounds.west)
  const sourceHeight = Math.max(0.0001, sourceBounds.north - sourceBounds.south)
  const scale = Math.min(
    (canvas.width - padding * 2) / sourceWidth,
    (canvas.height - padding * 2) / sourceHeight,
  )
  const offsetX = (canvas.width - sourceWidth * scale) / 2
  const offsetY = (canvas.height - sourceHeight * scale) / 2
  const project = (coordinate) => [
    offsetX + (Number(coordinate[0]) - sourceBounds.west) * scale,
    canvas.height - offsetY - (Number(coordinate[1]) - sourceBounds.south) * scale,
  ]

  drawBackdropGrid(context, canvas)
  drawBackdropHalo(context, canvas)

  const drawPath = () => {
    context.beginPath()
    features.forEach((feature) => {
      forEachPolygon(feature?.geometry, (rings) => {
        rings.forEach((ring) => {
          ring.forEach((coordinate, index) => {
            const point = project(coordinate)
            if (index === 0) context.moveTo(point[0], point[1])
            else context.lineTo(point[0], point[1])
          })
          context.closePath()
        })
      })
    })
  }

  const fill = context.createLinearGradient(0, 0, canvas.width, canvas.height)
  fill.addColorStop(0, "rgba(16, 47, 76, 0.14)")
  fill.addColorStop(0.48, "rgba(8, 38, 65, 0.1)")
  fill.addColorStop(1, "rgba(3, 22, 42, 0.06)")
  drawPath()
  context.fillStyle = fill
  context.fill("evenodd")

  drawPath()
  context.save()
  context.strokeStyle = "rgba(37, 139, 207, 0.14)"
  context.lineWidth = 5.6
  context.shadowColor = "rgba(38, 170, 255, 0.2)"
  context.shadowBlur = 14
  context.stroke()
  context.restore()

  drawPath()
  context.strokeStyle = "rgba(89, 168, 221, 0.32)"
  context.lineWidth = 1
  context.stroke()

  drawProvinceInnerLines(context, features, project)
  drawBackdropScanArcs(context, canvas)
  return canvas
}

function drawBackdropGrid(context, canvas) {
  const width = canvas.width
  const height = canvas.height
  context.save()
  context.globalAlpha = 0.72
  context.fillStyle = "rgba(0, 0, 0, 0)"
  context.clearRect(0, 0, width, height)

  const verticalGradient = context.createLinearGradient(0, 0, 0, height)
  verticalGradient.addColorStop(0, "rgba(25, 84, 126, 0.02)")
  verticalGradient.addColorStop(0.52, "rgba(20, 93, 145, 0.08)")
  verticalGradient.addColorStop(1, "rgba(9, 45, 77, 0.03)")
  context.fillStyle = verticalGradient
  context.fillRect(0, 0, width, height)

  context.strokeStyle = "rgba(47, 128, 179, 0.18)"
  context.lineWidth = 1
  for (let x = -width * 0.18; x <= width * 1.16; x += 88) {
    context.beginPath()
    context.moveTo(x, height)
    context.lineTo(x + width * 0.34, 0)
    context.stroke()
  }
  for (let y = 120; y <= height - 70; y += 86) {
    context.beginPath()
    context.moveTo(0, y)
    context.lineTo(width, y - 110)
    context.stroke()
  }

  context.fillStyle = "rgba(49, 157, 213, 0.22)"
  for (let x = 80; x < width; x += 76) {
    for (let y = 84; y < height; y += 76) {
      const fade = 1 - Math.min(0.78, Math.hypot(x - width * 0.5, y - height * 0.54) / (width * 0.62))
      if (fade <= 0.1) continue
      context.globalAlpha = fade * 0.5
      context.fillRect(x, y, 3, 3)
    }
  }
  context.restore()
}

function drawBackdropHalo(context, canvas) {
  const width = canvas.width
  const height = canvas.height
  const centerX = width * 0.5
  const centerY = height * 0.52
  context.save()
  context.globalCompositeOperation = "lighter"
  const glow = context.createRadialGradient(centerX, centerY, 80, centerX, centerY, width * 0.58)
  glow.addColorStop(0, "rgba(38, 198, 255, 0.16)")
  glow.addColorStop(0.36, "rgba(30, 132, 208, 0.08)")
  glow.addColorStop(1, "rgba(10, 38, 68, 0)")
  context.fillStyle = glow
  context.fillRect(0, 0, width, height)
  context.restore()
}

function drawBackdropScanArcs(context, canvas) {
  const width = canvas.width
  const height = canvas.height
  const centerX = width * 0.5
  const centerY = height * 0.52
  context.save()
  context.globalCompositeOperation = "lighter"
  context.lineCap = "round"
  ;[
    { radius: 330, start: -0.32, end: 0.78, alpha: 0.28 },
    { radius: 430, start: 2.58, end: 3.52, alpha: 0.2 },
    { radius: 540, start: 4.92, end: 5.76, alpha: 0.16 },
  ].forEach((arc) => {
    context.beginPath()
    context.strokeStyle = `rgba(45, 186, 255, ${arc.alpha})`
    context.lineWidth = 8
    context.arc(centerX, centerY, arc.radius, arc.start, arc.end)
    context.stroke()
  })
  context.restore()
}

function drawProvinceInnerLines(context, features, project) {
  context.save()
  context.strokeStyle = "rgba(94, 169, 218, 0.18)"
  context.lineWidth = 0.6
  features.forEach((feature) => {
    context.beginPath()
    forEachPolygon(feature?.geometry, (rings) => {
      rings.forEach((ring) => {
        ring.forEach((coordinate, index) => {
          const point = project(coordinate)
          if (index === 0) context.moveTo(point[0], point[1])
          else context.lineTo(point[0], point[1])
        })
        context.closePath()
      })
    })
    context.stroke()
  })
  context.restore()
}

function forEachPolygon(geometry, callback) {
  if (!geometry || typeof callback !== "function") return
  if (geometry.type === "Polygon") {
    callback(geometry.coordinates || [])
    return
  }
  if (geometry.type === "MultiPolygon") {
    ;(geometry.coordinates || []).forEach(callback)
  }
}

function createFeatureClipMultiPolygon(data, label = "feature") {
  const featurePolygons = []
  ;(data?.features || []).forEach((feature) => {
    const geometry = normalizeGeometry(feature?.geometry)
    getGeometryPolygons(geometry).forEach((polygon) => {
      const cleanPolygon = sanitizeClipPolygon(polygon)
      if (cleanPolygon) featurePolygons.push(cleanPolygon)
    })
  })
  if (!featurePolygons.length) return []
  try {
    return polygonClipping.union(...featurePolygons)
  } catch (error) {
    console.warn(`[cesium-map] ${label} union boundary failed`, error)
    return featurePolygons.map((polygon) => polygon)
  }
}

async function loadCountyBoundary() {
  if (hongyuanCountyClipMultiPolygon.length) return hongyuanCountyClipMultiPolygon
  if (!hongyuanCountyBoundaryPromise) {
    hongyuanCountyBoundaryPromise = fetchJson(HONGYUAN_COUNTY_OUTLINE_PATH)
      .then((data) => {
        const polygons = createFeatureClipMultiPolygon(data, "county outline")
        hongyuanCountyClipMultiPolygon = polygons
        return polygons
      })
      .catch((error) => {
        hongyuanCountyBoundaryPromise = null
        throw error
      })
  }
  return hongyuanCountyBoundaryPromise
}

function sanitizeClipPolygon(polygon) {
  if (!Array.isArray(polygon)) return null
  const rings = polygon
    .map((ring) => closeClipRing((ring || []).map(normalizeLngLat).filter(Boolean)))
    .filter((ring) => ring.length >= 4)
  return rings.length ? rings : null
}

function closeClipRing(ring = []) {
  if (!ring.length) return []
  const first = ring[0]
  const last = ring[ring.length - 1]
  if (isSameLngLat(first, last)) return ring
  return [...ring, first]
}

function bindTerrainFallback(provider, Cesium) {
  const applyFallback = () => {
    if (!viewer || terrainFallbackApplied) return
    terrainFallbackApplied = true
    viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider()
    viewer.scene.globe.terrainExaggeration = 1
    viewer.scene.requestRender()
    emit("layer-status", { type: "warning", text: "CXPT 地形服务暂不可用，已保留三维影像地图" })
  }
  provider?.errorEvent?.addEventListener?.(applyFallback)
  provider?.readyPromise?.catch?.(applyFallback)
}

function loadMap() {
  return initializeCesium()
}

function hasMap() {
  return Boolean(viewer && !viewer.isDestroyed?.())
}

function play() {
  resume()
}

function resume() {
  if (!viewer) {
    initializeCesium()
    return
  }
  viewer.useDefaultRenderLoop = true
  refreshView()
}

function pause() {
  destroyMap()
}

function refreshView() {
  if (!viewer || viewer.isDestroyed?.()) return
  viewer.resize()
  viewer.scene.requestRender()
}

function captureCameraView() {
  if (!viewer?.camera || !window.Cesium) return null
  const Cesium = window.Cesium
  const camera = viewer.camera
  return {
    position: Cesium.Cartesian3.clone(camera.positionWC || camera.position),
    direction: Cesium.Cartesian3.clone(camera.directionWC || camera.direction),
    up: Cesium.Cartesian3.clone(camera.upWC || camera.up),
  }
}

function restoreCameraView(snapshot) {
  if (!viewer?.camera || !snapshot || !window.Cesium) return
  const Cesium = window.Cesium
  viewer.camera.cancelFlight?.()
  viewer.camera.lookAtTransform?.(Cesium.Matrix4.IDENTITY)
  viewer.camera.setView({
    destination: Cesium.Cartesian3.clone(snapshot.position),
    orientation: {
      direction: Cesium.Cartesian3.clone(snapshot.direction),
      up: Cesium.Cartesian3.clone(snapshot.up),
    },
  })
  viewer.scene?.requestRender?.()
}

function scheduleCameraRestore(snapshot) {
  if (!snapshot || typeof window === "undefined") return
  window.requestAnimationFrame?.(() => restoreCameraView(snapshot))
  window.setTimeout?.(() => restoreCameraView(snapshot), 80)
}

function resetView() {
  clearSelectedFeature()
  setCountyView(true)
}

function setCountyView(animated = true) {
  if (!viewer) return
  const Cesium = window.Cesium
  const center = getCountyCenter()
  const range = zoomToAltitude(props.initialZoom)
  const targetOffset = getCountyViewTargetOffset()
  const target = Cesium.Cartesian3.fromDegrees(center[0] + targetOffset.lng, center[1] + targetOffset.lat, 2200)
  const offset = new Cesium.HeadingPitchRange(
    Cesium.Math.toRadians(COUNTY_VIEW_HEADING),
    Cesium.Math.toRadians(COUNTY_VIEW_PITCH),
    range,
  )
  if (animated) {
    viewer.camera.flyToBoundingSphere(new Cesium.BoundingSphere(target, 1), { offset, duration: 0.9 })
  } else {
    viewer.camera.lookAt(target, offset)
    viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY)
  }
  viewer.scene.requestRender()
}

function getCountyViewTargetOffset() {
  const offset = props.countyViewTargetOffset
  const lng = Number(offset?.lng ?? offset?.[0])
  const lat = Number(offset?.lat ?? offset?.[1])
  return {
    lng: Number.isFinite(lng) ? lng : 0,
    lat: Number.isFinite(lat) ? lat : -COUNTY_VIEW_CENTER_LAT_OFFSET,
  }
}

function zoomToAltitude(zoom) {
  const normalized = Number.isFinite(Number(zoom)) ? Number(zoom) : 9
  return Math.max(9000, Math.min(580000, 365000 / Math.pow(1.62, normalized - 9)))
}

function getCountyCenter() {
  const configuredCenter = normalizeLngLat(props.center)
  if (configuredCenter) return configuredCenter
  return [
    (HONGYUAN_BOUNDS.west + HONGYUAN_BOUNDS.east) / 2,
    (HONGYUAN_BOUNDS.south + HONGYUAN_BOUNDS.north) / 2,
  ]
}

function connectResizeObserver() {
  if (!containerRef.value || !window.ResizeObserver) return
  resizeObserver = new ResizeObserver(() => refreshView())
  resizeObserver.observe(containerRef.value)
}

function connectBoundaryVisualScale() {
  if (!viewer || cameraMoveEndRemove) return
  cameraMoveEndRemove = viewer.camera.moveEnd.addEventListener(updateBoundaryVisualScale)
}

function updateBoundaryVisualScale() {
  if (!viewer || viewer.isDestroyed?.()) return
  const height = Number(viewer.camera.positionCartographic?.height) || 320000
  const nearWeight = Math.max(0, Math.min(1, (360000 - height) / 300000))
  const widths = {
    township: 1.1 + nearWeight * 0.35,
    countyOutline: 1.9 + nearWeight * 0.35,
    countyHalo: 4.2 + nearWeight * 0.65,
  }
  boundaryEntities.forEach((entity) => {
    if (entity?.polyline && entity.__hyBoundaryRole === "township") entity.polyline.width = widths.township
  })
  countyFrameEntities.forEach((entity) => {
    if (entity?.polyline && entity.__hyBoundaryRole === "county-outline") entity.polyline.width = widths.countyOutline
    if (entity?.polyline && entity.__hyBoundaryRole === "county-halo") entity.polyline.width = widths.countyHalo
  })
  viewer.scene.requestRender()
}

async function refreshTopicLayers() {
  if (!viewer) return
  const revision = ++layerRevision
  clearTopicLayers()
  const layers = activeLayerStack.value
  if (!layers.length) {
    emit("layer-status", { type: "idle", text: "未选择专题图层" })
    viewer.scene.requestRender()
    return
  }
  emit("layer-status", { type: "loading", text: "正在加载三维专题图层" })
  try {
    await loadCountyBoundary()
    if (revision !== layerRevision || !viewer) return
    for (const layer of layers) {
      if (revision !== layerRevision) return
      await addTopicLayer(layer, revision)
    }
    if (revision !== layerRevision) return
    if (props.autoApplyLayerMapView) applyLayerMapView(layers.find((item) => item?.mapView || item?.cameraPosition))
    emit("layer-status", { type: "success", text: `${layers.map((item) => item.name).filter(Boolean).join("、")}已加载` })
  } catch (error) {
    if (revision !== layerRevision) return
    console.warn("[cesium-map] topic layer failed", error)
    emit("layer-status", { type: "error", text: "三维专题图层加载失败" })
  } finally {
    viewer?.scene?.requestRender?.()
  }
}

async function addTopicLayer(layer, revision) {
  const sourceType = String(layer?.sourceType || "wms").toLowerCase()
  if (["geojson", "wkt-api", "wfs"].includes(sourceType)) {
    const geojson = await loadVectorLayerData(layer)
    if (revision !== layerRevision || !geojson) return
    addFeatureCollection(geojson, layer, topicEntities)
    return
  }
  if (sourceType === "wmts") {
    addWmtsLayer(layer)
    return
  }
  addWmsLayer(layer)
}

function addWmtsLayer(layer) {
  const Cesium = window.Cesium
  const tileMatrixSetID = layer.tileMatrixSetID || layer.tileMatrixSet || "EPSG:900913"
  const maximumLevel = Number.isFinite(Number(layer.maximumLevel)) ? Number(layer.maximumLevel) : 21
  const provider = new Cesium.WebMapTileServiceImageryProvider({
    url: layer.url || "/geoserver/gwc/service/wmts",
    layer: layer.layerName,
    style: layer.style || layer.styles || "",
    format: layer.format || "image/png",
    tileMatrixSetID,
    tileMatrixLabels: layer.tileMatrixLabels || buildWmtsTileMatrixLabels(tileMatrixSetID, maximumLevel),
    tilingScheme: /4326/i.test(tileMatrixSetID)
      ? new Cesium.GeographicTilingScheme()
      : new Cesium.WebMercatorTilingScheme(),
    maximumLevel,
    credit: layer.credit || "",
  })
  const imageryLayer = viewer.imageryLayers.addImageryProvider(provider)
  const isYakRecognition = String(layer.layerName || "").includes("daping_yak_marks")
  imageryLayer.alpha = isYakRecognition
    ? clampOpacity(layer.opacity, 0.94)
    : clampOpacity(layer.opacity, 0.86)
  if (isYakRecognition) applyYakRecognitionImageryStyle(imageryLayer, layer)
  imageryLayer.__hyLayer = layer
  topicImageryLayers.push(imageryLayer)
}

function buildWmtsTileMatrixLabels(tileMatrixSetID, maximumLevel = 21) {
  return Array.from({ length: maximumLevel + 1 }, (_, level) => `${tileMatrixSetID}:${level}`)
}

function addWmsLayer(layer) {
  const Cesium = window.Cesium
  const parameters = {
    service: "WMS",
    transparent: true,
    format: "image/png",
    version: "1.1.1",
    styles: layer.styles || "",
  }
  if (layer.cqlFilter) parameters.cql_filter = layer.cqlFilter
  if (layer.sldBody) parameters.sld_body = layer.sldBody
  const provider = new Cesium.WebMapServiceImageryProvider({
    url: layer.url || "/geoserver/ne/wms",
    layers: layer.layerName,
    parameters,
    getFeatureInfoParameters: {
      ...parameters,
      info_format: "application/json",
      feature_count: 10,
    },
  })
  const imageryLayer = viewer.imageryLayers.addImageryProvider(provider)
  const isYakRecognition = String(layer.layerName || "").includes("daping_yak_marks")
  imageryLayer.alpha = isYakRecognition
    ? clampOpacity(layer.opacity, 0.94)
    : clampOpacity(layer.opacity, 0.86)
  if (isYakRecognition) applyYakRecognitionImageryStyle(imageryLayer, layer)
  imageryLayer.__hyLayer = layer
  topicImageryLayers.push(imageryLayer)
}

function applyYakRecognitionImageryStyle(imageryLayer, layer = {}) {
  imageryLayer.brightness = Number.isFinite(Number(layer.brightness)) ? Number(layer.brightness) : 1.08
  imageryLayer.contrast = Number.isFinite(Number(layer.contrast)) ? Number(layer.contrast) : 1.18
  imageryLayer.saturation = Number.isFinite(Number(layer.saturation)) ? Number(layer.saturation) : 1.42
  imageryLayer.gamma = Number.isFinite(Number(layer.gamma)) ? Number(layer.gamma) : 0.95
}

async function loadVectorLayerData(layer) {
  if (layer.sourceType === "wkt-api") {
    try {
      const response = await fetch(layer.apiUrl, { credentials: "same-origin" })
      if (!response.ok) throw new Error(`HTTP ${response.status}`)
      return normalizeWktPayload(await response.json())
    } catch (error) {
      if (!layer.fallbackPath) throw error
      return fetchJson(layer.fallbackPath)
    }
  }
  if (layer.sourceType === "wfs") {
    const url = buildWfsUrl(layer)
    return fetchJson(url)
  }
  return fetchJson(layer.dataPath || layer.fallbackPath)
}

async function fetchJson(path) {
  if (!path) return null
  const url = /^(https?:)?\/\//.test(path) || path.startsWith("/") ? path : getPublicMapResourcePath(path)
  const response = await fetch(url, { credentials: "same-origin" })
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
  return response.json()
}

function buildWfsUrl(layer) {
  const params = new URLSearchParams({
    service: "WFS",
    version: "1.1.0",
    request: "GetFeature",
    typeName: layer.layerName,
    outputFormat: "application/json",
    srsName: "EPSG:4326",
  })
  if (layer.cqlFilter) params.set("cql_filter", layer.cqlFilter)
  if (layer.maxFeatures) params.set("maxFeatures", String(layer.maxFeatures))
  if (Array.isArray(layer.propertyName) && layer.propertyName.length) params.set("propertyName", layer.propertyName.join(","))
  else if (layer.propertyName) params.set("propertyName", layer.propertyName)
  return `${layer.url || "/geoserver/ne/wfs"}?${params.toString()}`
}

function normalizeImageryFeatureInfo(info, properties = {}, layer = null, lngLat = null) {
  const sourceFeature = info?.data?.type === "Feature"
    ? info.data
    : info?.data?.features?.[0]
      || info?.feature
      || null
  const sourceProperties = sourceFeature?.properties || properties || {}
  const geometry = normalizeGeometry(
    sourceFeature?.geometry
      || sourceProperties.geometry
      || sourceProperties.geom
      || sourceProperties.the_geom
      || sourceProperties.theGeom
      || sourceProperties.wkt
      || sourceProperties.shape
  )
  const id = sourceFeature?.id
    || sourceProperties.id
    || sourceProperties.gid
    || sourceProperties.objectid
    || sourceProperties.OBJECTID
    || info?.name
  return {
    type: "Feature",
    id,
    properties: sourceProperties,
    feature: {
      type: "Feature",
      id,
      properties: sourceProperties,
      geometry,
    },
    geometry,
    layer,
    lngLat,
  }
}

function normalizeWktPayload(payload) {
  const body = payload?.data ?? payload
  const rows = Array.isArray(body)
    ? body
    : body?.rows || body?.list || body?.records || body?.data || []
  return {
    type: "FeatureCollection",
    features: rows.map((row, index) => ({
      type: "Feature",
      id: row?.id ?? index,
      properties: { ...row },
      geometry: normalizeGeometry(row?.geometry) || parseWktGeometry(pickWkt(row)),
    })).filter((feature) => feature.geometry),
  }
}

function pickWkt(row = {}) {
  return row.wkt || row.WKT || row.geom || row.the_geom || row.theGeom || row.shape || row.Shape
}

function addFeatureCollection(data, layer, target) {
  const features = data?.type === "FeatureCollection" ? data.features : data?.type === "Feature" ? [data] : []
  features
    .filter((feature) => feature?.geometry)
    .filter((feature) => matchesActiveLegend(feature.properties || {}, layer))
    .map((feature) => clipFeatureForLayer(feature, layer))
    .filter(Boolean)
    .forEach((feature) => addGeoFeature(feature, layer, target))
}

function clipFeatureForLayer(feature, layer) {
  const geometry = normalizeGeometry(feature?.geometry)
  const shouldClipToCounty = layer?.clipToHongyuanBoundary === true
  const clipBounds = getLayerClipBounds(layer)
  if (!geometry || !["Polygon", "MultiPolygon"].includes(geometry.type)) return feature
  if (!shouldClipToCounty && !clipBounds) return feature

  let polygons = geometry.type === "Polygon" ? [geometry.coordinates] : geometry.coordinates
  try {
    if (shouldClipToCounty && hongyuanCountyClipMultiPolygon.length) {
      polygons = polygonClipping.intersection(polygons, hongyuanCountyClipMultiPolygon)
    }
    if (clipBounds && polygons.length) {
      polygons = polygonClipping.intersection(polygons, [createClipBoundsPolygon(clipBounds)])
    }
  } catch (error) {
    console.warn("[cesium-map] vector layer clipping failed", layer?.key, error)
    return feature
  }
  if (!polygons.length) return null
  return {
    ...feature,
    geometry: {
      type: "MultiPolygon",
      coordinates: polygons,
    },
  }
}

function getLayerClipBounds(layer) {
  if (!layer?.clipBounds || layer.clipToLayerBounds === false) return null
  const bounds = layer.clipBounds
  const west = Math.max(Number(bounds.west), HONGYUAN_BOUNDS.west)
  const south = Math.max(Number(bounds.south), HONGYUAN_BOUNDS.south)
  const east = Math.min(Number(bounds.east), HONGYUAN_BOUNDS.east)
  const north = Math.min(Number(bounds.north), HONGYUAN_BOUNDS.north)
  if (![west, south, east, north].every(Number.isFinite) || west >= east || south >= north) return null
  return { west, south, east, north }
}

function createClipBoundsPolygon(bounds) {
  return [[
    [bounds.west, bounds.south],
    [bounds.east, bounds.south],
    [bounds.east, bounds.north],
    [bounds.west, bounds.north],
    [bounds.west, bounds.south],
  ]]
}

function addGeoFeature(feature, layer, target, overrides = {}) {
  const geometry = normalizeGeometry(feature.geometry)
  if (!geometry) return []
  const style = resolveFeatureStyle(layer, feature.properties || {}, overrides)
  const entities = addGeometryEntities(geometry, feature, layer, style)
  target.push(...entities)
  return entities
}

function addGeometryEntities(geometry, feature, layer, style) {
  const Cesium = window.Cesium
  const created = []
  const add = (options) => {
    const entity = viewer.entities.add(options)
    entity.__hyFeature = feature
    entity.__hyLayer = layer
    created.push(entity)
    return entity
  }
  if (geometry.type === "Point") {
    const point = normalizeLngLat(geometry.coordinates)
    if (!point || layer.hidePointFeatures || layer.hidePointMarkers) return created
    add({
      position: Cesium.Cartesian3.fromDegrees(point[0], point[1], 8),
      point: {
        pixelSize: style.pointSize,
        color: style.fill,
        outlineColor: style.stroke,
        outlineWidth: 2,
        heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
        disableDepthTestDistance: Number.POSITIVE_INFINITY,
      },
      label: layer.showPointLabels ? createPointLabel(feature, style) : undefined,
    })
    return created
  }
  if (geometry.type === "MultiPoint") {
    geometry.coordinates.forEach((coordinates) => created.push(...addGeometryEntities({ type: "Point", coordinates }, feature, layer, style)))
    return created
  }
  if (geometry.type === "LineString") {
    const positions = toCartesianPositions(geometry.coordinates)
    if (positions.length < 2) return created
    add({
      polyline: {
        positions,
        width: style.lineWidth,
        material: new Cesium.PolylineGlowMaterialProperty({ glowPower: 0.16, color: style.stroke }),
        clampToGround: true,
      },
    })
    return created
  }
  if (geometry.type === "MultiLineString") {
    geometry.coordinates.forEach((coordinates) => created.push(...addGeometryEntities({ type: "LineString", coordinates }, feature, layer, style)))
    return created
  }
  if (geometry.type === "Polygon") {
    const hierarchy = createPolygonHierarchy(geometry.coordinates)
    if (!hierarchy) return created
    add({
      polygon: {
        hierarchy,
        height: 0,
        heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
        perPositionHeight: false,
        material: style.fill,
        outline: false,
        classificationType: Cesium.ClassificationType.BOTH,
      },
    })
    addPolygonBoundaryLines(geometry.coordinates, feature, layer, style, created)
    return created
  }
  if (geometry.type === "MultiPolygon") {
    geometry.coordinates.forEach((coordinates) => created.push(...addGeometryEntities({ type: "Polygon", coordinates }, feature, layer, style)))
  }
  return created
}

function addPolygonBoundaryLines(rings, feature, layer, style, created) {
  const Cesium = window.Cesium
  rings.forEach((ring) => {
    const positions = toCartesianPositions(ring)
    if (positions.length < 2) return
    const entity = viewer.entities.add({
      polyline: {
        positions,
        width: style.lineWidth,
        material: style.stroke,
        depthFailMaterial: style.stroke,
        clampToGround: true,
      },
    })
    entity.__hyFeature = feature
    entity.__hyLayer = layer
    created.push(entity)
  })
}

function createPolygonHierarchy(rings = []) {
  const Cesium = window.Cesium
  const outer = toCartesianPositions(rings[0] || [])
  if (outer.length < 3) return null
  const holes = rings.slice(1).map((ring) => {
    const positions = toCartesianPositions(ring)
    return positions.length >= 3 ? new Cesium.PolygonHierarchy(positions) : null
  }).filter(Boolean)
  return new Cesium.PolygonHierarchy(outer, holes)
}

function createPointLabel(feature, style) {
  const Cesium = window.Cesium
  const properties = feature.properties || {}
  const name = properties.name || properties.project_name || properties.title || ""
  if (!name) return undefined
  return {
    text: String(name),
    font: "600 14px PingFang SC, Microsoft YaHei, sans-serif",
    fillColor: Cesium.Color.WHITE,
    outlineColor: Cesium.Color.fromCssColorString("#03121e"),
    outlineWidth: 3,
    style: Cesium.LabelStyle.FILL_AND_OUTLINE,
    pixelOffset: new Cesium.Cartesian2(0, -22),
    heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
    disableDepthTestDistance: Number.POSITIVE_INFINITY,
    distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 150000),
  }
}

function resolveFeatureStyle(layer, properties, overrides = {}) {
  const Cesium = window.Cesium
  const legend = resolveLegend(layer, properties)
  const opacity = clampOpacity(layer.opacity, 0.84)
  const fillText = overrides.fillColor || legend?.fillColor || layer.fillColor || layer.color || "#24F6C4"
  const strokeText = overrides.strokeColor || legend?.strokeColor || layer.strokeColor || layer.color || "#B7FFF2"
  const fillAlpha = overrides.fillAlpha ?? legend?.fillAlpha ?? layer.fillAlpha ?? Math.min(0.34, opacity * 0.34)
  const strokeAlpha = overrides.strokeAlpha ?? legend?.strokeAlpha ?? layer.strokeAlpha ?? Math.max(0.72, opacity)
  const lineWidth = overrides.lineWidth ?? legend?.lineWidth ?? layer.lineWidth ?? Math.max(1.6, 1.8 * Number(props.adminBoundaryLineScale || 1))
  return {
    fill: colorWithAlpha(Cesium, fillText, fillAlpha),
    stroke: colorWithAlpha(Cesium, strokeText, strokeAlpha),
    lineWidth,
    pointSize: overrides.pointSize || 12,
  }
}

function colorWithAlpha(Cesium, value, alpha) {
  try {
    return Cesium.Color.fromCssColorString(String(value || "#24F6C4")).withAlpha(clampOpacity(alpha, 1))
  } catch (error) {
    return Cesium.Color.CYAN.withAlpha(clampOpacity(alpha, 1))
  }
}

function resolveLegend(layer, properties) {
  if (layer?.activeLegend) return layer.activeLegend
  return (layer?.legends || []).find((legend) => matchesLegend(properties, legend)) || layer?.legends?.[0]
}

function matchesActiveLegend(properties, layer) {
  const legend = layer?.activeLegend
  if (!legend) return true
  return matchesLegend(properties, legend)
}

function matchesLegend(properties, legend = {}) {
  if (legend.match) return matchesProperty(properties, legend.match)
  if (Array.isArray(legend.matches)) return legend.matches.some((item) => matchesProperty(properties, item))
  return true
}

function matchesProperty(properties, matcher = {}) {
  return String(properties?.[matcher.field] ?? "") === String(matcher.value ?? "")
}

function addCountyStage() {
  if (!viewer) return
  removeEntities(countyStageEntities)
  const Cesium = window.Cesium
  const center = getCountyCenter()
  const stageMaterial = new Cesium.ImageMaterialProperty({
    image: createStageGlowCanvas().toDataURL("image/png"),
    transparent: true,
  })
  const stageBase = viewer.entities.add({
    position: Cesium.Cartesian3.fromDegrees(center[0], center[1], COUNTY_STAGE_HEIGHT),
    ellipse: {
      semiMajorAxis: 108000,
      semiMinorAxis: 64000,
      rotation: Cesium.Math.toRadians(-23),
      height: COUNTY_STAGE_HEIGHT,
      material: stageMaterial,
      outline: false,
    },
  })
  const orbitStyles = [
    { major: 104000, minor: 62000, height: COUNTY_STAGE_HEIGHT + 1800, width: 1.7, alpha: 0.08, glowPower: 0.14 },
    { major: 84000, minor: 50000, height: COUNTY_STAGE_HEIGHT + 2500, width: 1.2, alpha: 0.07, glowPower: 0.12 },
  ]
  const orbitEntities = orbitStyles.map((style) => viewer.entities.add({
    polyline: {
      positions: makeEllipsePositions(center, style.major, style.minor, -23, style.height, 192),
      width: style.width,
      material: new Cesium.PolylineGlowMaterialProperty({
        glowPower: style.glowPower,
        color: Cesium.Color.fromCssColorString("#39E8FF").withAlpha(style.alpha),
      }),
      clampToGround: false,
    },
  }))
  const arcEntities = [
    { major: 112000, minor: 68000, start: 212, end: 266, alpha: 0.16 },
    { major: 112000, minor: 68000, start: 31, end: 72, alpha: 0.13 },
    { major: 90000, minor: 54000, start: 322, end: 360, alpha: 0.12 },
  ].map((arc) => viewer.entities.add({
    polyline: {
      positions: makeEllipseArcPositions(center, arc.major, arc.minor, -23, arc.start, arc.end, COUNTY_STAGE_HEIGHT + 3500, 64),
      width: 3.4,
      material: new Cesium.PolylineGlowMaterialProperty({
        glowPower: 0.28,
        color: Cesium.Color.fromCssColorString("#7BFFF5").withAlpha(arc.alpha),
      }),
      clampToGround: false,
    },
  }))
  countyStageEntities.push(stageBase, ...orbitEntities, ...arcEntities)
  viewer.scene.requestRender()
}

function createStageGlowCanvas() {
  const canvas = document.createElement("canvas")
  canvas.width = 1024
  canvas.height = 640
  const context = canvas.getContext("2d")
  if (!context) return canvas
  const centerX = canvas.width / 2
  const centerY = canvas.height / 2
  context.clearRect(0, 0, canvas.width, canvas.height)
  const glow = context.createRadialGradient(centerX, centerY, 20, centerX, centerY, canvas.width * 0.48)
  glow.addColorStop(0, "rgba(39, 220, 255, 0.2)")
  glow.addColorStop(0.32, "rgba(18, 156, 203, 0.12)")
  glow.addColorStop(0.68, "rgba(14, 86, 139, 0.08)")
  glow.addColorStop(1, "rgba(8, 30, 58, 0)")
  context.fillStyle = glow
  context.fillRect(0, 0, canvas.width, canvas.height)

  context.save()
  context.translate(centerX, centerY)
  context.scale(1.45, 0.84)
  context.strokeStyle = "rgba(81, 226, 255, 0.2)"
  context.lineWidth = 3
  context.beginPath()
  context.arc(0, 0, 210, 0, Math.PI * 2)
  context.stroke()
  context.strokeStyle = "rgba(81, 226, 255, 0.1)"
  context.lineWidth = 1.3
  for (let radius = 90; radius <= 260; radius += 42) {
    context.beginPath()
    context.arc(0, 0, radius, 0, Math.PI * 2)
    context.stroke()
  }
  context.restore()
  return canvas
}

function makeEllipsePositions(center, semiMajorMeters, semiMinorMeters, rotationDeg, height, segments = 160) {
  return makeEllipseArcPositions(center, semiMajorMeters, semiMinorMeters, rotationDeg, 0, 360, height, segments)
}

function makeEllipseArcPositions(center, semiMajorMeters, semiMinorMeters, rotationDeg, startDeg, endDeg, height, segments = 64) {
  const Cesium = window.Cesium
  const centerLng = Number(center[0])
  const centerLat = Number(center[1])
  const cosLat = Math.max(0.2, Math.cos(Cesium.Math.toRadians(centerLat)))
  const rotation = Cesium.Math.toRadians(rotationDeg)
  const start = Cesium.Math.toRadians(startDeg)
  const end = Cesium.Math.toRadians(endDeg)
  const positions = []
  for (let index = 0; index <= segments; index += 1) {
    const t = start + (end - start) * (index / segments)
    const x = semiMajorMeters * Math.cos(t)
    const y = semiMinorMeters * Math.sin(t)
    const rotatedX = x * Math.cos(rotation) - y * Math.sin(rotation)
    const rotatedY = x * Math.sin(rotation) + y * Math.cos(rotation)
    const lng = centerLng + rotatedX / (111320 * cosLat)
    const lat = centerLat + rotatedY / 110540
    positions.push(Cesium.Cartesian3.fromDegrees(lng, lat, height))
  }
  return positions
}

function addTownshipBoundaries() {
  if (!viewer) return
  removeEntities(boundaryEntities)
  const Cesium = window.Cesium
  const style = {
    stroke: Cesium.Color.fromCssColorString("#B8FFF8").withAlpha(0.82),
    fill: Cesium.Color.TRANSPARENT,
    lineWidth: 1.2,
    pointSize: 0,
  }
  createTownshipInternalBoundarySegments(hongyuanTownshipsGeoJson.features || []).forEach((segment) => {
    const positions = toRaisedCartesianPositions(segment.points, TOWNSHIP_BOUNDARY_RENDER_HEIGHT)
    if (positions.length < 2) return
    const entity = viewer.entities.add({
      polyline: {
        positions,
        width: style.lineWidth,
        material: style.stroke,
        depthFailMaterial: style.stroke,
        clampToGround: false,
      },
    })
    entity.__hyBoundaryRole = "township"
    entity.__hyTownName = segment.townNames?.[0] || ""
    boundaryEntities.push(entity)
  })
  viewer.scene.requestRender()
}

function createTownshipInternalBoundarySegments(features = []) {
  const segmentMap = new Map()
  features.forEach((feature) => {
    const townName = String(feature?.properties?.name || "").trim()
    const geometry = normalizeGeometry(feature?.geometry)
    getGeometryPolygons(geometry).forEach((polygon) => {
      const ring = (polygon?.[0] || []).map(normalizeLngLat).filter(Boolean)
      for (let index = 0; index < ring.length - 1; index += 1) {
        const start = ring[index]
        const end = ring[index + 1]
        if (!start || !end || isSameLngLat(start, end)) continue
        const key = getBoundarySegmentKey(start, end)
        const existing = segmentMap.get(key) || {
          points: [start, end],
          count: 0,
          townNames: new Set(),
        }
        existing.count += 1
        if (townName) existing.townNames.add(townName)
        segmentMap.set(key, existing)
      }
    })
  })
  const internalSegments = Array.from(segmentMap.values())
    .map((segment) => ({
      ...segment,
      townNames: Array.from(segment.townNames),
    }))
    .filter((segment) => segment.count > 1 && segment.townNames.length > 1)
  return mergeBoundarySegments(internalSegments)
}

function getGeometryPolygons(geometry) {
  if (!geometry?.coordinates) return []
  if (geometry.type === "Polygon") return [geometry.coordinates]
  if (geometry.type === "MultiPolygon") return geometry.coordinates
  return []
}

function getBoundarySegmentKey(start, end) {
  const first = getBoundaryPointKey(start)
  const second = getBoundaryPointKey(end)
  return first < second ? `${first}|${second}` : `${second}|${first}`
}

function getBoundaryPointKey(point) {
  return `${Number(point[0]).toFixed(5)},${Number(point[1]).toFixed(5)}`
}

function isSameLngLat(start, end) {
  return Math.abs(Number(start[0]) - Number(end[0])) < 1e-10
    && Math.abs(Number(start[1]) - Number(end[1])) < 1e-10
}

function mergeBoundarySegments(segments = []) {
  const endpointMap = new Map()
  segments.forEach((segment, index) => {
    segment.points.forEach((point) => {
      const key = getBoundaryPointKey(point)
      const indexes = endpointMap.get(key) || []
      indexes.push(index)
      endpointMap.set(key, indexes)
    })
  })
  const used = new Set()
  const merged = []
  segments.forEach((segment, index) => {
    if (used.has(index)) return
    used.add(index)
    const path = [segment.points[0], segment.points[1]]
    extendBoundaryPath(path, endpointMap, segments, used, true)
    extendBoundaryPath(path, endpointMap, segments, used, false)
    merged.push({
      points: path,
      townNames: segment.townNames,
    })
  })
  return merged
}

function extendBoundaryPath(path, endpointMap, segments, used, atStart) {
  while (path.length) {
    const endpoint = atStart ? path[0] : path[path.length - 1]
    const endpointKey = getBoundaryPointKey(endpoint)
    const candidates = (endpointMap.get(endpointKey) || []).filter((index) => !used.has(index))
    if (candidates.length !== 1) return
    const segmentIndex = candidates[0]
    const segment = segments[segmentIndex]
    used.add(segmentIndex)
    const [start, end] = segment.points
    const nextPoint = getBoundaryPointKey(start) === endpointKey ? end : start
    if (atStart) path.unshift(nextPoint)
    else path.push(nextPoint)
  }
}

function addCountyFrame() {
  if (!viewer || !hongyuanCountyClipMultiPolygon.length) return
  removeEntities(countyFrameEntities)
  const Cesium = window.Cesium
  addCountyOutsideMask(Cesium)
  hongyuanCountyClipMultiPolygon.forEach((polygon) => {
    const outerRing = Array.isArray(polygon?.[0]) ? polygon[0] : []
    const ring = outerRing.map(normalizeLngLat).filter(Boolean)
    if (ring.length < 3) return
    const hierarchy = createPolygonHierarchy(polygon)
    if (hierarchy) {
      const topWashEntity = viewer.entities.add({
        polygon: {
          hierarchy,
          material: Cesium.Color.fromCssColorString("#20EAF2").withAlpha(0.026),
          outline: false,
          classificationType: Cesium.ClassificationType.BOTH,
        },
      })
      countyFrameEntities.push(topWashEntity)
    }
    const haloEntity = viewer.entities.add({
      polyline: {
        positions: toRaisedCartesianPositions(ring, COUNTY_BOUNDARY_RENDER_HEIGHT),
        width: 4.8,
        material: new Cesium.PolylineGlowMaterialProperty({
          glowPower: 0.16,
          color: Cesium.Color.fromCssColorString("#78FFF7").withAlpha(0.48),
        }),
        depthFailMaterial: Cesium.Color.fromCssColorString("#78FFF7").withAlpha(0.32),
        clampToGround: false,
        zIndex: 8,
      },
    })
    haloEntity.__hyBoundaryRole = "county-halo"
    const outlineEntity = viewer.entities.add({
      polyline: {
        positions: toRaisedCartesianPositions(ring, COUNTY_BOUNDARY_RENDER_HEIGHT + 80),
        width: 1.9,
        material: Cesium.Color.fromCssColorString("#F2FFFB").withAlpha(0.98),
        depthFailMaterial: Cesium.Color.fromCssColorString("#F2FFFB").withAlpha(0.9),
        clampToGround: false,
        zIndex: 9,
      },
    })
    outlineEntity.__hyBoundaryRole = "county-outline"
    countyFrameEntities.push(haloEntity, outlineEntity)
  })
  viewer.scene.requestRender()
}

function addCountyOutsideMask(Cesium) {
  const outerMask = createCountyOutsideMaskPolygon()
  let maskPolygons = []
  try {
    maskPolygons = polygonClipping.difference([outerMask], hongyuanCountyClipMultiPolygon)
  } catch (error) {
    console.warn("[cesium-map] county outside mask failed", error)
    maskPolygons = [outerMask]
  }
  maskPolygons.forEach((polygon) => {
    const hierarchy = createPolygonHierarchy(polygon)
    if (!hierarchy) return
    const maskEntity = viewer.entities.add({
      polygon: {
        hierarchy,
        material: Cesium.Color.fromCssColorString("#020A10").withAlpha(0.52),
        outline: false,
        perPositionHeight: false,
        heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
        classificationType: Cesium.ClassificationType.BOTH,
        zIndex: 1,
      },
    })
    countyFrameEntities.push(maskEntity)
  })
}

function createCountyOutsideMaskPolygon() {
  const west = HONGYUAN_BOUNDS.west - COUNTY_MASK_PADDING_LNG
  const south = HONGYUAN_BOUNDS.south - COUNTY_MASK_PADDING_LAT
  const east = HONGYUAN_BOUNDS.east + COUNTY_MASK_PADDING_LNG
  const north = HONGYUAN_BOUNDS.north + COUNTY_MASK_PADDING_LAT
  return [[
    [west, south],
    [east, south],
    [east, north],
    [west, north],
    [west, south],
  ]]
}

function getTownBadgeImage(labelLines, highlighted = false) {
  const lines = (Array.isArray(labelLines) ? labelLines : [labelLines])
    .map((line) => String(line || "").trim())
    .filter(Boolean)
    .slice(0, 2)
  const cacheKey = `v15-centered-map-badge:${highlighted ? "h" : "n"}:${lines.join("|")}`
  if (townBadgeImageCache.has(cacheKey)) return townBadgeImageCache.get(cacheKey)

  const canvas = document.createElement("canvas")
  const context = canvas.getContext("2d")
  if (!context) return { image: "", width: 1, height: 1 }
  const pixelRatio = Math.min(4, Math.max(3, window.devicePixelRatio || 2))
  const mainFont = "700 14px Microsoft YaHei, PingFang SC, sans-serif"
  const metricFont = "700 9px Microsoft YaHei, PingFang SC, sans-serif"
  const valueFont = "800 14px Microsoft YaHei, PingFang SC, sans-serif"
  const unitFont = "600 9px Microsoft YaHei, PingFang SC, sans-serif"
  const statParts = parseTownBadgeStatLine(lines[1])
  context.font = mainFont
  const mainWidth = Math.ceil(context.measureText(lines[0] || "").width)
  let statWidth = 0
  if (statParts) {
    context.font = metricFont
    statWidth += Math.ceil(context.measureText(statParts.metric).width)
    context.font = valueFont
    statWidth += Math.ceil(context.measureText(statParts.value).width)
    context.font = unitFont
    statWidth += Math.ceil(context.measureText(statParts.unit).width)
    statWidth += statParts.metric ? 5 : 0
    statWidth += statParts.unit ? 4 : 0
  }
  const bodyWidth = Math.min(118, Math.max(statParts ? 96 : 62, mainWidth + 20, statWidth + 20))
  const bodyHeight = statParts ? 40 : 25
  const pointerHeight = statParts ? 5 : 0
  const margin = 6
  const width = bodyWidth + margin * 2
  const height = bodyHeight + pointerHeight + margin * 2

  canvas.width = Math.ceil(width * pixelRatio)
  canvas.height = Math.ceil(height * pixelRatio)
  canvas.style.width = `${width}px`
  canvas.style.height = `${height}px`
  context.scale(pixelRatio, pixelRatio)
  context.clearRect(0, 0, width, height)

  const x = margin
  const y = margin
  const bodyBottom = y + bodyHeight
  const gradient = context.createLinearGradient(x, y, x, bodyBottom)
  gradient.addColorStop(0, highlighted ? "rgba(8, 58, 70, 0.9)" : "rgba(6, 45, 57, 0.78)")
  gradient.addColorStop(0.55, highlighted ? "rgba(4, 34, 46, 0.88)" : "rgba(3, 29, 41, 0.76)")
  gradient.addColorStop(1, highlighted ? "rgba(2, 22, 32, 0.88)" : "rgba(2, 18, 29, 0.78)")

  context.save()
  context.shadowColor = "rgba(48, 220, 255, 0.13)"
  context.shadowBlur = 6
  context.fillStyle = gradient
  drawBeveledBadgePath(context, x, y, bodyWidth, bodyHeight)
  context.fill()
  context.restore()

  context.save()
  context.globalCompositeOperation = "lighter"
  const sheen = context.createLinearGradient(x, y, x + bodyWidth, y)
  sheen.addColorStop(0, "rgba(42, 210, 255, 0)")
  sheen.addColorStop(0.5, highlighted ? "rgba(98, 246, 255, 0.34)" : "rgba(98, 246, 255, 0.26)")
  sheen.addColorStop(1, "rgba(42, 210, 255, 0)")
  context.strokeStyle = sheen
  context.lineWidth = 1
  context.beginPath()
  context.moveTo(x + 10, y + 2)
  context.lineTo(x + bodyWidth - 10, y + 2)
  context.stroke()
  context.restore()

  context.save()
  context.strokeStyle = highlighted ? "rgba(126, 251, 246, 0.58)" : "rgba(126, 251, 246, 0.46)"
  context.lineWidth = 0.9
  drawBeveledBadgePath(context, x + 0.5, y + 0.5, bodyWidth - 1, bodyHeight - 1)
  context.stroke()
  context.restore()

  const pointerX = x + bodyWidth / 2
  if (pointerHeight > 0) {
    context.save()
    context.shadowColor = "rgba(48, 220, 255, 0.2)"
    context.shadowBlur = 5
    context.beginPath()
    context.moveTo(pointerX - 5, bodyBottom - 1)
    context.lineTo(pointerX + 5, bodyBottom - 1)
    context.lineTo(pointerX, bodyBottom + pointerHeight)
    context.closePath()
    context.fillStyle = highlighted ? "rgba(4, 42, 52, 0.98)" : "rgba(3, 31, 43, 0.96)"
    context.fill()
    context.strokeStyle = highlighted ? "rgba(126, 251, 246, 0.48)" : "rgba(126, 251, 246, 0.34)"
    context.lineWidth = 0.8
    context.stroke()
    context.restore()
  }

  context.save()
  context.textAlign = "center"
  context.textBaseline = "alphabetic"
  context.fillStyle = "rgba(255, 255, 255, 0.98)"
  context.strokeStyle = "rgba(0, 8, 14, 0.98)"
  context.lineJoin = "round"
  context.shadowColor = "rgba(0, 8, 14, 0.72)"
  context.shadowBlur = 0
  context.font = mainFont
  context.lineWidth = 1.6
  const mainTextY = y + (statParts ? 12.5 : bodyHeight / 2)
  drawCenteredCanvasText(context, lines[0] || "", pointerX, mainTextY)
  if (statParts) {
    const statY = y + 29.5
    const totalStatWidth = statWidth
    let statX = pointerX - totalStatWidth / 2
    context.textAlign = "left"
    context.shadowBlur = 0
    context.font = metricFont
    context.lineWidth = 1.2
    context.fillStyle = "rgba(126, 251, 246, 0.9)"
    if (statParts.metric) {
      drawCenteredCanvasText(context, statParts.metric, statX, statY, { stroke: false })
      context.font = metricFont
      statX += Math.ceil(context.measureText(statParts.metric).width) + 5
    }
    context.font = valueFont
    context.lineWidth = 1.4
    context.fillStyle = "#FFFFFF"
    context.shadowColor = "rgba(0, 8, 14, 0.72)"
    context.shadowBlur = 0
    drawCenteredCanvasText(context, statParts.value, statX, statY)
    context.shadowBlur = 0
    statX += Math.ceil(context.measureText(statParts.value).width) + (statParts.unit ? 4 : 0)
    if (statParts.unit) {
      context.font = unitFont
      context.fillStyle = "rgba(232, 252, 255, 0.76)"
      drawCenteredCanvasText(context, statParts.unit, statX, statY + 1, { stroke: false })
    }
  }
  context.restore()

  const badge = {
    image: canvas.toDataURL("image/png"),
    width,
    height,
  }
  townBadgeImageCache.set(cacheKey, badge)
  return badge
}

function drawCenteredCanvasText(context, text, x, centerY, options = {}) {
  const metrics = context.measureText(text)
  const fontSize = getCanvasFontSize(context.font)
  const ascent = Number(metrics.actualBoundingBoxAscent) || fontSize * 0.74
  const descent = Number(metrics.actualBoundingBoxDescent) || fontSize * 0.18
  const baselineY = centerY + (ascent - descent) / 2
  if (options.stroke !== false && context.lineWidth > 0) {
    context.strokeText(text, x, baselineY)
  }
  context.fillText(text, x, baselineY)
}

function getCanvasFontSize(font) {
  const match = String(font || "").match(/(\d+(?:\.\d+)?)px/)
  return match ? Number(match[1]) : 14
}

function parseTownBadgeStatLine(line) {
  const text = String(line || "").trim()
  if (!text) return null
  const spacedMatch = text.match(/^(.+?)\s+([+-]?\d[\d,.]*)(.*)$/u)
  if (spacedMatch) {
    return {
      metric: spacedMatch[1].trim(),
      value: spacedMatch[2].trim(),
      unit: spacedMatch[3].trim(),
    }
  }
  const tailMatch = text.match(/^(.*?)([+-]?\d[\d,.]*)([\u4e00-\u9fa5A-Za-z/%]+)?$/u)
  if (tailMatch) {
    return {
      metric: tailMatch[1].trim(),
      value: tailMatch[2].trim(),
      unit: (tailMatch[3] || "").trim(),
    }
  }
  return {
    metric: "",
    value: text,
    unit: "",
  }
}

function drawBeveledBadgePath(context, x, y, width, height) {
  const cut = Math.min(7, width * 0.12, height * 0.34)
  context.beginPath()
  context.moveTo(x + cut, y)
  context.lineTo(x + width - cut, y)
  context.lineTo(x + width, y + cut)
  context.lineTo(x + width, y + height - cut)
  context.lineTo(x + width - cut, y + height)
  context.lineTo(x + cut, y + height)
  context.lineTo(x, y + height - cut)
  context.lineTo(x, y + cut)
  context.closePath()
}

function refreshMapRanking() {
  if (!viewer) return
  removeEntities(rankingEntities)
  const Cesium = window.Cesium
  const ranking = props.mapRanking || {}
  const rows = Array.isArray(ranking.rows) ? ranking.rows : []
  const rowByName = new Map(rows.map((row) => [normalizeTownName(row.name), row]))
  const showTownNames = ranking.showTownNames !== false && ranking.hideTownNames !== true
  const hideTownLabelStats = ranking.hideTownLabelStats ?? props.hideTownLabelStats
  if (showTownNames) {
    hongyuanTownshipLabelPoints.forEach((town) => {
      const row = rowByName.get(normalizeTownName(town.name))
      const labelLines = [town.name]
      if (!hideTownLabelStats && row && ranking.showMapLabels !== false) {
        const metric = ranking.metricLabel ? `${ranking.metricLabel} ` : ""
        labelLines.push(`${metric}${formatMapValue(row.value)}${row.unit || ranking.unit || ""}`)
      }
      const point = normalizeLngLat(town.center)
      if (!point) return
      const badge = getTownBadgeImage(labelLines, Boolean(row))
      const centerBadge = Boolean(hideTownLabelStats || labelLines.length === 1)
      const entity = viewer.entities.add({
        position: Cesium.Cartesian3.fromDegrees(point[0], point[1], 18),
        billboard: {
          image: badge.image,
          width: badge.width,
          height: badge.height,
          scale: 1,
          verticalOrigin: centerBadge ? Cesium.VerticalOrigin.CENTER : Cesium.VerticalOrigin.BOTTOM,
          horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
          pixelOffset: centerBadge ? new Cesium.Cartesian2(0, 0) : new Cesium.Cartesian2(0, -4),
          heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
          disableDepthTestDistance: Number.POSITIVE_INFINITY,
          scaleByDistance: new Cesium.NearFarScalar(20000, 0.94, 360000, 0.8),
          distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 420000),
        },
      })
      entity.__hyTownName = town.name
      entity.__hyRankingRow = row || null
      rankingEntities.push(entity)
    })
  }

  rows.filter((row) => normalizeRowPoint(row)).forEach((row) => {
    const point = normalizeRowPoint(row)
    const entity = viewer.entities.add({
      position: Cesium.Cartesian3.fromDegrees(point[0], point[1], 16),
      point: {
        pixelSize: 9,
        color: Cesium.Color.fromCssColorString("#35F3FF"),
        outlineColor: Cesium.Color.WHITE,
        outlineWidth: 2,
        heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
        disableDepthTestDistance: Number.POSITIVE_INFINITY,
      },
      label: ranking.showPointMarkers === false ? undefined : {
        text: String(row.name || ""),
        font: "600 12px PingFang SC, Microsoft YaHei, sans-serif",
        fillColor: Cesium.Color.WHITE,
        outlineColor: Cesium.Color.fromCssColorString("#03121e"),
        outlineWidth: 3,
        style: Cesium.LabelStyle.FILL_AND_OUTLINE,
        pixelOffset: new Cesium.Cartesian2(0, -18),
        heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
        disableDepthTestDistance: Number.POSITIVE_INFINITY,
      },
    })
    entity.__hyTownName = row.name
    entity.__hyRankingRow = row
    rankingEntities.push(entity)
  })
  viewer.scene.requestRender()
}

function bindMapClick() {
  if (!viewer) return
  const Cesium = window.Cesium
  clickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
  clickHandler.setInputAction(async (movement) => {
    const canvasPosition = normalizeCanvasPosition(movement.position)
    const cartesian = pickGlobePosition(canvasPosition)
    if (!cartesian) return
    const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
    const lngLat = {
      lng: Cesium.Math.toDegrees(cartographic.longitude),
      lat: Cesium.Math.toDegrees(cartographic.latitude),
      height: cartographic.height,
    }
    const screen = toViewportScreen(canvasPosition)
    if (drawingEnabled) {
      addDrawingPoint(lngLat)
      return
    }

    emit("map-click", {
      lngLat,
      screen,
      overlayLayer: activeLayerStack.value[0] || null,
    })

    const picked = viewer.scene.pick(canvasPosition)
    const pickedEntity = picked?.id
    if (props.enableTownPolygonPick && pickedEntity?.__hyTownName && selectTownByName(pickedEntity.__hyTownName)) return
    if (props.enableTownPolygonPick) {
      const pickedTownFeature = findSelectableTownshipFeatureByLngLat(lngLat)
      if (pickedTownFeature && selectTownByName(pickedTownFeature.properties?.name)) return
    }
    const pickedVectorEntity = findTopicEntityAtLngLat(lngLat, screen) || pickedEntity
    if (pickedVectorEntity?.__hyFeature) {
      selectEntity(pickedVectorEntity)
      emit("feature-info", {
        feature: {
          id: pickedVectorEntity.__hyFeature.id,
          properties: pickedVectorEntity.__hyFeature.properties || {},
          feature: pickedVectorEntity.__hyFeature,
          geometry: pickedVectorEntity.__hyFeature.geometry || null,
          layer: pickedVectorEntity.__hyLayer,
          lngLat,
        },
        screen,
      })
      return
    }

    if (props.enableImageryFeaturePick && topicImageryLayers.length) {
      emit("feature-loading", true)
      try {
        const ray = viewer.camera.getPickRay(canvasPosition)
        const features = await viewer.imageryLayers.pickImageryLayerFeatures(ray, viewer.scene)
        const info = features?.[0]
        if (info) {
          const imageryLayer = topicImageryLayers.find((item) => item.imageryProvider === info.imageryLayer?.imageryProvider)
            || topicImageryLayers[topicImageryLayers.length - 1]
          const layer = imageryLayer?.__hyLayer || activeLayerStack.value[0]
          const properties = info.properties || info.data?.properties || info.data || {}
          const pickedFeature = normalizeImageryFeatureInfo(info, properties, layer, lngLat)
          const highlightedFeature = pickedFeature
          const highlighted = highlightFeature(highlightedFeature)
          if (!highlighted) clearSelectedFeature()
          emit("feature-info", {
            feature: {
              id: highlightedFeature.id || properties.id || properties.fid || info.name,
              properties: highlightedFeature.properties || properties,
              feature: highlightedFeature.feature || info,
              geometry: highlightedFeature.geometry || highlightedFeature.feature?.geometry || null,
              layer,
              lngLat,
            },
            screen,
          })
        } else {
          clearSelectedFeature()
          emit("feature-info", { feature: null, screen })
        }
      } catch (error) {
        clearSelectedFeature()
        emit("feature-info", { feature: null, screen })
      } finally {
        emit("feature-loading", false)
      }
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
}

function normalizeCanvasPosition(position) {
  if (!position || !viewer?.scene?.canvas) return position
  const Cesium = window.Cesium
  const canvas = viewer.scene.canvas
  const rect = canvas.getBoundingClientRect?.()
  const rectWidth = Number(rect?.width) || 0
  const rectHeight = Number(rect?.height) || 0
  const canvasWidth = Number(canvas.clientWidth || containerRef.value?.clientWidth) || rectWidth || 1
  const canvasHeight = Number(canvas.clientHeight || containerRef.value?.clientHeight) || rectHeight || 1
  const x = Number(position.x)
  const y = Number(position.y)
  if (!Number.isFinite(x) || !Number.isFinite(y) || !rectWidth || !rectHeight) {
    return position
  }
  const scaleX = canvasWidth / rectWidth
  const scaleY = canvasHeight / rectHeight
  if (Math.abs(scaleX - 1) < 0.001 && Math.abs(scaleY - 1) < 0.001) {
    return position
  }
  return new Cesium.Cartesian2(x * scaleX, y * scaleY)
}

function toViewportScreen(point) {
  if (!point || !viewer?.scene?.canvas) return null
  const canvas = viewer.scene.canvas
  const rect = canvas.getBoundingClientRect?.()
  const rectWidth = Number(rect?.width) || 0
  const rectHeight = Number(rect?.height) || 0
  const canvasWidth = Number(canvas.clientWidth || containerRef.value?.clientWidth) || rectWidth || 1
  const canvasHeight = Number(canvas.clientHeight || containerRef.value?.clientHeight) || rectHeight || 1
  const x = Number(point.x)
  const y = Number(point.y)
  if (!Number.isFinite(x) || !Number.isFinite(y) || !rect) return null
  return {
    x: (Number(rect.left) || 0) + x * (rectWidth / canvasWidth),
    y: (Number(rect.top) || 0) + y * (rectHeight / canvasHeight),
  }
}

function pickGlobePosition(position) {
  if (!viewer) return null
  const ray = viewer.camera.getPickRay(position)
  return ray ? viewer.scene.globe.pick(ray, viewer.scene) : null
}

function selectEntity(entity) {
  clearSelectedFeature()
  selectedEntity = entity
  if (entity.point) {
    entity.__hyOriginalPointSize = entity.point.pixelSize
    entity.point.pixelSize = 18
  }
  if (entity.__hyFeature?.geometry && ["Polygon", "MultiPolygon", "LineString", "MultiLineString"].includes(entity.__hyFeature.geometry.type)) {
    addSelectedFeatureOverlay(entity.__hyFeature, {
      townName: entity.__hyTownName || entity.__hyFeature?.properties?.name,
      mode: "feature",
    })
  }
  viewer?.scene?.requestRender?.()
}

function clearSelectedFeature() {
  if (selectedEntity?.point && selectedEntity.__hyOriginalPointSize) {
    selectedEntity.point.pixelSize = selectedEntity.__hyOriginalPointSize
  }
  selectedEntity = null
  removeEntities(selectedOverlayEntities)
  clearSelectedOverlayDataSource()
  viewer?.scene?.requestRender?.()
}

function addSelectedFeatureOverlay(feature, options = {}) {
  if (!viewer || !feature?.geometry) return []
  const dataSource = createSelectedOverlayDataSource({ append: options.append === true })
  if (!dataSource) return []
  const Cesium = window.Cesium
  const isTown = options.mode === "town"
  const isWhite = options.highlightTone === "white" || options.mode === "white"
  const fillColor = isTown ? "#30DCFF" : isWhite ? "#FFFFFF" : "#FF2D2D"
  const strokeColor = isTown ? "#F2FFFF" : isWhite ? "#FFFFFF" : "#FF2D2D"
  const style = {
    fill: Cesium.Color.fromCssColorString(fillColor)
      .withAlpha(options.fillAlpha ?? (isTown ? 0.16 : isWhite ? 0.035 : 0.16)),
    stroke: Cesium.Color.fromCssColorString(strokeColor)
      .withAlpha(options.strokeAlpha ?? (isWhite ? 0.9 : 0.98)),
    lineWidth: options.lineWidth ?? (isTown ? 4.2 : isWhite ? 3.8 : 5.2),
    pointSize: isTown ? 0 : options.pointSize ?? 14,
  }
  const entities = addSelectedOverlayGeometryEntities(feature.geometry, feature, dataSource.entities, style)
  entities.forEach((entity) => {
    entity.__hyFeature = null
    entity.__hyLayer = null
    entity.__hyTownName = options.townName || feature.properties?.name || ""
    entity.__hySelectedOverlay = true
  })
  selectedOverlayEntities.push(...entities)
  raiseSelectedOverlayDataSource(dataSource)
  return entities
}

function createSelectedOverlayDataSource(options = {}) {
  if (!viewer || !window.Cesium) return null
  if (options.append && selectedOverlayDataSource) return selectedOverlayDataSource
  clearSelectedOverlayDataSource()
  selectedOverlayDataSource = new window.Cesium.CustomDataSource("selected-feature-overlay")
  viewer.dataSources.add(selectedOverlayDataSource)
  raiseSelectedOverlayDataSource(selectedOverlayDataSource)
  return selectedOverlayDataSource
}

function clearSelectedOverlayDataSource() {
  if (!viewer?.dataSources) {
    selectedOverlayDataSource = null
    return
  }
  const existingSources = Array.from(new Set([
    selectedOverlayDataSource,
    ...(viewer.dataSources.getByName?.("selected-feature-overlay") || []),
  ].filter(Boolean)))
  existingSources.forEach((dataSource) => {
    try {
      viewer.dataSources.remove(dataSource, true)
    } catch (error) {
      console.warn("[cesium-map] remove selected overlay failed", error)
    }
  })
  selectedOverlayDataSource = null
  selectedOverlayEntities = []
}

function raiseSelectedOverlayDataSource(dataSource) {
  if (!viewer?.dataSources || !dataSource) return
  try {
    if (viewer.dataSources.raiseToTop) {
      viewer.dataSources.raiseToTop(dataSource)
      return
    }
    while (viewer.dataSources.indexOf?.(dataSource) < viewer.dataSources.length - 1) {
      viewer.dataSources.raise(dataSource)
    }
  } catch (error) {
    console.warn("[cesium-map] raise selected overlay failed", error)
  }
}

function addSelectedOverlayGeometryEntities(geometry, feature, collection, style) {
  const Cesium = window.Cesium
  const created = []
  if (!geometry || !collection) return created
  const add = (options) => {
    const entity = collection.add(options)
    entity.__hyFeature = feature
    created.push(entity)
    return entity
  }
  if (geometry.type === "Point") {
    const point = normalizeLngLat(geometry.coordinates)
    if (!point || style.pointSize <= 0) return created
    add({
      position: Cesium.Cartesian3.fromDegrees(point[0], point[1]),
      point: {
        show: true,
        color: style.fill,
        pixelSize: style.pointSize,
        outlineColor: style.stroke,
        outlineWidth: 3,
        heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
        disableDepthTestDistance: Number.POSITIVE_INFINITY,
      },
    })
    return created
  }
  if (geometry.type === "MultiPoint") {
    geometry.coordinates.forEach((coordinates) => {
      created.push(...addSelectedOverlayGeometryEntities({ type: "Point", coordinates }, feature, collection, style))
    })
    return created
  }
  if (geometry.type === "LineString") {
    addSelectedOverlayLine(geometry.coordinates, feature, collection, style, created)
    return created
  }
  if (geometry.type === "MultiLineString") {
    geometry.coordinates.forEach((coordinates) => {
      addSelectedOverlayLine(coordinates, feature, collection, style, created)
    })
    return created
  }
  if (geometry.type === "Polygon") {
    addSelectedOverlayPolygon(geometry.coordinates, feature, collection, style, created)
    return created
  }
  if (geometry.type === "MultiPolygon") {
    geometry.coordinates.forEach((coordinates) => {
      addSelectedOverlayPolygon(coordinates, feature, collection, style, created)
    })
  }
  return created
}

function addSelectedOverlayPolygon(rings, feature, collection, style, created) {
  const Cesium = window.Cesium
  const hierarchy = createPolygonHierarchy(rings)
  if (!hierarchy) return
  const polygonEntity = collection.add({
    polygon: {
      hierarchy,
      height: 0,
      heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
      perPositionHeight: false,
      clampToGround: true,
      material: style.fill,
      outline: false,
      classificationType: Cesium.ClassificationType.BOTH,
      zIndex: 999,
    },
  })
  polygonEntity.__hyFeature = feature
  created.push(polygonEntity)
  rings.forEach((ring) => addSelectedOverlayLine(ring, feature, collection, style, created))
}

function addSelectedOverlayLine(coordinates, feature, collection, style, created) {
  const Cesium = window.Cesium
  const positions = toCartesianPositions(coordinates)
  if (positions.length < 2) return
  const entity = collection.add({
    polyline: {
      positions,
      width: style.lineWidth,
      material: style.stroke,
      depthFailMaterial: style.stroke,
      heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
      clampToGround: true,
      classificationType: Cesium.ClassificationType.BOTH,
      zIndex: 1000,
    },
  })
  entity.__hyFeature = feature
  created.push(entity)
}

function highlightFeature(feature, options = {}) {
  if (!viewer || !feature) return false
  const properties = feature.properties || feature.feature?.properties || (typeof feature === "object" ? feature : {})
  const geometry = normalizeGeometry(
    feature.geometry
      || feature.feature?.geometry
      || properties.geometry
      || properties.geom
      || properties.wkt
      || feature
  )
  if (!geometry) return false
  clearSelectedFeature()
  const highlightedFeature = {
    type: "Feature",
    id: feature.id || feature.feature?.id || properties.id,
    properties,
    geometry,
  }
  addSelectedFeatureOverlay(highlightedFeature, { mode: "feature", ...options })
  viewer?.scene?.requestRender?.()
  return selectedOverlayEntities.length > 0
}

function findTownshipFeature(name) {
  const normalized = normalizeTownName(name)
  if (!normalized) return null
  return (hongyuanTownshipsGeoJson.features || []).find((feature) => normalizeTownName(feature?.properties?.name) === normalized) || null
}

function canSelectTownFromMap() {
  return props.mapRanking?.scope === "town" && Array.isArray(props.mapRanking?.rows) && props.mapRanking.rows.length > 0
}

function canSelectTownName(name) {
  if (!canSelectTownFromMap()) return false
  const normalized = normalizeTownName(name)
  return (props.mapRanking?.rows || []).some((row) => normalizeTownName(row.name) === normalized)
}

function findSelectableTownshipFeatureByLngLat(lngLat) {
  if (!canSelectTownFromMap()) return null
  return (hongyuanTownshipsGeoJson.features || []).find((feature) => {
    const name = feature?.properties?.name
    return canSelectTownName(name) && isLngLatInGeometry(lngLat, normalizeGeometry(feature?.geometry))
  }) || null
}

function selectTownByName(name, options = {}) {
  if (!canSelectTownFromMap()) return false
  if (!canSelectTownName(name)) return false
  const feature = findTownshipFeature(name)
  if (!feature) return false
  clearSelectedFeature()
  addSelectedFeatureOverlay(feature, { mode: "town", townName: feature.properties?.name || name })
  if (options.emitFeatureInfo !== false) emit("feature-info", { feature: null })
  if (options.emitStatus !== false) {
    emit("layer-status", {
      type: "success",
      text: `已选中${feature.properties?.name || name}`,
    })
  }
  viewer?.scene?.requestRender?.()
  return true
}

function focusTown(name, options = {}) {
  const town = hongyuanTownshipLabelPoints.find((item) => normalizeTownName(item.name) === normalizeTownName(name))
  const point = normalizeLngLat(town?.center)
  if (!point) return null
  if (options.select) selectTownByName(name, { emitFeatureInfo: false, emitStatus: false })
  flyToPoint(point, 42000)
  return { screen: getLngLatScreen(point) }
}

function focusProject(payload = {}) {
  const query = normalizeProjectFocusPayload(payload)
  const targets = findTopicEntities((feature) => matchesProjectFocusPayload(feature, query))
  if (!targets.length) return null
  const features = uniqueEntityFeatures(targets)
  const highlightFeatures = getProjectHighlightFeatures(features)
  clearSelectedFeature()
  selectedEntity = targets[0]
  if (selectedEntity?.point) {
    selectedEntity.__hyOriginalPointSize = selectedEntity.point.pixelSize
    selectedEntity.point.pixelSize = 18
  }
  highlightFeatures.forEach((feature, index) => {
    addSelectedFeatureOverlay(feature, {
      mode: "feature",
      highlightTone: query.highlightTone,
      append: index > 0,
    })
  })
  const bounds = getFeaturesBounds(features)
  if (bounds) {
    flyToBounds(bounds, 0.9)
  } else {
    viewer.flyTo(targets[0], { duration: 0.9, offset: createFocusOffset(18000) })
  }
  viewer?.scene?.requestRender?.()
  return {
    entity: targets[0],
    entities: targets,
    feature: features[0] || targets[0].__hyFeature,
    features,
    highlightFeatures,
    screen: getVectorFeaturesAnchorScreen(highlightFeatures.length ? highlightFeatures : features),
  }
}

function focusYakIndustryPoint(payload = {}) {
  const point = normalizeRowPoint(payload)
  let target = findTopicEntity((feature) => matchesFeaturePayload(feature, payload))
  if (target) {
    selectEntity(target)
    viewer.flyTo(target, { duration: 0.9, offset: createFocusOffset(16000) })
  } else if (point) {
    flyToPoint(point, 16000)
  }
  return {
    feature: target?.__hyFeature || null,
    screen: getYakIndustryPointScreen(payload),
  }
}

function focusVectorFeature(payload = {}) {
  const target = findTopicEntity((feature) => matchesFeaturePayload(feature, payload))
  if (!target) return null
  selectEntity(target)
  viewer.flyTo(target, { duration: 0.9, offset: createFocusOffset(18000) })
  return {
    feature: target.__hyFeature,
    screen: getVectorFeatureScreen(target.__hyFeature),
  }
}

function getVectorFeatureScreen(feature) {
  const center = getGeometryCenter(normalizeGeometry(feature?.geometry || feature?.feature?.geometry))
  return center ? getLngLatScreen(center) : null
}

function getVectorFeatureAnchorScreen(feature) {
  return getVectorFeaturesAnchorScreen([feature]) || getVectorFeatureScreen(feature)
}

function getVectorFeaturesAnchorScreen(features = []) {
  const screens = []
  ;(Array.isArray(features) ? features : [features]).forEach((feature) => {
    const properties = feature?.properties || feature?.feature?.properties || {}
    const geometry = normalizeGeometry(
      feature?.geometry
        || feature?.feature?.geometry
        || properties.geometry
        || properties.geom
        || properties.wkt
        || properties.geo
        || properties.the_geom
        || properties.geometryWkt
        || properties.geometry_wkt
        || properties.shape
    )
    if (!geometry) {
      const fallback = getVectorFeatureScreen(feature)
      if (fallback) screens.push(fallback)
      return
    }
    const points = []
    collectCoordinates(geometry.coordinates, points)
    screens.push(...points.map(getLngLatScreen).filter(Boolean))
  })
  if (!screens.length) return null
  const xs = screens.map((point) => Number(point.x)).filter(Number.isFinite)
  const ys = screens.map((point) => Number(point.y)).filter(Number.isFinite)
  if (!xs.length || !ys.length) return null
  return {
    x: (Math.min(...xs) + Math.max(...xs)) / 2,
    y: Math.min(...ys),
  }
}

function getYakIndustryPointScreen(payload = {}) {
  const point = normalizeRowPoint(payload)
  if (point) return getLngLatScreen(point)
  const target = findTopicEntity((feature) => matchesFeaturePayload(feature, payload))
  const position = target?.position?.getValue?.(window.Cesium.JulianDate.now())
  return position ? toLogicalScreen(window.Cesium.SceneTransforms.wgs84ToWindowCoordinates(viewer.scene, position)) : null
}

function focusPasture(payload = {}) {
  clearPastureFocus()
  const grasslands = normalizePastureGrasslands(payload.grasslands || payload)
  const whiteTone = payload.highlightTone === "white"
  const glowStyle = whiteTone
    ? null
    : {
        fillColor: "#FF3B30",
        strokeColor: "#FF3B30",
        fillAlpha: 0.1,
        strokeAlpha: 0.68,
        lineWidth: 12,
      }
  const focusStyle = whiteTone
    ? {
        fillColor: "#F7FFFF",
        strokeColor: "#F7FFFF",
        fillAlpha: 0.025,
        strokeAlpha: 0.9,
        lineWidth: 3.8,
      }
    : {
        fillColor: "#FF2D2D",
        strokeColor: "#FFE1E1",
        fillAlpha: 0.3,
        strokeAlpha: 1,
        lineWidth: 6.2,
      }
  if (!payload.locateOnly) {
    grasslands.forEach((feature) => {
      if (glowStyle) addGeoFeature(feature, { key: "pasture-focus-glow", name: payload.name || "牧场高亮" }, focusEntities, glowStyle)
      addGeoFeature(feature, { key: "pasture-focus", name: payload.name || "牧场高亮" }, focusEntities, focusStyle)
    })
    ;(Array.isArray(payload.yakMarks) ? payload.yakMarks : []).forEach((row, index) => {
      const point = normalizeRowPoint(row)
      if (!point) return
      addGeoFeature({
        type: "Feature",
        id: row.id || index,
        properties: { ...row },
        geometry: { type: "Point", coordinates: point },
      }, { key: "pasture-focus-yak", name: "牦牛识别" }, focusEntities, {
        fillColor: "#FFCB45",
        strokeColor: "#FFF4B8",
        pointSize: 11,
      })
    })
  }
  const bounds = getFeaturesBounds(grasslands)
  if (bounds) flyToBounds(bounds, 0.9)
  viewer?.scene?.requestRender?.()
  return Boolean(grasslands.length)
}

function clearPastureFocus() {
  removeEntities(focusEntities)
  viewer?.scene?.requestRender?.()
}

function clearFloatingOverlays() {
  clearSelectedFeature()
}

function setBoundaryDrawing(enabled) {
  drawingEnabled = Boolean(enabled)
  if (!drawingEnabled) drawingPoints = []
  updateDrawingEntities()
  emit("draw-boundary", { status: drawingEnabled ? "drawing" : "clear", pointCount: drawingPoints.length, points: drawingPoints })
}

function addDrawingPoint(lngLat) {
  drawingPoints.push([lngLat.lng, lngLat.lat])
  updateDrawingEntities()
  emit("draw-boundary", { status: "drawing", pointCount: drawingPoints.length, points: drawingPoints })
}

function undoBoundaryPoint() {
  drawingPoints.pop()
  updateDrawingEntities()
  emit("draw-boundary", { status: "drawing", pointCount: drawingPoints.length, points: drawingPoints })
}

function finishBoundaryDrawing() {
  if (drawingPoints.length < 3) return null
  const cameraSnapshot = captureCameraView()
  const ring = [...drawingPoints, drawingPoints[0]]
  const feature = {
    type: "Feature",
    properties: { source: "drawn" },
    geometry: { type: "Polygon", coordinates: [ring] },
  }
  drawingEnabled = false
  drawingPoints = []
  removeEntities(drawingEntities)
  addGeoFeature(feature, { key: "analysis-boundary", name: "分析边界" }, drawingEntities, {
    fillColor: "#24F6C4",
    strokeColor: "#B7FFF2",
    fillAlpha: 0.12,
    strokeAlpha: 1,
    lineWidth: 3,
  })
  emit("draw-boundary", { status: "complete", pointCount: 0, points: [], feature })
  restoreCameraView(cameraSnapshot)
  scheduleCameraRestore(cameraSnapshot)
  return feature
}

function clearAnalysisBoundary() {
  drawingEnabled = false
  drawingPoints = []
  removeEntities(drawingEntities)
  removeEntities(analysisEntities)
  emit("draw-boundary", { status: "clear", pointCount: 0, points: [] })
  viewer?.scene?.requestRender?.()
}

function updateDrawingEntities() {
  if (!viewer) return
  const Cesium = window.Cesium
  removeEntities(drawingEntities)
  drawingPoints.forEach((point) => {
    drawingEntities.push(viewer.entities.add({
      position: Cesium.Cartesian3.fromDegrees(point[0], point[1], 12),
      point: {
        pixelSize: 10,
        color: Cesium.Color.fromCssColorString("#24F6C4"),
        outlineColor: Cesium.Color.WHITE,
        outlineWidth: 2,
        heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
        disableDepthTestDistance: Number.POSITIVE_INFINITY,
      },
    }))
  })
  if (drawingPoints.length > 1) {
    drawingEntities.push(viewer.entities.add({
      polyline: {
        positions: toCartesianPositions(drawingPoints),
        width: 3,
        material: Cesium.Color.fromCssColorString("#24F6C4"),
        clampToGround: true,
      },
    }))
  }
  viewer.scene.requestRender()
}

function showAnalysisResult(payload = {}) {
  const boundary = payload.boundary?.type === "Feature" ? payload.boundary : payload.boundary?.geometry ? payload.boundary : null
  const preserveView = payload.preserveView === true || boundary?.properties?.source === "drawn"
  const cameraSnapshot = preserveView ? captureCameraView() : null
  if (preserveView) viewer?.camera?.cancelFlight?.()
  removeEntities(analysisEntities)
  if (boundary) addGeoFeature(boundary, { key: "analysis-result-boundary" }, analysisEntities, {
    fillColor: "#24F6C4",
    strokeColor: "#B7FFF2",
    fillAlpha: 0.1,
    lineWidth: 3,
  })
  ;(Array.isArray(payload.matches) ? payload.matches : []).forEach((feature) => {
    const normalized = feature?.type === "Feature" ? feature : feature?.feature || null
    if (normalized) addGeoFeature(normalized, { key: "analysis-result-feature" }, analysisEntities, {
      fillColor: "#30DCFF",
      strokeColor: "#C9FBFF",
      fillAlpha: 0.1,
      lineWidth: 2.6,
    })
  })
  if (!preserveView) {
    const features = [boundary, ...(payload.matches || [])].filter(Boolean)
    const bounds = getFeaturesBounds(features)
    if (bounds) flyToBounds(bounds, 0.9)
  } else {
    restoreCameraView(cameraSnapshot)
    scheduleCameraRestore(cameraSnapshot)
  }
  viewer?.scene?.requestRender?.()
}

function flyToPoint(point, altitude = 22000, duration = 0.9) {
  if (!viewer || !point) return
  const Cesium = window.Cesium
  const target = Cesium.Cartesian3.fromDegrees(point[0], point[1], 3600)
  viewer.camera.flyToBoundingSphere(new Cesium.BoundingSphere(target, 1), {
    offset: createFocusOffset(altitude),
    duration,
  })
}

function flyToBounds(bounds, duration = 0.9) {
  if (!viewer || !bounds) return
  const Cesium = window.Cesium
  const center = [
    (Number(bounds.west) + Number(bounds.east)) / 2,
    (Number(bounds.south) + Number(bounds.north)) / 2,
  ]
  if (!center.every(Number.isFinite)) return
  const span = Math.max(
    Math.abs(Number(bounds.east) - Number(bounds.west)),
    Math.abs(Number(bounds.north) - Number(bounds.south)),
  )
  const range = Math.max(18000, Math.min(160000, span * 135000))
  const target = Cesium.Cartesian3.fromDegrees(center[0], center[1], 2600)
  viewer.camera.flyToBoundingSphere(new Cesium.BoundingSphere(target, 1), {
    offset: createFocusOffset(range),
    duration,
  })
}

function applyLayerMapView(layer) {
  if (!viewer || !layer) return
  const view = layer.mapView || (layer.cameraPosition ? { cameraPosition: layer.cameraPosition } : null)
  if (!view) return
  const Cesium = window.Cesium
  const duration = Number.isFinite(Number(view.duration)) ? Number(view.duration) : 0.9
  const targetLngLat = normalizeLngLat(view.targetLngLat)
  if (targetLngLat) {
    const baseRange = zoomToAltitude(props.initialZoom)
    const distanceScale = Number.isFinite(Number(view.distanceScale)) ? Number(view.distanceScale) : 1
    const range = Math.max(18000, Math.min(560000, baseRange * distanceScale))
    const targetHeight = Number.isFinite(Number(view.targetHeight))
      ? Number(view.targetHeight)
      : 2600
    const heading = Number.isFinite(Number(view.heading)) ? Number(view.heading) : DETAIL_VIEW_HEADING
    const pitch = Number.isFinite(Number(view.pitch)) ? Number(view.pitch) : DETAIL_VIEW_PITCH
    const target = Cesium.Cartesian3.fromDegrees(targetLngLat[0], targetLngLat[1], targetHeight)
    viewer.camera.flyToBoundingSphere(new Cesium.BoundingSphere(target, 1), {
      offset: new Cesium.HeadingPitchRange(
        Cesium.Math.toRadians(heading),
        Cesium.Math.toRadians(pitch),
        range,
      ),
      duration,
    })
    return
  }
  if (view.bounds) flyToBounds(view.bounds, duration)
}

function createFocusOffset(range) {
  const Cesium = window.Cesium
  return new Cesium.HeadingPitchRange(
    Cesium.Math.toRadians(DETAIL_VIEW_HEADING),
    Cesium.Math.toRadians(DETAIL_VIEW_PITCH),
    range,
  )
}

function getLngLatScreen(point) {
  if (!viewer || !point) return null
  const Cesium = window.Cesium
  const cartesian = Cesium.Cartesian3.fromDegrees(point[0], point[1], 16)
  return toViewportScreen(Cesium.SceneTransforms.wgs84ToWindowCoordinates(viewer.scene, cartesian))
}

function findTopicEntity(predicate) {
  return [...topicEntities, ...focusEntities].find((entity) => entity.__hyFeature && predicate(entity.__hyFeature)) || null
}

function findTopicEntities(predicate) {
  return [...topicEntities, ...focusEntities].filter((entity) => entity.__hyFeature && predicate(entity.__hyFeature))
}

function findTopicEntityAtLngLat(lngLat, screen) {
  if (!Number.isFinite(lngLat?.lng) || !Number.isFinite(lngLat?.lat)) return null
  const entities = uniqueFeatureEntities(topicEntities)
  const containing = entities
    .map((entity) => ({
      entity,
      geometry: normalizeGeometry(entity.__hyFeature?.geometry),
    }))
    .filter((item) => item.geometry && isLngLatInGeometry(lngLat, item.geometry))
    .sort((a, b) => getGeometryAreaScore(a.geometry) - getGeometryAreaScore(b.geometry))
  if (containing.length) return containing[0].entity
  const nearest = entities
    .map((entity) => ({
      entity,
      distance: getGeometryScreenDistance(normalizeGeometry(entity.__hyFeature?.geometry), screen),
    }))
    .filter((item) => Number.isFinite(item.distance))
    .sort((a, b) => a.distance - b.distance)[0]
  return nearest?.distance <= 18 ? nearest.entity : null
}

function matchesFeaturePayload(feature, payload = {}) {
  const properties = feature?.properties || {}
  const ids = [payload.id, payload.key, payload.featureId, payload.projectId].filter((value) => value !== undefined && value !== null && value !== "").map(String)
  const featureIds = [feature.id, properties.id, properties.key, properties.feature_id, properties.project_id].filter((value) => value !== undefined && value !== null && value !== "").map(String)
  if (ids.some((id) => featureIds.includes(id))) return true
  const names = [payload.name, payload.fullName, payload.projectName].filter(Boolean).map(String)
  const featureNames = [properties.name, properties.fullName, properties.project_name, properties.projectName].filter(Boolean).map(String)
  if (names.some((name) => featureNames.includes(name))) return true
  const point = normalizeRowPoint(payload)
  const featurePoint = normalizeRowPoint({ ...properties, geometry: feature.geometry })
  return Boolean(point && featurePoint && Math.abs(point[0] - featurePoint[0]) < 0.00002 && Math.abs(point[1] - featurePoint[1]) < 0.00002)
}

function normalizeProjectFocusPayload(payload = {}) {
  if (payload && typeof payload === "object") return payload
  return {
    name: payload,
    projectName: payload,
    project_name: payload,
  }
}

function matchesProjectFocusPayload(feature, payload = {}) {
  if (matchesFeaturePayload(feature, payload)) return true
  const properties = feature?.properties || {}
  const ids = collectMatchTexts([payload.id, payload.key, payload.featureId, payload.projectId, payload.project_id])
  const featureIds = collectMatchTexts([feature?.id, properties.id, properties.key, properties.feature_id, properties.project_id, properties.gid])
  if (ids.some((id) => featureIds.includes(id))) return true
  const names = collectMatchTexts([payload.name, payload.fullName, payload.projectName, payload.project_name, payload.title])
  const featureNames = collectMatchTexts([properties.name, properties.fullName, properties.project_name, properties.projectName, properties.title])
  return names.some((name) => featureNames.includes(name))
}

function collectMatchTexts(values = []) {
  const texts = []
  values.forEach((value) => {
    if (value === undefined || value === null || value === "") return
    const text = String(value).trim()
    if (!text) return
    texts.push(text)
    const compact = text.replace(/\s+/g, "")
    if (compact && compact !== text) texts.push(compact)
  })
  return Array.from(new Set(texts))
}

function uniqueFeatureEntities(entities = []) {
  const seen = new Set()
  return entities.filter((entity) => {
    const feature = entity?.__hyFeature
    if (!feature || seen.has(feature)) return false
    seen.add(feature)
    return true
  })
}

function uniqueEntityFeatures(entities = []) {
  return uniqueFeatureEntities(entities).map((entity) => entity.__hyFeature).filter(Boolean)
}

function getProjectHighlightFeatures(features = []) {
  const drawableFeatures = features.filter((feature) => {
    const type = String(normalizeGeometry(feature?.geometry)?.type || "")
    return type.includes("Polygon") || type.includes("LineString")
  })
  const rangeFeatures = drawableFeatures.filter((feature) => feature?.properties?.feature_role === "工程范围")
  return rangeFeatures.length ? rangeFeatures : drawableFeatures
}

function normalizePastureGrasslands(value) {
  const rows = Array.isArray(value)
    ? value
    : value?.grasslands || value?.features || value?.rows || value?.list || (value ? [value] : [])
  return rows.map((row, index) => {
    if (row?.type === "Feature") return row
    const geometry = normalizeGeometry(row?.geometry) || parseWktGeometry(pickWkt(row))
    if (!geometry) return null
    return {
      type: "Feature",
      id: row?.id ?? row?.grasslandId ?? index,
      properties: { ...row },
      geometry,
    }
  }).filter(Boolean)
}

function normalizeGeometry(value) {
  if (!value) return null
  if (value.type === "Feature") return normalizeGeometry(value.geometry)
  if (value.type && Array.isArray(value.coordinates)) return value
  if (typeof value === "string") {
    const text = value.trim()
    if (!text) return null
    if (text.startsWith("{") || text.startsWith("[")) {
      try {
        return normalizeGeometry(JSON.parse(text))
      } catch (error) {
        return null
      }
    }
    return parseWktGeometry(text)
  }
  return null
}

function parseWktGeometry(value) {
  const text = String(value || "").trim().replace(/^SRID=\d+;/i, "")
  const match = text.match(/^([A-Z]+)(?:\s+[A-Z]+)?\s*(\(.+\))$/i)
  if (!match) return null
  const type = match[1].toUpperCase()
  const body = match[2]
  if (type === "POINT") return { type: "Point", coordinates: parseWktPoint(body) }
  if (type === "MULTIPOINT") return { type: "MultiPoint", coordinates: getWktChildGroups(body).map(parseWktPoint).filter(Boolean) }
  if (type === "LINESTRING") return { type: "LineString", coordinates: parseWktLine(body) }
  if (type === "MULTILINESTRING") return { type: "MultiLineString", coordinates: getWktChildGroups(body).map(parseWktLine).filter((line) => line.length) }
  if (type === "POLYGON") return { type: "Polygon", coordinates: parseWktPolygon(body) }
  if (type === "MULTIPOLYGON") return { type: "MultiPolygon", coordinates: getWktChildGroups(body).map(parseWktPolygon).filter((polygon) => polygon.length) }
  return null
}

function parseWktPoint(value) {
  const parts = String(value || "").replace(/^\(+|\)+$/g, "").trim().split(/\s+/).slice(0, 2).map(Number)
  return parts.length === 2 && parts.every(Number.isFinite) ? parts : null
}

function parseWktLine(value) {
  return String(value || "").replace(/^\(+|\)+$/g, "").split(",").map(parseWktPoint).filter(Boolean)
}

function parseWktPolygon(value) {
  return getWktChildGroups(value).map(parseWktLine).filter((ring) => ring.length >= 3)
}

function getWktChildGroups(value) {
  const trimmed = String(value || "").trim()
  const text = trimmed.startsWith("(") && trimmed.endsWith(")") ? trimmed.slice(1, -1) : trimmed
  const groups = []
  let depth = 0
  let start = -1
  Array.from(text).forEach((char, index) => {
    if (char === "(") {
      if (depth === 0) start = index
      depth += 1
    } else if (char === ")") {
      depth -= 1
      if (depth === 0 && start >= 0) {
        groups.push(text.slice(start, index + 1))
        start = -1
      }
    }
  })
  return groups.length ? groups : [text]
}

function toCartesianPositions(coordinates = []) {
  const Cesium = window.Cesium
  return coordinates.map(normalizeLngLat).filter(Boolean).map((point) => Cesium.Cartesian3.fromDegrees(point[0], point[1], 8))
}

function toRaisedCartesianPositions(coordinates = [], height = COUNTY_BOUNDARY_RENDER_HEIGHT) {
  const Cesium = window.Cesium
  return coordinates
    .map(normalizeLngLat)
    .filter(Boolean)
    .map((point) => Cesium.Cartesian3.fromDegrees(point[0], point[1], height))
}

function normalizeLngLat(value) {
  if (!Array.isArray(value) || value.length < 2) return null
  const lng = Number(value[0])
  const lat = Number(value[1])
  return Number.isFinite(lng) && Number.isFinite(lat) ? [lng, lat] : null
}

function normalizeRowPoint(row = {}) {
  const geometry = normalizeGeometry(row.geometry)
  if (geometry?.type === "Point") return normalizeLngLat(geometry.coordinates)
  const candidates = [
    [row.longitude, row.latitude],
    [row.lng, row.lat],
    [row.lon, row.lat],
    [row.x, row.y],
    row.point,
    row.center,
    row.centroid,
    row.coordinates,
  ]
  return candidates.map(normalizeLngLat).find(Boolean) || null
}

function isLngLatInGeometry(lngLat, geometry) {
  if (!lngLat || !geometry?.coordinates) return false
  if (geometry.type === "Polygon") return isLngLatInPolygon(lngLat, geometry.coordinates)
  if (geometry.type === "MultiPolygon") {
    return geometry.coordinates.some((polygon) => isLngLatInPolygon(lngLat, polygon))
  }
  return false
}

function getGeometryAreaScore(geometry) {
  if (!geometry?.coordinates) return Number.POSITIVE_INFINITY
  if (geometry.type === "Polygon") return getLngLatPolygonAreaScore(geometry.coordinates)
  if (geometry.type === "MultiPolygon") {
    return geometry.coordinates.reduce((sum, polygon) => sum + getLngLatPolygonAreaScore(polygon), 0)
  }
  return Number.POSITIVE_INFINITY
}

function getLngLatPolygonAreaScore(rings = []) {
  const outer = rings[0] || []
  if (outer.length < 3) return Number.POSITIVE_INFINITY
  let area = 0
  for (let index = 0; index < outer.length; index += 1) {
    const current = outer[index]
    const next = outer[(index + 1) % outer.length]
    area += Number(current?.[0] || 0) * Number(next?.[1] || 0) - Number(next?.[0] || 0) * Number(current?.[1] || 0)
  }
  return Math.abs(area)
}

function getGeometryScreenDistance(geometry, screen) {
  if (!geometry?.coordinates || !screen) return Number.POSITIVE_INFINITY
  if (geometry.type === "Point") return getPointScreenDistance(geometry.coordinates, screen)
  if (geometry.type === "MultiPoint") {
    return Math.min(...geometry.coordinates.map((point) => getPointScreenDistance(point, screen)))
  }
  if (geometry.type === "LineString") return getLineScreenDistance(geometry.coordinates, screen)
  if (geometry.type === "MultiLineString") {
    return Math.min(...geometry.coordinates.map((line) => getLineScreenDistance(line, screen)))
  }
  return Number.POSITIVE_INFINITY
}

function getPointScreenDistance(coordinate, screen) {
  const point = getLngLatScreen(normalizeLngLat(coordinate))
  if (!point) return Number.POSITIVE_INFINITY
  return Math.hypot(Number(point.x) - Number(screen.x), Number(point.y) - Number(screen.y))
}

function getLineScreenDistance(line = [], screen) {
  if (!Array.isArray(line) || line.length < 2) return Number.POSITIVE_INFINITY
  let distance = Number.POSITIVE_INFINITY
  for (let index = 1; index < line.length; index += 1) {
    const first = getLngLatScreen(normalizeLngLat(line[index - 1]))
    const second = getLngLatScreen(normalizeLngLat(line[index]))
    if (!first || !second) continue
    distance = Math.min(distance, getScreenSegmentDistance(screen, first, second))
  }
  return distance
}

function getScreenSegmentDistance(point, first, second) {
  const x = Number(first.x)
  const y = Number(first.y)
  const dx = Number(second.x) - x
  const dy = Number(second.y) - y
  if (![x, y, dx, dy].every(Number.isFinite)) return Number.POSITIVE_INFINITY
  if (!dx && !dy) return Math.hypot(Number(point.x) - x, Number(point.y) - y)
  const t = Math.max(0, Math.min(1, ((Number(point.x) - x) * dx + (Number(point.y) - y) * dy) / (dx * dx + dy * dy)))
  return Math.hypot(Number(point.x) - (x + t * dx), Number(point.y) - (y + t * dy))
}

function isLngLatInPolygon(lngLat, rings = []) {
  if (!rings.length) return false
  if (!isLngLatInRing(lngLat, rings[0])) return false
  return !rings.slice(1).some((ring) => isLngLatInRing(lngLat, ring))
}

function isLngLatInRing(lngLat, ring = []) {
  if (ring.length < 3) return false
  const x = Number(lngLat.lng)
  const y = Number(lngLat.lat)
  if (!Number.isFinite(x) || !Number.isFinite(y)) return false
  let inside = false
  for (let currentIndex = 0, previousIndex = ring.length - 1; currentIndex < ring.length; previousIndex = currentIndex++) {
    const current = ring[currentIndex]
    const previous = ring[previousIndex]
    const xi = Number(current?.[0])
    const yi = Number(current?.[1])
    const xj = Number(previous?.[0])
    const yj = Number(previous?.[1])
    if (![xi, yi, xj, yj].every(Number.isFinite)) continue
    if (isPointOnSegment(x, y, xi, yi, xj, yj)) return true
    const intersects = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi
    if (intersects) inside = !inside
  }
  return inside
}

function isPointOnSegment(px, py, x1, y1, x2, y2) {
  const cross = (px - x1) * (y2 - y1) - (py - y1) * (x2 - x1)
  if (Math.abs(cross) > 1e-10) return false
  const dot = (px - x1) * (px - x2) + (py - y1) * (py - y2)
  return dot <= 1e-10
}

function getGeometryCenter(geometry) {
  if (!geometry) return null
  if (geometry.type === "Point") return normalizeLngLat(geometry.coordinates)
  const points = []
  collectCoordinates(geometry.coordinates, points)
  if (!points.length) return null
  return [
    points.reduce((sum, point) => sum + point[0], 0) / points.length,
    points.reduce((sum, point) => sum + point[1], 0) / points.length,
  ]
}

function collectCoordinates(value, target) {
  const point = normalizeLngLat(value)
  if (point && !Array.isArray(value[0])) {
    target.push(point)
    return
  }
  if (Array.isArray(value)) value.forEach((item) => collectCoordinates(item, target))
}

function getFeaturesBounds(features = []) {
  const points = []
  features.forEach((feature) => {
    const geometry = normalizeGeometry(feature?.geometry || feature?.feature?.geometry || feature)
    if (geometry) collectCoordinates(geometry.coordinates, points)
  })
  if (!points.length) return null
  const lngs = points.map((point) => point[0])
  const lats = points.map((point) => point[1])
  const paddingLng = Math.max(0.006, (Math.max(...lngs) - Math.min(...lngs)) * 0.18)
  const paddingLat = Math.max(0.006, (Math.max(...lats) - Math.min(...lats)) * 0.18)
  return {
    west: Math.max(HONGYUAN_BOUNDS.west, Math.min(...lngs) - paddingLng),
    south: Math.max(HONGYUAN_BOUNDS.south, Math.min(...lats) - paddingLat),
    east: Math.min(HONGYUAN_BOUNDS.east, Math.max(...lngs) + paddingLng),
    north: Math.min(HONGYUAN_BOUNDS.north, Math.max(...lats) + paddingLat),
  }
}

function removeEntities(target) {
  if (!viewer) {
    target.length = 0
    return
  }
  target.splice(0).forEach((entity) => viewer.entities.remove(entity))
}

function clearTopicLayers() {
  topicImageryLayers.splice(0).forEach((layer) => viewer?.imageryLayers?.remove?.(layer, true))
  removeEntities(topicEntities)
  clearSelectedFeature()
}

function destroyMap() {
  layerRevision += 1
  resizeObserver?.disconnect?.()
  resizeObserver = null
  cameraMoveEndRemove?.()
  cameraMoveEndRemove = null
  clickHandler?.destroy?.()
  clickHandler = null
  clearTopicLayers()
  removeEntities(countyStageEntities)
  removeEntities(countyFrameEntities)
  removeEntities(boundaryEntities)
  removeEntities(rankingEntities)
  removeEntities(focusEntities)
  removeEntities(drawingEntities)
  removeEntities(analysisEntities)
  if (viewer && !viewer.isDestroyed?.()) viewer.destroy()
  viewer = null
  baseImageryLayer = null
  nationalBackdropImageryLayer = null
  canvasMap.value = null
  if (viewerRef.value) viewerRef.value.replaceChildren()
}

function clampOpacity(value, fallback = 1) {
  const number = Number(value)
  return Number.isFinite(number) ? Math.max(0, Math.min(1, number)) : fallback
}

function normalizeTownName(value) {
  return String(value || "").trim().replace(/(镇|乡)$/, "")
}

function formatMapValue(value) {
  const number = Number(value)
  if (!Number.isFinite(number)) return "--"
  return Number.isInteger(number) ? String(number) : String(Number(number.toFixed(2)))
}

defineExpose({
  loadMap,
  hasMap,
  play,
  resume,
  pause,
  refreshView,
  resetView,
  destroyMap,
  highlightFeature,
  clearSelectedFeature,
  clearFloatingOverlays,
  focusProject,
  focusPasture,
  clearPastureFocus,
  focusTown,
  focusYakIndustryPoint,
  focusVectorFeature,
  getVectorFeatureAnchorScreen,
  getVectorFeaturesAnchorScreen,
  getVectorFeatureScreen,
  getYakIndustryPointScreen,
  setBoundaryDrawing,
  finishBoundaryDrawing,
  undoBoundaryPoint,
  clearAnalysisBoundary,
  showAnalysisResult,
  canvasMap,
})
</script>

<style scoped lang="scss">
.cesium-map {
  position: absolute;
  z-index: 1;
  inset: 0;
  overflow: hidden;
  background: #020b12;
}

.cesium-map__viewer,
.cesium-map__viewer :deep(.cesium-viewer),
.cesium-map__viewer :deep(.cesium-viewer-cesiumWidgetContainer),
.cesium-map__viewer :deep(.cesium-widget),
.cesium-map__viewer :deep(canvas) {
  width: 100%;
  height: 100%;
}

.cesium-map__viewer :deep(canvas) {
  display: block;
  outline: none;
}

.cesium-map__viewer :deep(.cesium-viewer-bottom),
.cesium-map__viewer :deep(.cesium-widget-credits) {
  display: none !important;
}
</style>
