You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2357 lines
79 KiB
2357 lines
79 KiB
<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, getPublicMapResourcePath } from "@/config/mapResources"
|
|
import { hongyuanTownshipLabelPoints, hongyuanTownshipsGeoJson } from "@/config/townshipBoundaries"
|
|
import hongyuanCountyOutlineRaw from "/public/assets/json/红原县-轮廓.json?raw"
|
|
|
|
const NATIONAL_BACKDROP_BOUNDS = {
|
|
west: 100.18,
|
|
south: 30.96,
|
|
east: 105.02,
|
|
north: 34.46,
|
|
}
|
|
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 HONGYUAN_CLIP_MULTIPOLYGON = parseClipMultiPolygon(hongyuanCountyOutlineRaw)
|
|
|
|
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://qkl-map.oss-cn-chengdu.aliyuncs.com/hy-result/{z}/{x}/{y}.png",
|
|
},
|
|
terrainUrl: {
|
|
type: String,
|
|
default: "/terrain",
|
|
},
|
|
center: {
|
|
type: Array,
|
|
default: () => [102.600005, 32.585593],
|
|
},
|
|
initialZoom: {
|
|
type: Number,
|
|
default: 9,
|
|
},
|
|
maxZoom: {
|
|
type: Number,
|
|
default: 21,
|
|
},
|
|
adminBoundaryLineScale: {
|
|
type: Number,
|
|
default: 1,
|
|
},
|
|
showNationalBackdrop: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
})
|
|
|
|
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 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.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 referenceProvider =
|
|
normalizedReferenceTileUrlTemplate && normalizedReferenceTileUrlTemplate !== normalizedTileUrlTemplate
|
|
? createImageryProvider(Cesium, normalizedReferenceTileUrlTemplate, {
|
|
maximumLevel: Math.min(props.maxZoom, 18),
|
|
})
|
|
: null
|
|
const baseProvider = normalizedTileUrlTemplate
|
|
? createImageryProvider(Cesium, normalizedTileUrlTemplate, { maximumLevel: props.maxZoom })
|
|
: 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,
|
|
requestRenderMode: true,
|
|
maximumRenderTimeChange: Infinity,
|
|
contextOptions: {
|
|
webgl: {
|
|
alpha: true,
|
|
antialias: true,
|
|
},
|
|
},
|
|
})
|
|
|
|
if (referenceProvider) {
|
|
const referenceLayer = viewer.imageryLayers.addImageryProvider(referenceProvider)
|
|
referenceLayer.brightness = 0.92
|
|
referenceLayer.contrast = 1.08
|
|
referenceLayer.saturation = 1.04
|
|
referenceLayer.gamma = 0.98
|
|
}
|
|
if (baseProvider) {
|
|
baseImageryLayer = viewer.imageryLayers.addImageryProvider(baseProvider)
|
|
baseImageryLayer.alpha = isHongyuanHighResolutionTile(normalizedTileUrlTemplate) ? 0.82 : 1
|
|
baseImageryLayer.brightness = 0.9
|
|
baseImageryLayer.contrast = 1.1
|
|
baseImageryLayer.saturation = 1.05
|
|
baseImageryLayer.gamma = 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.scene.fxaa = true
|
|
viewer.scene.postProcessStages.fxaa.enabled = true
|
|
viewer.scene.screenSpaceCameraController.enableCollisionDetection = true
|
|
viewer.scene.screenSpaceCameraController.minimumZoomDistance = 350
|
|
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)
|
|
addCountyFrame()
|
|
addTownshipBoundaries()
|
|
bindMapClick()
|
|
connectResizeObserver()
|
|
connectBoundaryVisualScale()
|
|
setCountyView(false)
|
|
updateBoundaryVisualScale()
|
|
refreshTopicLayers()
|
|
refreshMapRanking()
|
|
|
|
const facade = {
|
|
viewer,
|
|
clearSelectedFeature,
|
|
focusProject,
|
|
focusPasture,
|
|
focusTownByName: focusTown,
|
|
focusYakIndustryPoint,
|
|
focusVectorFeature,
|
|
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,
|
|
tilingScheme: new Cesium.WebMercatorTilingScheme(),
|
|
credit: "",
|
|
})
|
|
}
|
|
|
|
function normalizeTileUrlTemplate(url) {
|
|
return String(url || "").trim()
|
|
}
|
|
|
|
function isHongyuanHighResolutionTile(url) {
|
|
return /hy-result|qkl-map/i.test(String(url || ""))
|
|
}
|
|
|
|
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 parseClipMultiPolygon(raw) {
|
|
try {
|
|
const data = typeof raw === "string" ? JSON.parse(raw) : raw
|
|
const geometry = data?.type === "FeatureCollection"
|
|
? data.features?.[0]?.geometry
|
|
: data?.type === "Feature"
|
|
? data.geometry
|
|
: data
|
|
if (geometry?.type === "Polygon") return [geometry.coordinates]
|
|
if (geometry?.type === "MultiPolygon") return geometry.coordinates
|
|
} catch (error) {
|
|
console.warn("[cesium-map] county clip boundary failed", error)
|
|
}
|
|
return []
|
|
}
|
|
|
|
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 resetView() {
|
|
clearSelectedFeature()
|
|
setCountyView(true)
|
|
}
|
|
|
|
function setCountyView(animated = true) {
|
|
if (!viewer) return
|
|
const Cesium = window.Cesium
|
|
const center = getCountyCenter()
|
|
const range = zoomToAltitude(props.initialZoom)
|
|
const target = Cesium.Cartesian3.fromDegrees(center[0], center[1] - COUNTY_VIEW_CENTER_LAT_OFFSET, 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 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() {
|
|
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.25 + nearWeight * 0.55,
|
|
countyOutline: 1.35 + nearWeight * 0.45,
|
|
countyHalo: 2.25 + nearWeight * 0.7,
|
|
}
|
|
boundaryEntities.forEach((entity) => {
|
|
if (entity?.polyline && entity.__hyBoundaryRole === "township") entity.polyline.width = widths.township
|
|
})
|
|
countyFrameEntities.forEach((entity) => {
|
|
if (!entity?.polyline) return
|
|
if (entity.__hyBoundaryRole === "county-outline") entity.polyline.width = widths.countyOutline
|
|
if (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 {
|
|
for (const layer of layers) {
|
|
if (revision !== layerRevision) return
|
|
await addTopicLayer(layer, revision)
|
|
}
|
|
if (revision !== layerRevision) return
|
|
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.92)
|
|
: clampOpacity(layer.opacity, 0.86)
|
|
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.92)
|
|
: clampOpacity(layer.opacity, 0.86)
|
|
imageryLayer.__hyLayer = layer
|
|
topicImageryLayers.push(imageryLayer)
|
|
}
|
|
|
|
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 : `/${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 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 && HONGYUAN_CLIP_MULTIPOLYGON.length) {
|
|
polygons = polygonClipping.intersection(polygons, HONGYUAN_CLIP_MULTIPOLYGON)
|
|
}
|
|
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,
|
|
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"
|
|
return {
|
|
fill: colorWithAlpha(Cesium, fillText, overrides.fillAlpha ?? Math.min(0.34, opacity * 0.34)),
|
|
stroke: colorWithAlpha(Cesium, strokeText, overrides.strokeAlpha ?? Math.max(0.72, opacity)),
|
|
lineWidth: overrides.lineWidth || Math.max(1.6, 1.8 * Number(props.adminBoundaryLineScale || 1)),
|
|
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.94),
|
|
fill: Cesium.Color.TRANSPARENT,
|
|
lineWidth: 1.65,
|
|
pointSize: 0,
|
|
}
|
|
;(hongyuanTownshipsGeoJson.features || []).forEach((feature) => {
|
|
const geometry = normalizeGeometry(feature.geometry)
|
|
if (!geometry) return
|
|
const entities = addGeometryEntities(geometry, feature, { key: "township-boundary", name: "乡镇边界", hidePointFeatures: true }, style)
|
|
entities.forEach((entity) => {
|
|
if (entity.polygon) viewer.entities.remove(entity)
|
|
if (entity.polyline) {
|
|
entity.__hyBoundaryRole = "township"
|
|
entity.__hyTownName = feature.properties?.name
|
|
}
|
|
})
|
|
boundaryEntities.push(...entities.filter((entity) => !entity.isDestroyed?.() && viewer.entities.contains(entity)))
|
|
})
|
|
viewer.scene.requestRender()
|
|
}
|
|
|
|
function addCountyFrame() {
|
|
if (!viewer || !HONGYUAN_CLIP_MULTIPOLYGON.length) return
|
|
removeEntities(countyFrameEntities)
|
|
const Cesium = window.Cesium
|
|
addCountyOutsideMask(Cesium)
|
|
HONGYUAN_CLIP_MULTIPOLYGON.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: toCartesianPositions(ring),
|
|
width: 2.8,
|
|
material: new Cesium.PolylineGlowMaterialProperty({
|
|
glowPower: 0.18,
|
|
color: Cesium.Color.fromCssColorString("#00E7FF").withAlpha(0.24),
|
|
}),
|
|
depthFailMaterial: Cesium.Color.fromCssColorString("#00E7FF").withAlpha(0.16),
|
|
clampToGround: true,
|
|
},
|
|
})
|
|
haloEntity.__hyBoundaryRole = "county-halo"
|
|
const outlineEntity = viewer.entities.add({
|
|
polyline: {
|
|
positions: toCartesianPositions(ring),
|
|
width: 1.45,
|
|
material: Cesium.Color.fromCssColorString("#8FFFF7").withAlpha(0.9),
|
|
depthFailMaterial: Cesium.Color.fromCssColorString("#8FFFF7").withAlpha(0.9),
|
|
clampToGround: true,
|
|
},
|
|
})
|
|
outlineEntity.__hyBoundaryRole = "county-outline"
|
|
countyFrameEntities.push(haloEntity, outlineEntity)
|
|
})
|
|
viewer.scene.requestRender()
|
|
}
|
|
|
|
function addCountyOutsideMask(Cesium) {
|
|
const outerMask = createCountyOutsideMaskPolygon()
|
|
let maskPolygons = []
|
|
try {
|
|
maskPolygons = polygonClipping.difference([outerMask], HONGYUAN_CLIP_MULTIPOLYGON)
|
|
} 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.64),
|
|
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 = `v13-lightweight-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(3, Math.max(2.5, window.devicePixelRatio || 2))
|
|
const mainFont = "760 13px 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 = 5
|
|
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
|
|
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(126, 251, 246, 0.34)"
|
|
context.shadowBlur = 2
|
|
context.font = mainFont
|
|
context.lineWidth = 2
|
|
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(48, 220, 255, 0.5)"
|
|
context.shadowBlur = 5
|
|
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
|
|
if (showTownNames) {
|
|
hongyuanTownshipLabelPoints.forEach((town) => {
|
|
const row = rowByName.get(normalizeTownName(town.name))
|
|
const labelLines = [town.name]
|
|
if (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 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: Cesium.VerticalOrigin.BOTTOM,
|
|
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
|
pixelOffset: 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 cartesian = pickGlobePosition(movement.position)
|
|
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 = toLogicalScreen(movement.position)
|
|
if (drawingEnabled) {
|
|
addDrawingPoint(lngLat)
|
|
return
|
|
}
|
|
|
|
emit("map-click", {
|
|
lngLat,
|
|
screen,
|
|
overlayLayer: activeLayerStack.value[0] || null,
|
|
})
|
|
|
|
const picked = viewer.scene.pick(movement.position)
|
|
const pickedEntity = picked?.id
|
|
if (pickedEntity?.__hyTownName && selectTownByName(pickedEntity.__hyTownName)) return
|
|
const pickedTownFeature = findSelectableTownshipFeatureByLngLat(lngLat)
|
|
if (pickedTownFeature && selectTownByName(pickedTownFeature.properties?.name)) return
|
|
if (pickedEntity?.__hyFeature) {
|
|
selectEntity(pickedEntity)
|
|
emit("feature-info", {
|
|
feature: {
|
|
id: pickedEntity.__hyFeature.id,
|
|
properties: pickedEntity.__hyFeature.properties || {},
|
|
feature: pickedEntity.__hyFeature,
|
|
layer: pickedEntity.__hyLayer,
|
|
lngLat,
|
|
},
|
|
screen,
|
|
})
|
|
return
|
|
}
|
|
|
|
if (topicImageryLayers.length) {
|
|
emit("feature-loading", true)
|
|
try {
|
|
const ray = viewer.camera.getPickRay(movement.position)
|
|
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 properties = info.properties || info.data?.properties || info.data || {}
|
|
emit("feature-info", {
|
|
feature: {
|
|
id: properties.id || properties.fid || info.name,
|
|
properties,
|
|
feature: info,
|
|
layer: imageryLayer?.__hyLayer || activeLayerStack.value[0],
|
|
lngLat,
|
|
},
|
|
screen,
|
|
})
|
|
} else {
|
|
emit("feature-info", { feature: null, screen })
|
|
}
|
|
} catch (error) {
|
|
emit("feature-info", { feature: null, screen })
|
|
} finally {
|
|
emit("feature-loading", false)
|
|
}
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
|
|
}
|
|
|
|
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)
|
|
viewer?.scene?.requestRender?.()
|
|
}
|
|
|
|
function addSelectedFeatureOverlay(feature, options = {}) {
|
|
if (!viewer || !feature?.geometry) return []
|
|
const entities = addGeoFeature(feature, { key: "selected-feature-overlay", opacity: 1 }, selectedOverlayEntities, {
|
|
fillColor: options.mode === "town" ? "#30DCFF" : "#FF3B4D",
|
|
strokeColor: options.mode === "town" ? "#F2FFFF" : "#FFECEC",
|
|
fillAlpha: options.mode === "town" ? 0.16 : 0.2,
|
|
strokeAlpha: 0.98,
|
|
lineWidth: options.mode === "town" ? 4.2 : 4.8,
|
|
pointSize: 0,
|
|
})
|
|
entities.forEach((entity) => {
|
|
entity.__hyFeature = null
|
|
entity.__hyLayer = null
|
|
entity.__hyTownName = options.townName || feature.properties?.name || ""
|
|
entity.__hySelectedOverlay = true
|
|
if (entity.polyline) {
|
|
entity.polyline.width = options.mode === "town" ? 4.2 : 4.8
|
|
entity.polyline.clampToGround = true
|
|
}
|
|
if (entity.polygon) {
|
|
entity.polygon.classificationType = window.Cesium.ClassificationType.BOTH
|
|
}
|
|
})
|
|
return entities
|
|
}
|
|
|
|
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(name) {
|
|
const target = findTopicEntity((feature) => {
|
|
const properties = feature.properties || {}
|
|
return [properties.name, properties.project_name, properties.projectName, properties.title].some((value) => String(value || "") === String(name || ""))
|
|
})
|
|
if (!target) return null
|
|
viewer.flyTo(target, { duration: 0.9, offset: createFocusOffset(18000) })
|
|
selectEntity(target)
|
|
return target
|
|
}
|
|
|
|
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 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)
|
|
if (!payload.locateOnly) {
|
|
grasslands.forEach((feature) => {
|
|
addGeoFeature(feature, { key: "pasture-focus-glow", name: payload.name || "选中草场" }, focusEntities, {
|
|
fillColor: "#30DCFF",
|
|
strokeColor: "#30DCFF",
|
|
fillAlpha: 0.1,
|
|
strokeAlpha: 0.58,
|
|
lineWidth: 8.2,
|
|
})
|
|
addGeoFeature(feature, { key: "pasture-focus", name: payload.name || "选中草场" }, focusEntities, {
|
|
fillColor: "#FFCB45",
|
|
strokeColor: "#FFF4B8",
|
|
fillAlpha: 0.18,
|
|
strokeAlpha: 1,
|
|
lineWidth: 4.6,
|
|
})
|
|
})
|
|
;(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 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 })
|
|
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 = {}) {
|
|
removeEntities(analysisEntities)
|
|
const boundary = payload.boundary?.type === "Feature" ? payload.boundary : payload.boundary?.geometry ? payload.boundary : null
|
|
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 (!payload.preserveView) {
|
|
const features = [boundary, ...(payload.matches || [])].filter(Boolean)
|
|
const bounds = getFeaturesBounds(features)
|
|
if (bounds) flyToBounds(bounds, 0.9)
|
|
}
|
|
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 toLogicalScreen(Cesium.SceneTransforms.wgs84ToWindowCoordinates(viewer.scene, cartesian))
|
|
}
|
|
|
|
function toLogicalScreen(point) {
|
|
if (!point || !containerRef.value) return null
|
|
const width = Math.max(1, containerRef.value.clientWidth)
|
|
const height = Math.max(1, containerRef.value.clientHeight)
|
|
return {
|
|
x: (Number(point.x) / width) * 1920,
|
|
y: (Number(point.y) / height) * 1080,
|
|
}
|
|
}
|
|
|
|
function findTopicEntity(predicate) {
|
|
return [...topicEntities, ...focusEntities].find((entity) => entity.__hyFeature && predicate(entity.__hyFeature)) || 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 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 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 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,
|
|
clearSelectedFeature,
|
|
clearFloatingOverlays,
|
|
focusProject,
|
|
focusPasture,
|
|
clearPastureFocus,
|
|
focusTown,
|
|
focusYakIndustryPoint,
|
|
focusVectorFeature,
|
|
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>
|
|
|