Files
Hist/src/scene/CesiumGlobe.js
T

709 lines
26 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';
// 计算多边形集合的球面面积(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 = 4, arrowW = 13, arrowH = 16, 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.quadraticCurveTo(
ox + arrowW * 0.7, oy + h / 2 - arrowH / 2,
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.quadraticCurveTo(
ox + arrowW * 0.7, oy + h / 2 + arrowH / 2,
ox, oy + h / 2,
);
ctx.closePath();
if (selected) {
// 选中:黑底金字,外圈 2px 金边(先描边再填充,外露一半)
ctx.strokeStyle = '#e0c46a';
ctx.lineWidth = B * 2;
ctx.lineJoin = 'round';
ctx.stroke();
ctx.fillStyle = '#14100d';
ctx.fill();
ctx.fillStyle = '#e0c46a';
} else {
// 非选中:金底黑字 + 1px 黑边(描 2px,填充盖掉内侧 1px)+ 柔和投影
ctx.strokeStyle = '#14100d';
ctx.lineWidth = 2;
ctx.lineJoin = 'round';
ctx.stroke();
ctx.shadowColor = 'rgba(0, 0, 0, 0.20)';
ctx.shadowBlur = 5;
ctx.shadowOffsetY = 2;
ctx.fillStyle = '#e0c46a';
ctx.fill();
ctx.shadowColor = 'transparent';
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 = 500; // 最近约 500 m(街区尺度)
controller.maximumZoomDistance = 8e6; // 最远约 8000 km
// 限制鼠标按钮:只保留左键旋转 + 滚轮缩放;禁用中键倾斜、右键缩放
controller.rotateEventTypes = Cesium.CameraEventType.LEFT_DRAG;
controller.zoomEventTypes = [
Cesium.CameraEventType.WHEEL,
Cesium.CameraEventType.PINCH,
];
controller.tiltEventTypes = [
Cesium.CameraEventType.RIGHT_DRAG, // 右键 = 原生 tilt(传统环视)
Cesium.CameraEventType.PINCH,
{
eventType: Cesium.CameraEventType.LEFT_DRAG,
modifier: Cesium.KeyboardEventModifier.CTRL,
},
];
// 阻止右键弹出浏览器菜单;阻止中键触发浏览器自动滚动
scene.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
scene.canvas.addEventListener('mousedown', (e) => { if (e.button === 1) e.preventDefault(); });
// 精细化渲染:高清 + 精细地形
scene.globe.maximumScreenSpaceError = 2.0; // 地形细分
scene.globe.tileCacheSize = 600; // 更大的瓦片缓存,减少细节刷新
scene.fxaa = true; // 后处理抗锯齿(MSAA 默认已 4x)
viewer.useBrowserRecommendedResolution = false; // 按设备像素比渲染,高分屏清晰
viewer.resolutionScale = 1.0;
scene.maximumRenderTimeChange = Infinity; // 配合 requestRenderMode:静止时停止重绘,大幅省电省帧
// 卫星影像
// 检测 ESRI "Map data not yet available" 灰色错误瓦片并丢弃,自动回退父级放大
const discardNotAvailableTile = {
_canvas: document.createElement('canvas'),
_ctx: null,
isReady() {
return true;
},
shouldDiscardImage(image) {
try {
if (!this._ctx) {
this._ctx = this._canvas.getContext('2d', { willReadFrequently: true });
}
this._canvas.width = 8;
this._canvas.height = 8;
this._ctx.drawImage(image, 0, 0, 8, 8);
const d = this._ctx.getImageData(0, 0, 8, 8).data;
let gray = 0;
let total = 0;
for (let i = 0; i < d.length; i += 4) {
const r = d[i];
const g = d[i + 1];
const b = d[i + 2];
const mx = Math.max(r, g, b);
const mn = Math.min(r, g, b);
if (mx - mn < 15 && r > 110 && r < 240) gray++;
total++;
}
return total > 0 && gray / total > 0.92;
} catch (e) {
return false;
}
},
};
const imagery = new Cesium.UrlTemplateImageryProvider({
url: IMAGERY_URL,
credit: 'Imagery © Esri, Maxar, Earthstar Geographics',
maximumLevel: 19,
tileDiscardPolicy: discardNotAvailableTile,
});
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._clampMarkersToTerrain();
this._setupPick();
this._setupRightRestore();
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({
lng: entry.lng,
lat: entry.lat,
position,
r2: Cesium.Cartesian3.magnitudeSquared(position),
noteNormal,
noteSelected,
index: i,
});
});
this._initMarkerLayer();
}
// 把便签锚点贴到地形表面(否则在椭球面高度 0 上,放大旋转时会飘)
async _clampMarkersToTerrain() {
const scene = this.viewer.scene;
if (!scene) return;
// 等待地形 provider 就绪(setTerrain 是异步的,调用时 provider 可能还没准备好)
let tp = scene.terrainProvider;
if (!tp) {
tp = await new Promise((resolve) => {
const started = Date.now();
const poll = () => {
const p = scene.terrainProvider;
if (p) { resolve(p); return; }
if (Date.now() - started > 30000) { resolve(undefined); return; }
setTimeout(poll, 500);
};
poll();
});
}
if (!tp) {
console.warn('[CesiumGlobe] 地形 provider 未就绪,便签保持椭球面高度');
return;
}
const cartos = this.markers.map((m) =>
Cesium.Cartographic.fromDegrees(m.lng, m.lat),
);
try {
// 固定层级 15(约 30m):ArcGIS 高程服务无 availability,不用 sampleTerrainMostDetailed
await Cesium.sampleTerrain(tp, 15, cartos);
} catch (err) {
console.warn('[CesiumGlobe] 地形高度采样失败:', err);
}
// 失败的点逐级降级重试(更粗的层级更容易成功)
for (const level of [12, 8, 4]) {
const retry = cartos.filter((c) => c.height == null || !isFinite(c.height));
if (retry.length === 0) break;
try {
await Cesium.sampleTerrain(tp, level, retry);
} catch (err) {
break;
}
}
let clamped = 0;
for (let i = 0; i < this.markers.length; i++) {
const h = cartos[i] && cartos[i].height;
if (h != null && isFinite(h)) {
const m = this.markers[i];
m.position = Cesium.Cartesian3.fromDegrees(m.lng, m.lat, h);
m.r2 = Cesium.Cartesian3.magnitudeSquared(m.position);
clamped++;
}
}
console.log(`[CesiumGlobe] 便签已贴附地形:${clamped}/${this.markers.length}`);
}
// 便签用 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();
m.screenX = -1e9;
m.screenY = -1e9;
el.style.backgroundImage = `url("${m.normalUrl}")`;
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|²;背面隐藏(不渐变淡出)
const frontness = Cesium.Cartesian3.dot(m.position, camPos) - m.r2;
if (frontness <= 0) { el.style.display = 'none'; continue; }
el.style.display = 'block';
el.style.opacity = '1';
m.screenX = win.x - m.tipX;
m.screenY = win.y - m.tipY;
el.style.left = `${m.screenX}px`;
el.style.top = `${m.screenY}px`;
}
}
_setupPick() {
const viewer = this.viewer;
// 移除 Viewer 默认的单击选中与双击“追踪实体”
viewer.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
viewer.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
// 便签层 pointer-events:none 后,左键通过 canvas 命中测试选中便签
viewer.screenSpaceEventHandler.setInputAction((movement) => {
const x = movement.position.x;
const y = movement.position.y;
for (const m of this.markers) {
if (!m.el || m.el.style.display === 'none') continue;
const w = m.noteNormal.width;
const h = m.noteNormal.height;
if (
x >= m.screenX && x <= m.screenX + w &&
y >= m.screenY && y <= m.screenY + h
) {
this.onPick?.(m.index);
break;
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}
// 右键原生 tilt:按下记录位置;松开不回正。中键:回正到“不倾斜不旋转”(天底 + 北朝上)
_setupRightRestore() {
const scene = this.viewer.scene;
const camera = this.viewer.camera;
const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
this._rightHome = null;
handler.setInputAction(() => {
camera.cancelFlight(); // 取消可能仍在进行的回正动画
this._rightHome = Cesium.Cartesian3.clone(camera.position);
}, Cesium.ScreenSpaceEventType.RIGHT_DOWN);
handler.setInputAction(() => {
// 松开右键:只清掉 tilt 惯性余力,不回正
const tiltState = scene.screenSpaceCameraController._lastInertiaTiltMovement;
if (tiltState) tiltState.inertiaEnabled = false;
}, Cesium.ScreenSpaceEventType.RIGHT_UP);
handler.setInputAction(() => {
// 中键:回正(天底 + 北朝上,保持原注视点)
if (!this._rightHome) return;
const pos = this._rightHome;
this._rightHome = null;
camera.flyTo({
destination: pos,
orientation: { heading: 0, pitch: Cesium.Math.toRadians(-90), roll: 0 },
duration: 0.5,
});
}, Cesium.ScreenSpaceEventType.MIDDLE_DOWN);
this._rightRestoreHandler = handler;
}
// 相机平滑飞至某个标记上空并居中:回到与初始一致的高度与北朝上方向,左键拖拽手感不变
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');
// 国家名用 LabelCollection(避免大量 entity 开销)
const countryLabels = new Cesium.LabelCollection({ scene: this.viewer.scene });
this.viewer.scene.primitives.add(countryLabels);
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 ? 28 : areaKm2 > 1.5e6 ? 21 : areaKm2 > 3e5 ? 18 : 16;
countryLabels.add({
position: Cesium.Cartesian3.fromDegrees(props.LABEL_X, props.LABEL_Y),
text: cname,
font: `bold ${fontSize}px "Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif`,
fillColor: textGold,
outlineColor: black,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: 1000, // 1km 内不深度测试(近处不消失),远处仍遮挡背面
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 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, 2.5e6);
const tdc = new Cesium.NearFarScalar(1.25e6, 1.0, 2.5e6, 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,
});
labels.add({
position,
text: c.name,
font: 'bold 17px "Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif',
fillColor: textGold,
outlineColor: black,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -6),
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: 1000, // 1km 内不深度测试(近处不消失),远处仍遮挡背面
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();
}
}