Files
Hist/src/scene/CesiumGlobe.js
T

656 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ENTRIES } from '../data/entries.js';
// Cesium 懒加载:脚本首次进入“地理”视图时才加载;用 Proxy 惰性访问 window.Cesium,
// 使下方 50 余处 Cesium.xxx 引用无需逐个改动。
const Cesium = new Proxy({}, {
get(_t, prop) { return window.Cesium[prop]; },
});
const CESIUM_BASE = '/cesium';
function loadCesium() {
return new Promise((resolve, reject) => {
if (window.Cesium) return resolve();
window.CESIUM_BASE_URL = CESIUM_BASE;
const s = document.createElement('script');
s.src = `${CESIUM_BASE}/Cesium.js`;
s.onload = () => resolve();
s.onerror = () => reject(new Error('Cesium 脚本加载失败'));
document.head.appendChild(s);
});
}
// 免费、无需 Ion 令牌的影像与地形源
const IMAGERY_URL =
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}';
const TERRAIN_URL =
'https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer';
// 右键环视角度限制与灵敏度
const MAX_LOOK_ROT = (40 * Math.PI) / 180; // 左右旋转 ±40°
const MAX_LOOK_TILT = (20 * Math.PI) / 180; // 上下倾角 ±20°
const LOOK_RATE = Math.PI / 720; // 每像素约 0.25°
// 计算多边形集合的球面面积(km²),用于按国土面积分级国家名字号
function sphericalAreaKm2(polys) {
let area = 0;
for (let p = 0; p < polys.length; p++) {
const rings = polys[p];
for (let r = 0; r < rings.length; r++) {
const ring = rings[r];
let s = 0;
for (let i = 0; i < ring.length; i++) {
const [lng1, lat1] = ring[i];
const [lng2, lat2] = ring[(i + 1) % ring.length];
let dlng = ((lng2 - lng1) * Math.PI) / 180;
while (dlng > Math.PI) dlng -= 2 * Math.PI;
while (dlng < -Math.PI) dlng += 2 * Math.PI;
const f1 = (lat1 * Math.PI) / 180;
const f2 = (lat2 * Math.PI) / 180;
s += dlng * (2 + Math.sin(f1) + Math.sin(f2));
}
const ringArea = Math.abs(s) / 2;
area += (r === 0 ? 1 : -1) * ringArea;
}
}
const R = 6371; // km
return area * R * R;
}
// 生成便签 canvas:黄底圆角 + 左侧箭头 + 黑色加粗文字(整张图,箭头尖端指向坐标)
// 选中态:先描粗边再填充,得到一条干净的 2px 外描边(四周预留 B 的 padding 防止裁切)
function makeStickyNote(text, fontSize, selected = false) {
const padX = 8, padY = 5, radius = 7, arrowW = 10, arrowH = 14, S = 2, B = 2; // S: 2x 超采样,B: 边框预留
const font = `bold ${fontSize}px "Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif`;
const m = document.createElement('canvas');
const mctx = m.getContext('2d');
mctx.font = font;
const textW = mctx.measureText(text).width;
const bodyW = Math.max(1, Math.ceil(textW + padX * 2));
const w = arrowW + bodyW;
const h = Math.ceil(fontSize + padY * 2);
const cw = w + B * 2;
const ch = h + B * 2;
const c = document.createElement('canvas');
c.width = cw * S;
c.height = ch * S;
const ctx = c.getContext('2d');
ctx.scale(S, S);
const ox = B, oy = B; // 形状整体内缩 B,给黑边留空间
// 箭头 + 圆角矩形一体化路径(箭头尖端在 (ox, oy + h/2),指向坐标)
ctx.beginPath();
ctx.moveTo(ox, oy + h / 2);
ctx.lineTo(ox + arrowW, oy + h / 2 - arrowH / 2);
ctx.lineTo(ox + arrowW, oy + radius);
ctx.arcTo(ox + arrowW, oy, ox + arrowW + radius, oy, radius);
ctx.lineTo(ox + w - radius, oy);
ctx.arcTo(ox + w, oy, ox + w, oy + radius, radius);
ctx.lineTo(ox + w, oy + h - radius);
ctx.arcTo(ox + w, oy + h, ox + w - radius, oy + h, radius);
ctx.lineTo(ox + arrowW + radius, oy + h);
ctx.arcTo(ox + arrowW, oy + h, ox + arrowW, oy + h - radius, radius);
ctx.lineTo(ox + arrowW, oy + h / 2 + arrowH / 2);
ctx.closePath();
if (selected) {
// 先描粗边,再填充盖住内侧一半 → 外露 2px 干净黑边
ctx.strokeStyle = '#14100d';
ctx.lineWidth = B * 2; // 4px,外露 2px
ctx.lineJoin = 'round';
ctx.stroke();
} else {
// 非选中:柔和的投影
ctx.shadowColor = 'rgba(0, 0, 0, 0.20)';
ctx.shadowBlur = 5;
ctx.shadowOffsetY = 2;
}
ctx.fillStyle = '#e0c46a';
ctx.fill();
// 黑色加粗文字
ctx.fillStyle = '#14100d';
ctx.font = font;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText(text, ox + arrowW + bodyW / 2, oy + h / 2 + 1);
return { canvas: c, scale: 1 / S, width: cw, height: ch, tipX: B, tipY: B + h / 2 };
}
export class CesiumGlobe {
constructor({ onPick } = {}) {
this.onPick = onPick;
this.viewer = null;
this.markers = [];
this.selected = -1;
this._created = false;
this._visible = false;
this._pendingData = 2; // 国家边界 + 城市数据
this.ready = new Promise((resolve) => { this._resolveReady = resolve; });
this.viewerReady = new Promise((resolve) => { this._resolveViewerReady = resolve; });
}
async ensureCreated() {
if (this._created) return;
this._created = true;
try {
await loadCesium();
} catch (err) {
this._created = false; // 失败可重试
console.warn('[CesiumGlobe] Cesium 脚本加载失败:', err);
return;
}
// 调整实体渲染顺序:先渲染标签(城市/国家名),再渲染 Billboard(标记便签),
// 让词条标记始终显示在城市/国家名的上层。
Cesium.DataSourceDisplay.defaultVisualizersCallback = function (
scene,
entityCluster,
dataSource,
) {
const entities = dataSource.entities;
return [
new Cesium.LabelVisualizer(entityCluster, entities),
new Cesium.BillboardVisualizer(entityCluster, entities),
new Cesium.GeometryVisualizer(
scene,
entities,
dataSource._primitives,
dataSource._groundPrimitives,
),
new Cesium.ModelVisualizer(scene, entities),
new Cesium.Cesium3DTilesetVisualizer(scene, entities),
new Cesium.PointVisualizer(entityCluster, entities),
new Cesium.PathVisualizer(scene, entities),
new Cesium.PolylineVisualizer(
scene,
entities,
dataSource._primitives,
dataSource._groundPrimitives,
),
];
};
const container = document.getElementById('cesium-container');
this.viewer = new Cesium.Viewer(container, {
animation: false,
timeline: false,
geocoder: false,
homeButton: false,
sceneModePicker: false,
baseLayerPicker: false,
navigationHelpButton: false,
fullscreenButton: false,
infoBox: false,
selectionIndicator: false,
baseLayer: false,
requestRenderMode: true,
});
const viewer = this.viewer;
const scene = viewer.scene;
// 贴合暗色主题
scene.backgroundColor = Cesium.Color.fromCssColorString('#14100d');
scene.skyAtmosphere.show = false;
scene.skyBox.show = false;
scene.sun.show = false;
scene.moon.show = false;
scene.globe.enableLighting = false;
scene.globe.showGroundAtmosphere = false;
scene.globe.baseColor = Cesium.Color.fromCssColorString('#d9e6f0'); // 冰盖浅色:影像/地形未覆盖的南北极区域显示为冰盖,而非黑洞
// 相机交互:限制滚轮缩放范围,避免过近/过远
const controller = scene.screenSpaceCameraController;
controller.enableCollisionDetection = true; // 必须开启:缩放距离限制在关闭时会被忽略
controller.minimumZoomDistance = 10000; // 最近约 10 km(区域尺度)
controller.maximumZoomDistance = 8e6; // 最远约 8000 km
// 限制鼠标按钮:只保留左键旋转 + 滚轮缩放;禁用中键倾斜、右键缩放
controller.rotateEventTypes = Cesium.CameraEventType.LEFT_DRAG;
controller.zoomEventTypes = [
Cesium.CameraEventType.WHEEL,
Cesium.CameraEventType.PINCH,
];
controller.tiltEventTypes = [
Cesium.CameraEventType.PINCH,
{
eventType: Cesium.CameraEventType.LEFT_DRAG,
modifier: Cesium.KeyboardEventModifier.CTRL,
},
];
// 阻止右键弹出浏览器菜单
scene.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
// 精细化渲染:质量与性能的平衡
scene.globe.maximumScreenSpaceError = 2.0; // 默认细分,最省性能
scene.globe.tileCacheSize = 600; // 更大的瓦片缓存,减少细节刷新
scene.fxaa = true; // 开启抗锯齿
viewer.resolutionScale = 1.0; // 关闭超采样(DPR 已提供足够分辨率),省填充率
scene.maximumRenderTimeChange = Infinity; // 配合 requestRenderMode:静止时停止重绘,大幅省电省帧
// 卫星影像
const imagery = new Cesium.UrlTemplateImageryProvider({
url: IMAGERY_URL,
credit: 'Imagery © Esri, Maxar, Earthstar Geographics',
maximumLevel: 19,
});
const imageryLayer = new Cesium.ImageryLayer(imagery, {
// 卫星影像不覆盖极区(约 ±85° 外返回黑块),限制范围让极区用冰盖底色
rectangle: Cesium.Rectangle.fromDegrees(-180, -85, 180, 85),
});
viewer.imageryLayers.add(imageryLayer);
// 真实地形(Esri World Elevation 3D,免费无 token;失败则保持默认椭球)
const terrain = new Cesium.Terrain(
Cesium.ArcGISTiledElevationTerrainProvider.fromUrl(TERRAIN_URL),
);
terrain.errorEvent.addEventListener((err) => {
console.warn('[CesiumGlobe] 地形加载失败,使用椭球:', err);
});
scene.setTerrain(terrain);
viewer.clock.shouldAnimate = false;
this._buildMarkers();
this._setupPick();
this._setupLookAround();
this._loadBoundaries();
this._buildCityLabels();
viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(35, 12, 7.2e6),
});
// 创建完成:同步当前选中/可见/相机状态(首次进入时这些调用可能因 viewer 未建而跳过)
this.setSelected(this.selected);
this.setVisible(this._visible);
this.resize();
this.flyToEntry(this.selected);
// 地球已就绪(影像/地形/城市数据继续后台加载),通知 App 隐藏加载遮罩
if (this._resolveViewerReady) {
this._resolveViewerReady();
this._resolveViewerReady = null;
}
}
_buildMarkers() {
ENTRIES.forEach((entry, i) => {
const position = Cesium.Cartesian3.fromDegrees(entry.lng, entry.lat);
const noteNormal = makeStickyNote(entry.name, 16, false);
const noteSelected = makeStickyNote(entry.name, 16, true);
this.markers.push({
position,
r2: Cesium.Cartesian3.magnitudeSquared(position),
noteNormal,
noteSelected,
index: i,
});
});
this._initMarkerLayer();
}
// 便签用 DOM 覆盖层渲染:始终在城市/国家名上层,且缩放到最近也不会漂移
_initMarkerLayer() {
const container = document.getElementById('cesium-container');
this._markerLayer = document.createElement('div');
this._markerLayer.id = 'marker-layer';
container.appendChild(this._markerLayer);
for (const m of this.markers) {
const el = document.createElement('div');
el.className = 'marker-note';
el.style.width = `${m.noteNormal.width}px`;
el.style.height = `${m.noteNormal.height}px`;
el.dataset.index = m.index;
m.el = el;
m.tipX = m.noteNormal.tipX; // 箭头尖相对元素左上角的偏移
m.tipY = m.noteNormal.tipY;
m.normalUrl = m.noteNormal.canvas.toDataURL();
m.selectedUrl = m.noteSelected.canvas.toDataURL();
el.style.backgroundImage = `url("${m.normalUrl}")`;
el.addEventListener('click', (e) => {
e.stopPropagation();
this.onPick?.(m.index);
});
this._markerLayer.appendChild(el);
}
// 用 postUpdate(每帧都触发,即使 requestRenderMode 下不真正渲染)更新便签位置,
// 避免拖拽时 postRender 不触发导致的闪烁/消失。
this.viewer.scene.postUpdate.addEventListener(() => this._updateMarkerLayer());
}
// 每帧更新便签屏幕位置(相机移动/缩放/飞行时跟随)
_updateMarkerLayer() {
const scene = this.viewer.scene;
const camera = scene.camera;
const camPos = camera.positionWC; // 每帧只取一次相机位置
for (const m of this.markers) {
const el = m.el;
if (!el) continue;
const win = Cesium.SceneTransforms.worldToWindowCoordinates(scene, m.position);
if (!win) { el.style.display = 'none'; continue; }
// 前/后半球:dot(P, C) > |P|²;地平线附近用 frontness 渐变淡出(避免 acos)
const frontness = Cesium.Cartesian3.dot(m.position, camPos) - m.r2;
if (frontness <= 0) { el.style.display = 'none'; continue; }
const opacity = Math.min(1, frontness / (m.r2 * 0.02));
el.style.display = 'block';
el.style.opacity = String(opacity);
el.style.left = `${win.x - m.tipX}px`;
el.style.top = `${win.y - m.tipY}px`;
}
}
_setupPick() {
const viewer = this.viewer;
// 移除 Viewer 默认的单击选中与双击“追踪实体”:否则双击后会锁定相机(定住)
// 标记便签已改为 DOM 覆盖层,点击由 DOM 元素自行处理
viewer.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
viewer.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
}
// 右键环视:以点击处为球心,左右旋转 ±40°、上下倾角 ±20°,松开自动归位。
// 不用原生 tilt(RIGHT_DRAG):原生 tilt 会绕点击点做轨道移动,heading/pitch
// 是相对“不断移动的相机位置”计算的,无法干净地限制成 40°/20°。
_setupLookAround() {
const scene = this.viewer.scene;
const camera = this.viewer.camera;
const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
this._lookState = null;
this._orbitScratch = {
offset: new Cesium.Cartesian3(),
upAxis: new Cesium.Cartesian3(),
eastAxis: new Cesium.Cartesian3(),
qAz: new Cesium.Quaternion(),
qEl: new Cesium.Quaternion(),
q: new Cesium.Quaternion(),
rotMat: new Cesium.Matrix3(),
newOffset: new Cesium.Cartesian3(),
newPos: new Cesium.Cartesian3(),
dir: new Cesium.Cartesian3(),
upProj: new Cesium.Cartesian3(),
};
handler.setInputAction((movement) => {
// 旋转中心 = 相机当前注视的屏幕中心点(与原生 tilt 一致),
// 而不是点击点:用点击点会导致一拖动就“瞬移”到点击处。
const canvas = scene.canvas;
const center = Cesium.Cartesian2.fromElements(
canvas.clientWidth / 2,
canvas.clientHeight / 2,
);
const pivot = camera.pickEllipsoid(center, scene.globe.ellipsoid);
if (!Cesium.defined(pivot)) return;
const sc = this._orbitScratch;
const offset = Cesium.Cartesian3.subtract(camera.position, pivot, sc.offset);
const upAxis = Cesium.Cartesian3.normalize(pivot, sc.upAxis); // 绕它转 = 左右旋转
const eastAxis = Cesium.Cartesian3.cross(Cesium.Cartesian3.UNIT_Z, pivot, sc.eastAxis); // 绕它转 = 上下倾角
if (Cesium.Cartesian3.magnitudeSquared(eastAxis) < 1e-12) {
eastAxis.x = 1;
eastAxis.y = 0;
eastAxis.z = 0; // 极点退化兜底
} else {
Cesium.Cartesian3.normalize(eastAxis, eastAxis);
}
this._lookState = {
pivot: Cesium.Cartesian3.clone(pivot),
startOffset: Cesium.Cartesian3.clone(offset),
upAxis: Cesium.Cartesian3.clone(upAxis),
eastAxis: Cesium.Cartesian3.clone(eastAxis),
position: Cesium.Cartesian3.clone(camera.position),
direction: Cesium.Cartesian3.clone(camera.direction),
up: Cesium.Cartesian3.clone(camera.up),
azimuth: 0,
elevation: 0,
lastX: movement.position.x,
lastY: movement.position.y,
};
}, Cesium.ScreenSpaceEventType.RIGHT_DOWN);
handler.setInputAction((movement) => {
if (!this._lookState) return;
const s = this._lookState;
const dx = movement.endPosition.x - s.lastX;
const dy = movement.endPosition.y - s.lastY;
s.lastX = movement.endPosition.x;
s.lastY = movement.endPosition.y;
s.azimuth = Cesium.Math.clamp(s.azimuth + dx * LOOK_RATE, -MAX_LOOK_ROT, MAX_LOOK_ROT);
s.elevation = Cesium.Math.clamp(s.elevation + dy * LOOK_RATE, -MAX_LOOK_TILT, MAX_LOOK_TILT);
this._orbitCamera(s);
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
handler.setInputAction(() => {
if (!this._lookState) return;
const s = this._lookState;
this._lookState = null;
camera.setView({
destination: s.position,
orientation: { direction: s.direction, up: s.up },
});
}, Cesium.ScreenSpaceEventType.RIGHT_UP);
this._lookHandler = handler;
}
// 按累计角度(已限制)把相机绕点击点旋转;始终保持北朝上、看向点击点
_orbitCamera(s) {
const camera = this.viewer.camera;
const sc = this._orbitScratch;
Cesium.Quaternion.fromAxisAngle(s.upAxis, s.azimuth, sc.qAz);
Cesium.Quaternion.fromAxisAngle(s.eastAxis, s.elevation, sc.qEl);
Cesium.Quaternion.multiply(sc.qEl, sc.qAz, sc.q);
Cesium.Matrix3.fromQuaternion(sc.q, sc.rotMat);
Cesium.Matrix3.multiplyByVector(sc.rotMat, s.startOffset, sc.newOffset);
Cesium.Cartesian3.add(s.pivot, sc.newOffset, sc.newPos);
const dir = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.subtract(s.pivot, sc.newPos, sc.dir),
sc.dir,
);
const z = dir.z;
sc.upProj.x = -dir.x * z;
sc.upProj.y = -dir.y * z;
sc.upProj.z = 1 - z * z;
Cesium.Cartesian3.normalize(sc.upProj, sc.upProj);
camera.setView({
destination: Cesium.Cartesian3.clone(sc.newPos),
orientation: {
direction: Cesium.Cartesian3.clone(dir),
up: Cesium.Cartesian3.clone(sc.upProj),
},
});
}
// 相机平滑飞至某个标记上空并居中:回到与初始一致的高度与北朝上方向,左键拖拽手感不变
flyToEntry(index) {
const entry = ENTRIES[index];
if (!entry || !this.viewer) return;
const now = Date.now();
if (this._lastFly && now - this._lastFly < 400) return; // 双击/连点去抖,避免重复飞
this._lastFly = now;
const camera = this.viewer.camera;
const height = camera.positionCartographic.height; // 保持当前缩放,平移过去
camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(entry.lng, entry.lat, height),
orientation: {
heading: 0,
pitch: Cesium.Math.toRadians(-90),
roll: 0,
},
duration: 1.2,
});
}
// 加载行政区划(国家边界,自然地球 110m):把多边形轮廓转成贴合地形的金色细线 + 国家名标签
async _loadBoundaries() {
try {
const res = await fetch('/world-countries.geojson');
const geojson = await res.json();
const textGold = Cesium.Color.fromCssColorString('#e0c46a');
const black = Cesium.Color.fromCssColorString('#14100d');
const lines = [];
for (const feature of geojson.features || []) {
const props = feature.properties || {};
const geom = feature.geometry;
const isPoly = geom && (geom.type === 'Polygon' || geom.type === 'MultiPolygon');
const polys = isPoly ? (geom.type === 'MultiPolygon' ? geom.coordinates : [geom.coordinates]) : [];
const areaKm2 = isPoly ? sphericalAreaKm2(polys) : 0;
// 国家名(优先中文),落在 NATURAL EARTH 提供的标注点上,字号按国土面积分级
let cname = props.NAME_ZH || props.NAME;
if (cname === '中华民国') cname = '中国台湾';
if (cname && props.LABEL_X != null && props.LABEL_Y != null) {
const fontSize = areaKm2 > 5e6 ? 30 : areaKm2 > 1.5e6 ? 19 : areaKm2 > 3e5 ? 15 : 12;
this.viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(props.LABEL_X, props.LABEL_Y),
label: {
text: cname,
font: `bold ${fontSize}px "Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif`,
fillColor: black,
style: Cesium.LabelStyle.FILL,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(1e6, 1e7),
scaleByDistance: new Cesium.NearFarScalar(1e4, 20 / 15, 6e6, 1.0),
translucencyByDistance: new Cesium.NearFarScalar(9e6, 1.0, 1e7, 0.0),
},
});
}
if (!isPoly) continue;
for (const poly of polys) {
for (const ring of poly) {
if (ring.length < 2) continue;
const flat = [];
for (const [lng, lat] of ring) flat.push(lng, lat);
const positions = Cesium.Cartesian3.fromDegreesArray(flat);
// 行政区划:一根黑线
lines.push(new Cesium.GeometryInstance({ geometry: new Cesium.GroundPolylineGeometry({ positions, width: 2.6 }) }));
}
}
}
if (lines.length) {
this.viewer.scene.groundPrimitives.add(
new Cesium.GroundPolylinePrimitive({
geometryInstances: lines,
appearance: new Cesium.PolylineMaterialAppearance({
material: Cesium.Material.fromType('Color', { color: black }),
}),
}),
);
}
} catch (err) {
console.warn('[CesiumGlobe] 行政区划加载失败:', err);
} finally {
this._dataReady();
}
}
// 全部城市:Natural Earth 10m 人口稠密点(7342 个),中文名直接用数据自带的 name_zh
async _buildCityLabels() {
const scene = this.viewer.scene;
const textGold = Cesium.Color.fromCssColorString('#e0c46a');
const black = Cesium.Color.fromCssColorString('#14100d');
const ddc = new Cesium.DistanceDisplayCondition(0, 4e6);
const tdc = new Cesium.NearFarScalar(2e6, 1.0, 4e6, 0.0);
let cities = [];
try {
const res = await fetch('/world-cities-zh.json');
cities = await res.json();
} catch (err) {
console.warn('[CesiumGlobe] 城市数据加载失败:', err);
cities = [];
}
// 用 primitive 集合替代 7342 个 entity,大幅降低 Cesium 实体系统开销
const points = new Cesium.PointPrimitiveCollection();
const labels = new Cesium.LabelCollection({ scene });
scene.primitives.add(points);
scene.primitives.add(labels);
for (const c of cities) {
const position = Cesium.Cartesian3.fromDegrees(c.lng, c.lat);
points.add({
position,
pixelSize: 3,
color: textGold,
outlineColor: black,
outlineWidth: 1,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
distanceDisplayCondition: ddc,
translucencyByDistance: tdc,
});
labels.add({
position,
text: c.name,
font: 'bold 14px "Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif',
fillColor: black,
style: Cesium.LabelStyle.FILL,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -6),
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
distanceDisplayCondition: ddc,
translucencyByDistance: tdc,
});
}
this._dataReady();
}
_dataReady() {
this._pendingData--;
if (this._pendingData <= 0 && this._resolveReady) {
this._resolveReady();
this._resolveReady = null;
}
}
setSelected(index) {
this.selected = index;
if (!this.viewer) return;
this.markers.forEach((m) => {
const on = m.index === index;
if (m.el) m.el.style.backgroundImage = `url("${on ? m.selectedUrl : m.normalUrl}")`;
});
}
setVisible(visible) {
this._visible = visible;
if (visible && this.viewer) this.viewer.scene.requestRender();
}
// 等待当前视图的影像/地形瓦片加载完成(超时兜底),用于切换时“一起出现”
waitUntilViewReady(timeout = 4000) {
return new Promise((resolve) => {
const globe = this.viewer.scene.globe;
let done = false;
const finish = () => {
if (done) return;
done = true;
globe.tileLoadProgressEvent.removeEventListener(onProgress);
resolve();
};
const onProgress = (remaining) => { if (remaining <= 0) finish(); };
globe.tileLoadProgressEvent.addEventListener(onProgress);
if (globe.tilesLoaded) finish();
setTimeout(finish, timeout);
});
}
resize() {
if (this.viewer) this.viewer.resize();
}
}