release: bump version to 0.43.0

This commit is contained in:
linkong
2026-04-28 16:10:17 +08:00
parent 1cd2dab0ee
commit ac69d5d354
69 changed files with 6954 additions and 1141 deletions

View File

@@ -168,6 +168,19 @@ import {
toggleComputeCenters,
updateComputeCenterVisualState,
} from "./compute-centers.js";
import {
clearVesselData,
clearVesselSelection,
getShowVessels,
getVesselCount,
getVesselLegendItems,
getVesselMarkers,
loadVessels,
setVesselMarkerState,
showVesselTrack,
toggleVessels,
updateVesselVisualState,
} from "./vessels.js";
import {
setupControls,
getAutoRotate,
@@ -228,6 +241,7 @@ let inertialVelocity = { x: 0, y: 0 };
let hoveredCable = null;
let hoveredBGP = null;
let hoveredComputeCenter = null;
let hoveredVessel = null;
let hoveredSatellite = null;
let hoveredSatelliteIndex = null;
let lockedSatellite = null;
@@ -251,6 +265,7 @@ let isDataLoading = false;
let currentLoadToken = 0;
let cablesEnabled = true;
let satellitesEnabled = false;
let vesselsEnabled = false;
let cableToggleToken = 0;
let satelliteToggleToken = 0;
let satelliteHydrationToken = 0;
@@ -278,6 +293,8 @@ const scratchBGPDirection = new THREE.Vector3();
const scratchBGPWorldPosition = new THREE.Vector3();
const scratchComputeCenterDirection = new THREE.Vector3();
const scratchComputeCenterWorldPosition = new THREE.Vector3();
const scratchVesselDirection = new THREE.Vector3();
const scratchVesselWorldPosition = new THREE.Vector3();
const scratchSatelliteWorldPosition = new THREE.Vector3();
const scratchSatelliteScreenPosition = new THREE.Vector3();
const scratchViewCenterWorld = new THREE.Vector3();
@@ -425,6 +442,7 @@ export function clearLockedObject() {
clearCableSelection();
clearBGPSelection();
clearComputeCenterSelection();
clearVesselSelection();
clearRelatedSatelliteHighlights();
setSatelliteRingState(null, "none", null);
clearRuntimeSelection();
@@ -498,9 +516,11 @@ function resetTransientComputeCenterStates() {
function clearTransientHoverState() {
resetTransientBGPStates();
resetTransientComputeCenterStates();
resetTransientVesselStates();
clearCountryBoundaryHover();
hoveredBGP = null;
hoveredComputeCenter = null;
hoveredVessel = null;
if (hoveredCable && !isSameCable(hoveredCable, lockedObject)) {
setCableState(hoveredCable.userData.cableId, CABLE_STATE.NORMAL);
@@ -515,6 +535,25 @@ function clearTransientHoverState() {
setHoveredSatelliteIndex(null);
}
function getFrontFacingVesselMarkers(markers) {
const earth = getEarth();
if (!earth) return markers;
scratchCameraToEarth.subVectors(camera.position, earth.position).normalize();
return markers.filter((marker) => {
scratchVesselWorldPosition.copy(marker.position);
marker.parent?.localToWorld(scratchVesselWorldPosition);
scratchVesselDirection
.subVectors(scratchVesselWorldPosition, earth.position)
.normalize();
return (
scratchCameraToEarth.dot(scratchVesselDirection) >
SATELLITE_CONFIG.frontFacingDotThreshold
);
});
}
function applyBGPHoverState(marker) {
resetTransientBGPStates();
if (!marker) {
@@ -549,6 +588,30 @@ function applyComputeCenterHoverState(marker) {
}
}
function resetTransientVesselStates() {
getVesselMarkers().forEach((marker) => {
if (marker !== lockedObject) {
setVesselMarkerState(marker, "normal");
}
});
}
function applyVesselHoverState(marker) {
resetTransientVesselStates();
if (!marker) {
hoveredVessel = null;
return;
}
hoveredVessel = marker;
if (marker !== lockedObject) {
setVesselMarkerState(marker, "hover");
}
}
function isSameVessel(marker1, marker2) {
return Boolean(marker1 && marker2 && marker1.userData?.mmsi === marker2.userData?.mmsi);
}
function getPrimaryBGPHoverTarget(bgpAnomalyIntersects, bgpCollectorIntersects) {
if (bgpAnomalyIntersects.length > 0) {
return bgpAnomalyIntersects[0].object;
@@ -665,6 +728,37 @@ function showComputeCenterInfo(marker, coords) {
}, coords);
}
function formatVesselStatus(navStatus) {
if (navStatus === 1) return "锚泊";
if (navStatus === 5) return "停靠";
if (navStatus === 0) return "航行中";
return navStatus ?? "-";
}
function showVesselInfo(marker, coords) {
setLegendMode("vessels");
showInfoCard("vessel", {
name: marker.userData?.name || `MMSI ${marker.userData?.mmsi}`,
mmsi: marker.userData?.mmsi,
imo: marker.userData?.imo || "-",
flag: marker.userData?.flag || "-",
vessel_type: marker.userData?.vessel_type_name || "-",
speed: marker.userData?.sog ?? "-",
course: marker.userData?.cog ?? marker.userData?.heading ?? "-",
status: formatVesselStatus(marker.userData?.nav_status),
length: marker.userData?.length ?? "-",
received_at: marker.userData?.received_at
? new Date(marker.userData.received_at).toLocaleString("zh-CN", { hour12: false })
: "-",
}, coords);
}
function getVesselBriefHtml(marker) {
const name = marker.userData?.name || `MMSI ${marker.userData?.mmsi}`;
const speed = marker.userData?.sog ?? "-";
return `<strong>${name}</strong><br>${marker.userData?.vessel_type_name || "Vessel"} · ${speed} kn`;
}
function getComputeCenterBriefHtml(marker) {
const name = marker.userData?.name || "算力中心";
const type = formatComputeCenterTypeLabel(marker.userData?.site_type);
@@ -864,6 +958,13 @@ function getComputeCenterFocusCoords(marker) {
return { lat, lon };
}
function getVesselFocusCoords(marker) {
const lat = marker?.userData?.latitude;
const lon = marker?.userData?.longitude;
if (typeof lat !== "number" || typeof lon !== "number") return null;
return { lat, lon };
}
async function focusSearchTarget(coords, zoom = Math.max(getZoomLevel(), 1.12)) {
if (!coords || !camera) return;
await focusEarthView(camera, {
@@ -1037,6 +1138,33 @@ async function focusSearchComputeCenter(marker) {
);
}
async function focusSearchVessel(marker) {
await setVesselsEnabled(true, {
suppressStatus: true,
suppressLoadingUi: true,
});
interruptCruisePresentation({ resetLoop: true });
clearLockedObject();
setAutoRotate(false);
const coords = getVesselFocusCoords(marker);
if (coords) {
await focusSearchTarget(coords, Math.max(getZoomLevel(), 1.2));
}
setVesselMarkerState(marker, "locked");
lockedObject = marker;
lockedObjectType = "vessel";
showVesselInfo(marker, getSearchCardCoords());
showVesselTrack(marker, getEarth()).catch((error) => {
console.warn("船只轨迹加载失败:", error);
});
showStatusMessage(
`已定位船只:${marker.userData?.name || marker.userData?.mmsi || "未知船只"}`,
"info",
);
}
function resolveEarthSearchResults(query) {
const results = [];
const normalizedQuery = query.trim().toLowerCase();
@@ -1205,6 +1333,33 @@ function resolveEarthSearchResults(query) {
});
});
getVesselMarkers().forEach((marker) => {
const score = computeSearchScore(
normalizedQuery,
marker.userData?.name,
marker.userData?.mmsi,
marker.userData?.imo,
marker.userData?.flag,
marker.userData?.vessel_type_name,
"船只 船舶 ais vessel ship maritime",
);
if (score < 0) return;
results.push({
id: `vessel:${marker.userData?.mmsi || marker.uuid}`,
kind: "vessel",
icon: "directions_boat",
typeLabel: "船只",
title: marker.userData?.name || `MMSI ${marker.userData?.mmsi}`,
subtitle: [
marker.userData?.vessel_type_name,
marker.userData?.flag,
marker.userData?.sog !== undefined ? `${marker.userData.sog} kn` : null,
].filter(Boolean).join(" · ") || "AIS 船只",
score,
entity: marker,
});
});
return results
.sort((left, right) => {
if (right.score !== left.score) return right.score - left.score;
@@ -1234,6 +1389,10 @@ async function handleSearchSelection(result) {
}
if (result.kind === "compute_center") {
await focusSearchComputeCenter(result.entity);
return;
}
if (result.kind === "vessel") {
await focusSearchVessel(result.entity);
}
}
@@ -1270,6 +1429,7 @@ function applyEarthStatsSummary(summary) {
landingPointCount: `${summary.landingPointCount}`,
satelliteCount: `${summary.satelliteCount}`,
computeCenterCount: `${summary.computeCenterCount}`,
vesselCount: `${summary.vesselCount}`,
bgpAnomalyCount: `${summary.bgpEventCount}`,
bgpCollectorCount: `${summary.bgpCollectorCount}`,
bgpStatusSummary: formatBGPStatusFromSummary(summary),
@@ -1291,6 +1451,7 @@ async function loadEarthStatsSummary() {
landingPointCount: toCount(stats.landing_point_count),
satelliteCount: toCount(stats.satellite_count),
computeCenterCount: toCount(stats.compute_center_count),
vesselCount: toCount(stats.vessel_count),
bgpEventCount: toCount(stats.bgp_event_count),
bgpIncidentCount: toCount(stats.bgp_incident_count),
bgpAnomalyCount: toCount(stats.bgp_anomaly_count),
@@ -1945,6 +2106,23 @@ function updateSatelliteToggleUi(enabled, satelliteCount = getSatelliteCount())
setEarthStatValue("satellite-count", `${resolvedCount}`);
}
function updateVesselHud(result = {}) {
const count = Number(result.totalCount ?? getVesselCount() ?? 0);
setEarthStatValue("vessel-count", `${count}`);
}
function updateVesselToggleUi(enabled, vesselCount = getVesselCount()) {
const vesselBtn = document.getElementById("toggle-vessels");
if (vesselBtn) {
setLayerButtonState(vesselBtn, {
active: enabled,
loading: false,
tooltip: enabled ? "隐藏船只" : "显示船只",
});
}
setEarthStatValue("vessel-count", `${vesselCount || 0}`);
}
function updateCableToggleUi(enabled) {
const cableBtn = document.getElementById("toggle-cables");
if (cableBtn) {
@@ -2063,6 +2241,29 @@ async function ensureSatellitesEnabled() {
return loadResult.count;
}
async function ensureVesselsEnabled() {
if (!scene || !camera || !renderer || destroyed) return 0;
const earth = getEarth();
if (!earth) return 0;
vesselsEnabled = true;
const result = await loadVessels(scene, earth);
toggleVessels(true);
updateVesselToggleUi(true, result.totalCount);
setLegendItems("vessels", getVesselLegendItems());
refreshLegend();
return result.totalCount;
}
function disableVessels() {
vesselsEnabled = false;
toggleVessels(false);
clearVesselSelection();
updateVesselToggleUi(false, 0);
setLegendItems("vessels", getVesselLegendItems());
refreshLegend();
}
function disableSatellites() {
satellitesEnabled = false;
satelliteToggleToken += 1;
@@ -2079,6 +2280,7 @@ function updateStatsSummary() {
const landingPointCount =
getLandingPoints().length || earthStatsSummary?.landingPointCount || 0;
const satelliteCount = getSatelliteCount() || earthStatsSummary?.satelliteCount || 0;
const vesselCount = getVesselCount() || earthStatsSummary?.vesselCount || 0;
const computeCenterCount =
getComputeCenterCount() || earthStatsSummary?.computeCenterCount || 0;
const bgpEventCount = getBGPCount() || earthStatsSummary?.bgpEventCount || 0;
@@ -2088,6 +2290,7 @@ function updateStatsSummary() {
cableCount: `${cableCount}`,
landingPointCount: `${landingPointCount}`,
satelliteCount: `${satelliteCount}`,
vesselCount: `${vesselCount}`,
computeCenterCount: `${computeCenterCount}`,
bgpAnomalyCount: `${bgpEventCount}`,
bgpCollectorCount: `${bgpCollectorCount}`,
@@ -2333,6 +2536,7 @@ async function loadData() {
clearBGPData(earth);
clearCableData(earth);
clearComputeCenterData(earth);
clearVesselData(earth);
clearSatelliteData();
clearCountryBoundaryHover();
@@ -2370,8 +2574,10 @@ async function loadData() {
updateCableToggleUi,
updateSatelliteToggleUi,
updateComputeCenterHud,
updateVesselHud,
updateBGPHud,
getShowComputeCenters,
getShowVessels,
getShowCountryBoundaries,
getShowBGP,
isEarthTextureVisible: () => getEarthTextureVisible(),
@@ -2429,6 +2635,7 @@ async function loadData() {
setLegendItems("satellites", getSatelliteLegendItems());
setLegendItems("countryBoundaries", getCountryBoundaryLegendItems());
setLegendItems("computeCenters", getComputeCenterLegendItems());
setLegendItems("vessels", getVesselLegendItems());
setLegendItems("bgp", getBGPLegendItems());
refreshLegend();
setLoading(false);
@@ -2463,6 +2670,10 @@ export function getSatellitesEnabled() {
return satellitesEnabled;
}
export function getVesselsEnabled() {
return vesselsEnabled;
}
export async function setCablesEnabled(
enabled,
{ suppressStatus = false, suppressLoadingUi = false } = {},
@@ -2668,6 +2879,62 @@ export async function setSatellitesEnabled(
}
}
export async function setVesselsEnabled(
enabled,
{ suppressStatus = false, suppressLoadingUi = false } = {},
) {
if (enabled === vesselsEnabled) {
updateVesselToggleUi(enabled);
return getVesselCount();
}
if (!enabled) {
clearSelectionAndInfo();
disableVessels();
if (!suppressStatus) {
showStatusMessage("船只已隐藏", "info");
}
return 0;
}
if (!suppressLoadingUi) {
setLoadingMessage("正在加载船只数据...");
setLoading(true);
hideError();
}
try {
const vesselCount = await ensureVesselsEnabled();
if (!suppressStatus) {
showStatusMessage("船只已显示", "info");
}
return vesselCount;
} catch (error) {
vesselsEnabled = false;
clearVesselData(getEarth());
updateVesselToggleUi(false, 0);
const message = `船只加载失败: ${error?.message || String(error)}`;
void reportEarthClientLog({
level: "error",
category: "layer-toggle",
module: "vessels",
message,
detail: error,
});
if (!suppressLoadingUi) {
showError(message);
}
if (!suppressStatus) {
showStatusMessage(message, "error");
}
throw error;
} finally {
if (!suppressLoadingUi) {
setLoading(false);
}
}
}
function setupEventListeners() {
const handleResize = () => onWindowResize();
const handleVisibilityChange = () => onVisibilityChange();
@@ -2865,6 +3132,11 @@ function onMouseMove(event) {
const computeCenterIntersects = getShowComputeCenters()
? interactionRaycaster.intersectObjects(frontFacingComputeCenterMarkers)
: [];
const vesselIntersects = getShowVessels()
? interactionRaycaster.intersectObjects(
getFrontFacingVesselMarkers(getVesselMarkers()),
)
: [];
let hoveredSat = null;
let hoveredSatIndexFromIntersect = null;
@@ -2886,6 +3158,8 @@ function onMouseMove(event) {
const hoveredComputeCenterMarker =
computeCenterIntersects.length > 0 ? computeCenterIntersects[0].object : null;
const hoveredVesselMarker =
vesselIntersects.length > 0 ? vesselIntersects[0].object : null;
if (
hoveredComputeCenter &&
@@ -2893,6 +3167,9 @@ function onMouseMove(event) {
) {
clearTransientHoverState();
}
if (hoveredVessel && !isSameVessel(hoveredVessel, hoveredVesselMarker)) {
clearTransientHoverState();
}
if (
hoveredCable &&
@@ -2936,6 +3213,18 @@ function onMouseMove(event) {
getComputeCenterBriefHtml(hoveredComputeCenterMarker),
);
objectTooltipShown = true;
} else if (
hoveredVesselMarker &&
getShowVessels() &&
lockedObjectType !== "vessel"
) {
applyVesselHoverState(hoveredVesselMarker);
showTooltip(
event.clientX + TOOLTIP_CURSOR_OFFSET,
event.clientY + TOOLTIP_CURSOR_OFFSET,
getVesselBriefHtml(hoveredVesselMarker),
);
objectTooltipShown = true;
} else if (cableIntersects.length > 0 && getShowCables()) {
const cable = cableIntersects[0].object;
hoveredCable = cable;
@@ -2966,9 +3255,12 @@ function onMouseMove(event) {
applyBGPHoverState(lockedObject);
} else if (lockedObjectType === "compute_center" && lockedObject) {
applyComputeCenterHoverState(lockedObject);
} else if (lockedObjectType === "vessel" && lockedObject) {
applyVesselHoverState(lockedObject);
} else if (!lockedObjectType && !isCruisePresentationPinned()) {
resetTransientBGPStates();
resetTransientComputeCenterStates();
resetTransientVesselStates();
hideInfoCard();
}
@@ -3185,6 +3477,11 @@ function onClick(event) {
getFrontFacingComputeCenterMarkers(getComputeCenterMarkers()),
)
: [];
const vesselIntersects = getShowVessels()
? interactionRaycaster.intersectObjects(
getFrontFacingVesselMarkers(getVesselMarkers()),
)
: [];
const satIntersects = getSatellitePointerIntersections(event);
const clickedBGPMarker = getShowBGP()
@@ -3193,6 +3490,9 @@ function onClick(event) {
const clickedComputeCenterMarker = computeCenterIntersects.length > 0
? computeCenterIntersects[0].object
: null;
const clickedVesselMarker = vesselIntersects.length > 0
? vesselIntersects[0].object
: null;
if (clickedBGPMarker?.userData?.type === "bgp") {
interruptCruisePresentation();
@@ -3260,6 +3560,26 @@ function onClick(event) {
return;
}
if (clickedVesselMarker?.userData?.type === "vessel") {
interruptCruisePresentation();
clearLockedObject();
const clickedMarker = clickedVesselMarker;
setVesselMarkerState(clickedMarker, "locked");
lockedObject = clickedMarker;
lockedObjectType = "vessel";
setAutoRotate(false);
showVesselInfo(clickedMarker, { x: event.clientX, y: event.clientY });
showVesselTrack(clickedMarker, earth).catch((error) => {
console.warn("船只轨迹加载失败:", error);
});
showStatusMessage(
`已选择船只: ${clickedMarker.userData?.name || clickedMarker.userData?.mmsi}`,
"info",
);
return;
}
if (cableIntersects.length > 0 && getShowCables()) {
interruptCruisePresentation();
clearLockedObject();
@@ -3412,6 +3732,7 @@ function animate() {
: null;
updateBGPVisualState(lockedObjectType, lockedObject, camera, activeCruiseMarker);
updateComputeCenterVisualState(lockedObjectType, lockedObject, camera);
updateVesselVisualState(lockedObjectType, lockedObject, camera);
if (lockedObjectType === "cable" && lockedObject) {
applyLandingPointVisualState(lockedObject.userData.name, false, camera);