fix:完成SpectralPlot软件测试报告2中提出的Bug及新需求

This commit is contained in:
he-zhi-cheng
2026-06-25 18:42:49 +08:00
parent 90b1fe2f7a
commit 983be8fdef
7 changed files with 2057 additions and 179 deletions

View File

@ -5,7 +5,9 @@
</a-layout-header>
<a-layout>
<a-layout-sider :resize-directions="['right']" style=" min-width: 20vw;max-width: 50vw;">
<a-dropdown trigger="contextMenu" alignPoint :style="{ display: 'block' }">
<a-dropdown ref="contextMenuDropdown" :popup-visible="contextMenuVisible"
@popup-visible-change="onContextMenuVisibleChange" trigger="contextMenu" alignPoint
:style="{ display: 'block' }">
<GuiLeftSider class="lefttree" v-on:NodeClicked="ononeFilechoese"
v-on:NodeDblClicked="onFileDblClick" v-on:FilesSelected="onFilesSelected"
v-on:FolderClicked="onFolderClicked" v-on:FolderDblClicked="onFolderDblClick"
@ -86,9 +88,47 @@ export default {
],
showSaveDialog: false,
filesToSave: [],
contextMenuVisible: false,
}
},
mounted() {
document.addEventListener('mousedown', this.handleGlobalMouseAction, true)
document.addEventListener('wheel', this.handleGlobalMouseAction, true)
window.addEventListener('blur', this.closeContextMenu)
window.addEventListener('close-app-context-menu', this.closeContextMenu)
document.addEventListener('visibilitychange', this.handleVisibilityChange)
},
beforeUnmount() {
document.removeEventListener('mousedown', this.handleGlobalMouseAction, true)
document.removeEventListener('wheel', this.handleGlobalMouseAction, true)
window.removeEventListener('blur', this.closeContextMenu)
window.removeEventListener('close-app-context-menu', this.closeContextMenu)
document.removeEventListener('visibilitychange', this.handleVisibilityChange)
},
methods: {
onContextMenuVisibleChange(visible) {
this.contextMenuVisible = visible
},
closeContextMenu() {
this.contextMenuVisible = false
},
handleVisibilityChange() {
if (document.visibilityState !== 'visible') {
this.closeContextMenu()
}
},
handleGlobalMouseAction(event) {
if (!this.contextMenuVisible) {
return
}
const popup = document.querySelector('.arco-trigger-popup')
if (popup && popup.contains(event.target)) {
return
}
this.closeContextMenu()
},
// 处理单个文件选择(单击)
async ononeFilechoese(filePath) {
// console.log('选中文件路径:', filePath);
@ -136,6 +176,7 @@ export default {
// 处理"显示曲线"菜单项点击事件
async onShowCurvesClick() {
this.closeContextMenu()
// console.log('点击显示曲线菜单项');
// 如果有多选文件,则加载所有选中的文件
if (this.selectedFilePaths && this.selectedFilePaths.length > 0) {
@ -185,6 +226,7 @@ export default {
this.filesToSave = []
},
async openCurveSaveDialogForSelectedFiles() {
this.closeContextMenu()
const paths = (this.selectedFilePaths && this.selectedFilePaths.length > 0)
? this.selectedFilePaths
: (this.selectedFilePath ? [this.selectedFilePath] : []);
@ -213,4 +255,4 @@ h1 {
height: 100%;
width: 100%;
}
</style>
</style>

View File

@ -0,0 +1,71 @@
function toFixedNumber(value, digits = 6) {
return Number(Number(value).toFixed(digits));
}
function buildSeedFromText(text) {
const input = String(text || 'default');
let hash = 0;
for (let i = 0; i < input.length; i++) {
hash = ((hash << 5) - hash) + input.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
function buildFallbackCoordinate(seedText) {
const seed = buildSeedFromText(seedText);
const lonRatio = (seed % 100000) / 100000;
const latRatio = (Math.floor(seed / 100000) % 100000) / 100000;
return {
longitude: toFixedNumber(115.7 + (117.4 - 115.7) * lonRatio),
latitude: toFixedNumber(40.2 + (40.5 - 40.2) * latRatio)
};
}
function normalizeGpsCoordinate(gps) {
if (!gps) return null;
let latitude = Number(gps.latitude);
let longitude = Number(gps.longitude);
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
return null;
}
// 某些数据源纬经度字段可能写反,优先纠正到合法范围
if (Math.abs(latitude) > 90 && Math.abs(longitude) <= 90) {
[latitude, longitude] = [longitude, latitude];
}
if (Math.abs(latitude) > 90 || Math.abs(longitude) > 180) {
return null;
}
return {
latitude: toFixedNumber(latitude),
longitude: toFixedNumber(longitude),
altitude: Number.isFinite(Number(gps.altitude)) ? Number(gps.altitude) : null
};
}
function resolveDisplayCoordinate(item) {
const environmentData = item?.environmentData || {};
const normalizedGps = normalizeGpsCoordinate(environmentData.gps);
if (normalizedGps) {
return normalizedGps;
}
return {
...buildFallbackCoordinate(environmentData.fileName || item?.name || ''),
altitude: null
};
}
export {
normalizeGpsCoordinate,
resolveDisplayCoordinate
};

View File

@ -5,7 +5,7 @@
justify-content: space-between;">
<el-row class="secondhang">
<GuiForPlotShow @onComBox1="onComBox1" @onComBox2="onComBox2" @legendselectchanged="legendselectchanged"
@useDarkDnChanged="onUseDarkDnChanged" ref="ASDPlotShow" class="plotcontainer">
@processOptionsChanged="onProcessOptionsChanged" @useDarkDnChanged="onUseDarkDnChanged" ref="ASDPlotShow" class="plotcontainer">
</GuiForPlotShow>
</el-row>
<el-row class="firsthang">
@ -40,9 +40,20 @@ defineOptions({
name: "GuiForDataShow"
});
function createDefaultProcessConfig() {
return {
movingAverage: {
window: 5,
startMethod: 'shrink',
endMethod: 'shrink'
}
};
}
const fromData = ref({
comBox1: spectralTypeList[0].value,
comBox2: '',
processConfig: createDefaultProcessConfig()
});
const useDarkDn = ref(true);
const ASDPlotShow = ref(null);
@ -53,6 +64,148 @@ const spectralDataForInfo = ref([]); // 新增:专供 Info/图片使用的数
const imgeList = ref([]);
const fileData = ref([]);
function getTypeLabel(type) {
return spectralTypeList.find(item => item.value === type)?.label || type || '';
}
function formatMissingItem(type) {
if (type === 'flat_ref') {
return '白板校准文件原数据(flat_ref)';
}
const label = getTypeLabel(type);
if (!label) return '';
if (!type || label === type) return label;
return `${label}(${type})`;
}
function getAvailableRawTypes(irisData) {
const set = new Set();
const sections = irisData?.spectral_data_section || [];
for (const element of sections) {
const name = (element?.name || '').toString().toLowerCase();
if (!name) continue;
if (name.includes('ground_dn')) set.add('ground_dn');
if (name.includes('flat_dn')) set.add('flat_dn');
if (name.includes('dark_dn')) set.add('dark_dn');
if (name.includes('flat_ref')) set.add('flat_ref');
if (name.includes('gain')) set.add('gain');
}
return set;
}
function getRequiredRawTypesForSpectralType(spectralType, useDarkDn) {
const needDark = !!useDarkDn;
const addDark = (arr) => (needDark ? [...arr, 'dark_dn'] : arr);
switch ((spectralType || '').toString()) {
case 'ground_dn':
return ['ground_dn'];
case 'flat_dn':
return ['flat_dn'];
case 'dark_dn':
return ['dark_dn'];
case 'gain':
return ['gain'];
case 'bbjzwj':
return ['flat_ref'];
case 'flat_ref':
return addDark(['ground_dn', 'flat_dn']);
case 'radiance_ground':
return addDark(['gain', 'ground_dn']);
case 'radiance_flat':
return addDark(['gain', 'flat_dn']);
case 'refrad':
return addDark(['gain', 'ground_dn', 'flat_dn']);
case 'ref_abs':
return addDark(['ground_dn', 'flat_dn', 'flat_ref']);
case 'fszd':
return addDark(['gain', 'ground_dn']);
default:
return [];
}
}
function parseFiberValue(value) {
if (value === undefined || value === null) return undefined;
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string') {
const match = value.match(/-?\d+(\.\d+)?/);
if (match) {
const n = Number(match[0]);
if (Number.isFinite(n)) return n;
}
}
return undefined;
}
function parseFiberFromCaliFilePath(value) {
if (typeof value !== 'string' || !value) return undefined;
const match = value.match(/_(\d+(?:\.\d+)?)d_/i);
if (!match) return undefined;
const n = Number(match[1]);
return Number.isFinite(n) ? n : undefined;
}
function isIrradianceMode(irisData) {
for (const info of irisData?.spectral_info_section || []) {
const explicit = parseFiberValue(info?.ForeOptics);
const parsed = explicit ?? parseFiberFromCaliFilePath(info?.cailifilePath) ?? parseFiberFromCaliFilePath(info?.califilePath);
if (parsed === undefined) continue;
const rounded = Math.round(Number(parsed));
return rounded === 180 || rounded === 360;
}
return false;
}
function buildNoDataDetailMessage(files, spectralType, useDarkDn) {
const usableFiles = (files || []).filter(file => file?.data);
const fileCount = usableFiles.length;
const required = getRequiredRawTypesForSpectralType(spectralType, useDarkDn);
const missingCounts = new Map();
const blockedCounts = new Map();
for (const file of usableFiles) {
const present = getAvailableRawTypes(file.data);
for (const dep of required) {
if (!present.has(dep)) {
missingCounts.set(dep, (missingCounts.get(dep) || 0) + 1);
}
}
if (spectralType === 'radiance_ground' && isIrradianceMode(file.data)) {
blockedCounts.set('辐射照度模式不生成地物辐射亮度(radiance_ground)', (blockedCounts.get('辐射照度模式不生成地物辐射亮度(radiance_ground)') || 0) + 1);
}
if (spectralType === 'fszd' && !isIrradianceMode(file.data)) {
blockedCounts.set('非辐射照度模式不生成辐射照度(fszd)', (blockedCounts.get('非辐射照度模式不生成辐射照度(fszd)') || 0) + 1);
}
}
const missingList = Array.from(missingCounts.entries())
.sort((a, b) => b[1] - a[1])
.map(([dep, count]) => {
const formatted = formatMissingItem(dep);
if (!formatted) return '';
return count === fileCount ? formatted : `${formatted}(部分文件)`;
})
.filter(Boolean);
const blockedList = Array.from(blockedCounts.entries())
.sort((a, b) => b[1] - a[1])
.map(([reason, count]) => (count === fileCount ? reason : `${reason}(部分文件)`));
const parts = [];
if (missingList.length) parts.push(`缺少:${missingList.join('、')}`);
if (blockedList.length) parts.push(`原因:${blockedList.join('、')}`);
return parts.join('');
}
function warnNoData() {
const spectralType = fromData.value.comBox1;
const typeLabel = getTypeLabel(spectralType);
const detail = buildNoDataDetailMessage(fileData.value, spectralType, useDarkDn.value);
ElMessage.warning(detail ? `当前没有${typeLabel}数据;${detail}` : `当前没有${typeLabel}数据`);
}
async function onloaddata(data) {
// 重置数据
spectralDataList.value = [];
@ -67,13 +220,11 @@ async function onloaddata(data) {
try {
// 加载文件数据
fileData.value = await SpectralDataService.loadFileData(data);
// 处理数据
await processSpectralData();
if (!spectralDataList.value || spectralDataList.value.length === 0) {
const typeLabel = spectralTypeList.find(i => i.value === fromData.value.comBox1)?.label || fromData.value.comBox1;
ElMessage.warning(`当前数据没有${typeLabel}数据`);
warnNoData();
}
} catch (error) {
console.error('加载数据失败:', error);
@ -87,7 +238,8 @@ async function processSpectralData() {
fileData.value,
fromData.value.comBox1,
fromData.value.comBox2,
useDarkDn.value
useDarkDn.value,
fromData.value.processConfig
);
@ -143,8 +295,7 @@ const onComBox1 = async (e) => {
await processSpectralData();
if (!spectralDataList.value || spectralDataList.value.length === 0) {
const typeLabel = spectralTypeList.find(i => i.value === fromData.value.comBox1)?.label || fromData.value.comBox1;
ElMessage.warning(`当前数据没有${typeLabel}数据`);
warnNoData();
}
};
@ -155,8 +306,26 @@ const onComBox2 = async (val) => {
await processSpectralData();
if (!spectralDataList.value || spectralDataList.value.length === 0) {
const typeLabel = spectralTypeList.find(i => i.value === fromData.value.comBox1)?.label || fromData.value.comBox1;
ElMessage.warning(`当前数据没有${typeLabel}数据`);
warnNoData();
}
}
};
const onProcessOptionsChanged = async (config) => {
const nextConfig = createDefaultProcessConfig();
if (config?.movingAverage) {
nextConfig.movingAverage = {
...nextConfig.movingAverage,
...config.movingAverage
};
}
fromData.value.processConfig = nextConfig;
if (fromData.value.comBox2 === 'MA' && fromData.value.comBox1) {
await processSpectralData();
if (!spectralDataList.value || spectralDataList.value.length === 0) {
warnNoData();
}
}
};
@ -257,4 +426,4 @@ const onUseDarkDnChanged = async (val) => {
border-radius: 4px;
margin-left: 24px;
}
</style>
</style>

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@ import { Icon, Style, Text, Fill, Stroke } from 'ol/style';
import { fromLonLat } from 'ol/proj';
import pinIcon from '../assets/图钉.png';
import { boundingExtent } from 'ol/extent';
import { resolveDisplayCoordinate } from '../utils/mapLocation';
const props = defineProps({
dataListMap: {
@ -37,10 +38,64 @@ const map = ref(null);
const vectorLayer = ref(null);
const isMapReady = ref(false);
const isFullscreen = ref(false);
const DEFAULT_FALLBACK_LATITUDE = 39.0856735;
const DEFAULT_FALLBACK_LONGITUDE = 117.1951073;
const DEFAULT_FALLBACK_LATITUDE_OFFSET = 0.08;
const DEFAULT_FALLBACK_LONGITUDE_OFFSET = 0.12;
function isValidMapCoordinate(longitude, latitude) {
const lng = Number(longitude);
const lat = Number(latitude);
return Number.isFinite(lng) && Number.isFinite(lat) && lng !== 0 && lat !== 0;
}
function hashStringToSeed(value) {
const text = String(value || '');
let hash = 0;
for (let index = 0; index < text.length; index += 1) {
hash = ((hash << 5) - hash) + text.charCodeAt(index);
hash |= 0;
}
return Math.abs(hash);
}
function createStableRandom(seed) {
const normalizedSeed = (seed % 2147483647) || 1;
let currentSeed = normalizedSeed;
return () => {
currentSeed = (currentSeed * 16807) % 2147483647;
return (currentSeed - 1) / 2147483646;
};
}
function getFallbackCoordinateForMap(item, index) {
const fileName = item?.environmentData?.fileName || item?.name || `fallback-${index}`;
const random = createStableRandom(hashStringToSeed(fileName));
const longitudeOffset = (random() - 0.5) * DEFAULT_FALLBACK_LONGITUDE_OFFSET;
const latitudeOffset = (random() - 0.5) * DEFAULT_FALLBACK_LATITUDE_OFFSET;
return {
longitude: DEFAULT_FALLBACK_LONGITUDE + longitudeOffset,
latitude: DEFAULT_FALLBACK_LATITUDE + latitudeOffset
};
}
function getDisplayCoordinateForMap(item, index) {
const resolvedCoordinate = resolveDisplayCoordinate(item);
if (isValidMapCoordinate(resolvedCoordinate?.longitude, resolvedCoordinate?.latitude)) {
return {
longitude: Number(resolvedCoordinate.longitude),
latitude: Number(resolvedCoordinate.latitude)
};
}
// 仅用于地图显示保底,不回写源数据,避免影响导出等真实数据流程。
return getFallbackCoordinateForMap(item, index);
}
const initializeMap = () => {
const center = fromLonLat([116.255535, 40.204654]);
const center = fromLonLat([DEFAULT_FALLBACK_LONGITUDE, DEFAULT_FALLBACK_LATITUDE]);
const view = new View({
center,
@ -78,33 +133,18 @@ const initializeMap = () => {
isMapReady.value = true;
};
const getRandomCoordinateInBeijing = () => {
const minLon = 115.7;
const maxLon = 117.4;
const minLat = 40.2;
const maxLat = 40.5;
const lon = Math.random() * (maxLon - minLon) + minLon;
const lat = Math.random() * (maxLat - minLat) + minLat;
return {
longitude: parseFloat(lon.toFixed(6)),
latitude: parseFloat(lat.toFixed(6))
};
};
const handleTagging = (data) => {
if (!isMapReady.value || !Array.isArray(data)) return;
const features = [];
const coords = [];
data.forEach(item => {
const { longitude, latitude } = getRandomCoordinateInBeijing(); // 模拟
if (longitude && latitude) {
data.forEach((item, index) => {
const { longitude, latitude } = getDisplayCoordinateForMap(item, index);
if (isValidMapCoordinate(longitude, latitude)) {
const feature = new Feature({
geometry: new Point(fromLonLat([longitude, latitude])),
name: item.environmentData.fileName || ''
name: item?.environmentData?.fileName || item?.name || ''
});
feature.setStyle(
@ -115,7 +155,7 @@ const handleTagging = (data) => {
scale: 1
}),
text: new Text({
text: item.environmentData.fileName || '',
text: item?.environmentData?.fileName || item?.name || '',
offsetY: 14,
font: '300 14px sans-serif',
fill: new Fill({
@ -144,8 +184,9 @@ const handleTagging = (data) => {
}
});
vectorLayer.value?.getSource()?.clear();
if (features.length > 0) {
vectorLayer.value.getSource().clear();
vectorLayer.value.getSource().addFeatures(features);
const extent = boundingExtent(coords);

View File

@ -21,31 +21,59 @@ class getIrisDataDispose {
this.environmentData = element;
}
}
this.trimOffset = this.computeTrimOffset();
}
computeTrimOffset() {
const bands = Number(this.devinfoData?.bandnum ?? 0);
if (bands < 3) return 0;
const firstSensorId = this.irisData?.spectral_info_section?.[0]?.sensor_id;
return /is3/i.test(String(firstSensorId || "")) ? 3 : 0;
}
getEffectiveBandnum() {
const bands = Number(this.devinfoData?.bandnum ?? 0);
return Math.max(0, bands - (this.trimOffset || 0));
}
toNormalArray(element) {
const typedArray = manageSpectralData(element, this.devinfoData);
const normalArray = typedArray ? Array.from(typedArray) : [];
if ((this.trimOffset || 0) <= 0) return normalArray;
const end = Math.max(0, normalArray.length - this.trimOffset);
return normalArray.slice(0, end);
}
initData() {
const basicTypes = ["ground_dn", "flat_dn", "dark_dn", "gain"];
const basicTypes = ["ground_dn", "flat_dn", "dark_dn", "gain", "bbjzwj"];
const specialTypes = [
"flat_ref",
"radiance_ground",
"radiance_flat",
"refrad",
"ref_abs",
"fszd",
];
let a = null;
if (basicTypes.includes(this.spectralName)) {
return this.getFileSpectralData();
a = this.getFileSpectralData();
} else if (specialTypes.includes(this.spectralName)) {
return this.getSpecialFileSpectralData();
a = this.getSpecialFileSpectralData();
}
return a;
}
//获取一般后端直接返回的类型
getFileSpectralData() {
// 基础类型适配暗噪开关ground_dn / flat_dn 在启用暗噪时做 “- dark_dn”
const nameLower = (this.spectralName || '').toLowerCase();
const bands = this.devinfoData?.bandnum ?? 0;
const matchLower = nameLower === 'bbjzwj' ? 'flat_ref' : nameLower;
const outputName = nameLower;
const bands = this.getEffectiveBandnum();
const needDarkCorr =
this.useDarkDn && (nameLower === 'ground_dn' || nameLower === 'flat_dn');
this.useDarkDn && (matchLower === 'ground_dn' || matchLower === 'flat_dn');
if (needDarkCorr) {
let mainEl = null; // ground_dn 或 flat_dn 的原始 element
@ -53,7 +81,7 @@ class getIrisDataDispose {
for (const element of this.spectral_data) {
const lower = (element.name || '').toLowerCase();
if (!mainEl && lower.includes(nameLower)) {
if (!mainEl && lower.includes(matchLower)) {
mainEl = element;
} else if (!darkEl && lower.includes('dark_dn')) {
darkEl = element;
@ -66,10 +94,8 @@ class getIrisDataDispose {
return undefined;
}
const mainTyped = manageSpectralData(mainEl, this.devinfoData);
const main = Array.isArray(mainTyped) ? mainTyped : Array.from(mainTyped || []);
const darkTyped = darkEl ? manageSpectralData(darkEl, this.devinfoData) : null;
const dark = darkTyped ? (Array.isArray(darkTyped) ? darkTyped : Array.from(darkTyped)) : null;
const main = this.toNormalArray(mainEl);
const dark = darkEl ? this.toNormalArray(darkEl) : null;
const corrected = [];
const invalidCount = { missing: 0, invalidResult: 0 };
@ -106,15 +132,21 @@ class getIrisDataDispose {
);
}
return { ...mainEl, normalArray: corrected };
return { ...mainEl, name: outputName, normalArray: corrected };
}
// 默认路径:保持原逻辑(未启用暗噪或其它基础类型)
for (const element of this.spectral_data) {
const typedArray = manageSpectralData(element, this.devinfoData);
const normalArray = Array.from(typedArray);
if ((element.name || '').toLowerCase().includes(nameLower)) {
return { ...element, normalArray };
const normalArray = this.toNormalArray(element);
if ((element.name || '').toLowerCase().includes(matchLower)) {
if (nameLower === 'bbjzwj') {
const sanitized = normalArray.map((v) => {
const n = Number(v);
return Number.isFinite(n) ? n : 0;
});
return { ...element, name: outputName, normalArray: sanitized };
}
return { ...element, name: outputName, normalArray };
}
}
}
@ -130,21 +162,23 @@ class getIrisDataDispose {
["radiance_ground", null],
["radiance_flat", null],
["refrad", null],
["fszd", null],
]);
for (const element of this.spectral_data) {
const typedArray = manageSpectralData(element, this.devinfoData);
const normalArray = Array.from(typedArray);
const normalArray = this.toNormalArray(element);
if (element.name.toLowerCase().includes("ground_dn")) {
spectralDataMap.set("ground_dn", { ...element, normalArray });
} else if (element.name.toLowerCase().includes("flat_dn")) {
spectralDataMap.set("flat_dn", { ...element, normalArray });
} else if (element.name.toLowerCase().includes("dark_dn")) {
spectralDataMap.set("dark_dn", { ...element, normalArray });
} else if (element.name.toLowerCase().includes("flat_ref")) {
spectralDataMap.set("flat_ref", { ...element, normalArray });
} else if (element.name.toLowerCase().includes("gain")) {
spectralDataMap.set("gain", { ...element, normalArray });
}
}
// 分支依赖校验与辅助函数
const has = (key) => !!spectralDataMap.get(key);
const requireDeps = (deps) => deps.every((k) => has(k));
@ -153,13 +187,81 @@ class getIrisDataDispose {
for (const k of keys) obj[k] = spectralDataMap.get(k).normalArray;
return mergeObjectArrays(obj);
};
const baseName = (...preferredKeys) => {
for (const k of preferredKeys) {
if (has(k)) return spectralDataMap.get(k).name.split("_")[0];
const outputName = (this.spectralName || '').toLowerCase();
const parseFiberValue = (value) => {
if (value === undefined || value === null) return undefined;
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string") {
const match = value.match(/-?\d+(\.\d+)?/);
if (match) {
const n = Number(match[0]);
if (Number.isFinite(n)) return n;
}
}
return "unknown";
return undefined;
};
const parseFiberFromCaliFilePath = (value) => {
if (typeof value !== "string" || !value) return undefined;
const match = value.match(/_(\d+(?:\.\d+)?)d_/i);
if (!match) return undefined;
const n = Number(match[1]);
return Number.isFinite(n) ? n : undefined;
};
const pickFiber = (...preferredKeys) => {
for (const info of this.irisData?.spectral_info_section || []) {
const explicit = parseFiberValue(info?.ForeOptics);
if (explicit !== undefined) return explicit;
const fromCaliFilePath =
parseFiberFromCaliFilePath(info?.cailifilePath) ??
parseFiberFromCaliFilePath(info?.califilePath);
if (fromCaliFilePath !== undefined) return fromCaliFilePath;
}
return undefined;
};
const isIrradianceMode = () => {
const fiber = pickFiber("ground_dn", "flat_dn", "gain", "flat_ref", "dark_dn");
if (fiber === undefined) return false;
const rounded = Math.round(Number(fiber));
return rounded === 180 || rounded === 360;
};
const computeRadiance = (targetKey) => {
const deps = ["gain", targetKey].concat(this.useDarkDn ? ["dark_dn"] : []);
if (!requireDeps(deps)) return undefined;
const arrAll = buildArrAll(deps);
const out = [];
const invalidCount = { zeroDiv: 0, invalidResult: 0 };
for (const element of arrAll) {
const gainExposure = spectralDataMap.get("gain").exposure;
const targetExposure = spectralDataMap.get(targetKey).exposure;
if (Math.abs(targetExposure) < 1e-10) {
invalidCount.zeroDiv++;
out.push(0);
continue;
}
const exposureRatio = gainExposure / targetExposure;
const dnDiff = element[targetKey] - (this.useDarkDn ? element.dark_dn : 0);
const value = exposureRatio * (element.gain * dnDiff);
if (isFinite(value) && !isNaN(value)) {
out.push(value);
} else {
invalidCount.invalidResult++;
out.push(0);
}
}
return { out, invalidCount };
};
// 结果验证(依赖不够时不会走到这里)
const validateResult = (array, name, type) => {
if (array.length === 0) {
@ -168,30 +270,30 @@ class getIrisDataDispose {
}
return { normalArray: array, name: name };
};
if (this.spectralName == "radiance_ground") {
if (isIrradianceMode()) return undefined;
const deps = ["gain", "ground_dn"].concat(this.useDarkDn ? ["dark_dn"] : []);
if (!requireDeps(deps)) return undefined; // 依赖不够,直接返回空,保持原逻辑
const arrAll = buildArrAll(deps);
const name = baseName("ground_dn", "gain");
const radiance_ground = [];
const invalidCount = { zeroDiv: 0, invalidResult: 0 };
for (const element of arrAll) {
const gainExposure = spectralDataMap.get("gain").exposure;
const groundExposure = spectralDataMap.get("ground_dn").exposure;
if (Math.abs(groundExposure) < 1e-10) {
invalidCount.zeroDiv++;
radiance_ground.push(0);
continue;
}
const exposureRatio = gainExposure / groundExposure;
const dnDiff = element.ground_dn - (this.useDarkDn ? element.dark_dn : 0);
const a = exposureRatio * (element.gain * dnDiff);
if (isFinite(a) && !isNaN(a)) {
radiance_ground.push(a);
} else {
@ -202,31 +304,30 @@ class getIrisDataDispose {
if (invalidCount.zeroDiv > 0 || invalidCount.invalidResult > 0) {
console.warn(`radiance_ground计算统计: 除零错误 ${invalidCount.zeroDiv} 个,无效结果 ${invalidCount.invalidResult} 个,有效数据 ${radiance_ground.length}`);
}
return validateResult(radiance_ground, name + "_radiance_ground", "radiance_ground");
return validateResult(radiance_ground, outputName, "radiance_ground");
} else if (this.spectralName == "radiance_flat") {
const deps = ["gain", "flat_dn"].concat(this.useDarkDn ? ["dark_dn"] : []);
if (!requireDeps(deps)) return undefined; // 依赖不够,直接返回空
const arrAll = buildArrAll(deps);
const name = baseName("flat_dn", "gain");
const radiance_flat = [];
const invalidCount = { zeroDiv: 0, invalidResult: 0 };
for (const element of arrAll) {
const gainExposure = spectralDataMap.get("gain").exposure;
const flatExposure = spectralDataMap.get("flat_dn").exposure;
if (Math.abs(flatExposure) < 1e-10) {
invalidCount.zeroDiv++;
radiance_flat.push(0);
continue;
}
const exposureRatio = gainExposure / flatExposure;
const dnDiff = element.flat_dn - (this.useDarkDn ? element.dark_dn : 0);
const b = exposureRatio * (element.gain * dnDiff);
if (isFinite(b) && !isNaN(b)) {
radiance_flat.push(b);
} else {
@ -237,37 +338,36 @@ class getIrisDataDispose {
if (invalidCount.zeroDiv > 0 || invalidCount.invalidResult > 0) {
console.warn(`radiance_flat计算统计: 除零错误 ${invalidCount.zeroDiv} 个,无效结果 ${invalidCount.invalidResult} 个,有效数据 ${radiance_flat.length}`);
}
return validateResult(radiance_flat, name + "_radiance_flat", "radiance_flat");
return validateResult(radiance_flat, outputName, "radiance_flat");
} else if (this.spectralName == "refrad") {
const deps = ["gain", "ground_dn", "flat_dn"].concat(this.useDarkDn ? ["dark_dn"] : []);
if (!requireDeps(deps)) return undefined; // 依赖不够,直接返回空
const arrAll = buildArrAll(deps);
const name = baseName("ground_dn", "flat_dn", "gain");
const refrad = [];
const invalidCount = { zeroDiv: 0, invalidResult: 0 };
for (const element of arrAll) {
const gainExposure = spectralDataMap.get("gain").exposure;
const groundExposure = spectralDataMap.get("ground_dn").exposure;
const flatExposure = spectralDataMap.get("flat_dn").exposure;
if (Math.abs(groundExposure) < 1e-10 || Math.abs(flatExposure) < 1e-10) {
invalidCount.zeroDiv++;
refrad.push(0);
continue;
}
const groundRatio = gainExposure / groundExposure;
const flatRatio = gainExposure / flatExposure;
const groundDnDiff = element.ground_dn - (this.useDarkDn ? element.dark_dn : 0);
const flatDnDiff = element.flat_dn - (this.useDarkDn ? element.dark_dn : 0);
const a = groundRatio * (element.gain * groundDnDiff);
const b = flatRatio * (element.gain * flatDnDiff);
if (Math.abs(b) < 1e-10) {
invalidCount.zeroDiv++;
refrad.push(0);
@ -284,21 +384,20 @@ class getIrisDataDispose {
if (invalidCount.zeroDiv > 0 || invalidCount.invalidResult > 0) {
console.warn(`refrad计算统计: 除零错误 ${invalidCount.zeroDiv} 个,无效结果 ${invalidCount.invalidResult} 个,有效数据 ${refrad.length}`);
}
return validateResult(refrad, name + "_refrad", "refrad");
return validateResult(refrad, outputName, "refrad");
} else if (this.spectralName == "flat_ref") {
const deps = ["ground_dn", "flat_dn"].concat(this.useDarkDn ? ["dark_dn"] : []);
if (!requireDeps(deps)) return undefined; // 依赖不够,直接返回空
const arrAll = buildArrAll(deps);
const name = baseName("ground_dn", "flat_dn");
const validData = [];
const invalidCount = { zeroDiv: 0, invalidResult: 0 };
for (const element of arrAll) {
const denominator = element.flat_dn - (this.useDarkDn ? element.dark_dn : 0);
const numerator = element.ground_dn - (this.useDarkDn ? element.dark_dn : 0);
if (Math.abs(denominator) < 1e-10) {
invalidCount.zeroDiv++;
validData.push(0);
@ -315,13 +414,61 @@ class getIrisDataDispose {
if (invalidCount.zeroDiv > 0 || invalidCount.invalidResult > 0) {
console.warn(`flat_ref计算统计: 除零错误 ${invalidCount.zeroDiv} 个,无效结果 ${invalidCount.invalidResult} 个,有效数据 ${validData.length}`);
}
return validateResult(validData, name + "_flat_ref", "flat_ref");
return validateResult(validData, outputName, "flat_ref");
} else if (this.spectralName == "ref_abs") {
const deps = ["ground_dn", "flat_dn", "flat_ref"].concat(this.useDarkDn ? ["dark_dn"] : []);
if (!requireDeps(deps)) return undefined;
const arrAll = buildArrAll(deps);
const ref_abs = [];
const invalidCount = { zeroDiv: 0, invalidResult: 0 };
for (const element of arrAll) {
const denominator = element.flat_dn - (this.useDarkDn ? element.dark_dn : 0);
const numerator = element.ground_dn - (this.useDarkDn ? element.dark_dn : 0);
if (Math.abs(denominator) < 1e-10) {
invalidCount.zeroDiv++;
ref_abs.push(0);
continue;
}
const ratio = numerator / denominator;
const calib = Number(element.flat_ref);
const value = ratio * (Number.isFinite(calib) ? calib : 0);
if (isFinite(value) && !isNaN(value)) {
ref_abs.push(value);
} else {
invalidCount.invalidResult++;
ref_abs.push(0);
}
}
if (invalidCount.zeroDiv > 0 || invalidCount.invalidResult > 0) {
console.warn(`ref_abs计算统计: 除零错误 ${invalidCount.zeroDiv} 个,无效结果 ${invalidCount.invalidResult} 个,有效数据 ${ref_abs.length}`);
}
return validateResult(ref_abs, outputName, "ref_abs");
} else if (this.spectralName == "fszd") {
if (!isIrradianceMode()) return undefined;
if (!has("ground_dn")) return undefined;
const computed = computeRadiance("ground_dn");
if (!computed) return undefined;
const typeLabel = "fszd";
if (computed.invalidCount.zeroDiv > 0 || computed.invalidCount.invalidResult > 0) {
console.warn(`fszd计算统计: 除零错误 ${computed.invalidCount.zeroDiv} 个,无效结果 ${computed.invalidCount.invalidResult} 个,有效数据 ${computed.out.length}`);
}
return validateResult(computed.out, outputName, typeLabel);
}
}
getSpectralInfoData() {
let spectralInfoData = [];
for (let i = 0; i < this.devinfoData.bandnum; i++) {
const bands = this.getEffectiveBandnum();
for (let i = 0; i < bands; i++) {
const a =
this.devinfoData.wave_coeff.a1 * i ** 3 +
this.devinfoData.wave_coeff.a2 * i * i +
@ -414,10 +561,9 @@ const manageSpectralData = (rawData, devinfoData) => {
case 0x14: // uint32
return new Uint32Array(uint8Array.buffer, 0, bands);
default:
throw new Error(`Unsupported data type: 0x${dataType.toString(16)}`);
throw new Error(`Unsupported data type: 0x${rawData.data_type.toString(16)}`);
}
} catch (e) {
error.value = `Failed to parse spectral data: ${e.message}`;
return null;
}
};
@ -455,18 +601,18 @@ const spectralTypeList = [
// value: "refrad",
// label: "反射率能量",
// },
// {
// value: "jdfs",
// label: "绝对反射率",
// },
// {
// value: "ckbzwj",
// label: "参考校准文件",
// },
// {
// value: "fszd",
// label: "辐射照度",
// },
{
value: "ref_abs",
label: "绝对反射率",
},
{
value: "bbjzwj",
label: "白板校准文件",
},
{
value: "fszd",
label: "辐射照度",
},
];
const spectralProcessTypeList = [
@ -485,6 +631,9 @@ const spectralProcessTypeList = [
];
function D1(arr) {
if (!Array.isArray(arr) || arr.length === 0) {
return [];
}
const result = [];
for (let i = 1; i < arr.length; i++) {
result.push(arr[i] - arr[i - 1]);
@ -494,6 +643,9 @@ function D1(arr) {
}
function D2(arr) {
if (!Array.isArray(arr) || arr.length === 0) {
return [];
}
const arr2 = D1(arr);
return D1(arr2);
}

View File

@ -1,6 +1,92 @@
import { getIrisDataDispose, D1, D2 } from '../utils/irisDataDispose';
import { invoke } from "@tauri-apps/api/tauri";
const DEFAULT_MOVING_AVERAGE_OPTIONS = Object.freeze({
window: 5,
startMethod: 'shrink',
endMethod: 'shrink'
});
function normalizeMovingAverageOptions(options = {}) {
let window = Number(options.window);
if (!Number.isFinite(window) || window < 1) {
window = DEFAULT_MOVING_AVERAGE_OPTIONS.window;
}
window = Math.max(1, Math.round(window));
if (window % 2 === 0) {
window += 1;
}
const startMethod = ['shrink', 'pad', 'keep'].includes(options.startMethod)
? options.startMethod
: DEFAULT_MOVING_AVERAGE_OPTIONS.startMethod;
const endMethod = ['shrink', 'pad', 'keep'].includes(options.endMethod)
? options.endMethod
: DEFAULT_MOVING_AVERAGE_OPTIONS.endMethod;
return {
window,
startMethod,
endMethod
};
}
function average(values) {
const validValues = values
.map(value => Number(value))
.filter(value => Number.isFinite(value));
if (!validValues.length) {
return 0;
}
return validValues.reduce((sum, value) => sum + value, 0) / validValues.length;
}
function buildBoundaryValues(value, count, method) {
if (count <= 0 || method !== 'pad') {
return [];
}
return Array.from({ length: count }, () => value);
}
function movingAverage(data, options = {}) {
if (!Array.isArray(data) || !data.length) {
return [];
}
const normalizedOptions = normalizeMovingAverageOptions(options);
const radius = Math.floor(normalizedOptions.window / 2);
return data.map((currentValue, index) => {
const startIndex = index - radius;
const endIndex = index + radius;
const missingLeft = Math.max(0, 0 - startIndex);
const missingRight = Math.max(0, endIndex - (data.length - 1));
if (
(missingLeft > 0 && normalizedOptions.startMethod === 'keep') ||
(missingRight > 0 && normalizedOptions.endMethod === 'keep')
) {
return currentValue;
}
const values = [];
if (missingLeft > 0) {
values.push(...buildBoundaryValues(data[0], missingLeft, normalizedOptions.startMethod));
}
values.push(...data.slice(Math.max(0, startIndex), Math.min(data.length, endIndex + 1)));
if (missingRight > 0) {
values.push(...buildBoundaryValues(data[data.length - 1], missingRight, normalizedOptions.endMethod));
}
return average(values);
});
}
/**
* 光谱数据处理服务
*/
@ -17,10 +103,13 @@ export class SpectralDataService {
* 加载文件数据
*/
static async loadFileData(filePaths) {
const fileData = [];
for (const path of filePaths) {
try {
const src = await invoke("getoneirisfile", { path });
console.log('原始文件:', src);
const fileName = this.extractFileName(path);
fileData.push({ data: src, name: fileName });
} catch (error) {
@ -33,7 +122,7 @@ export class SpectralDataService {
/**
* 处理光谱数据
*/
static processSpectralData(fileData, spectralType, processType = '', useDarkDn = true) {
static processSpectralData(fileData, spectralType, processType = '', useDarkDn = true, processOptions = {}) {
const spectralDataList = [];
const imageList = [];
@ -55,11 +144,18 @@ export class SpectralDataService {
url: 'jzsb'
}]);
}
if (datay?.normalArray && datax) {
const y = datay?.normalArray;
if (Array.isArray(y) && y.length > 0 && Array.isArray(datax) && datax.length > 0) {
if (datax.length !== y.length) {
console.warn(`波长/数据长度不一致: ${element.name} datax=${datax.length} datay=${y.length}`);
}
const fileBaseName = (element.name || '').toString().replace(/\.[^.]+$/, '');
const typeSuffix = (datay?.name || spectralType || '').toString();
const seriesName = [fileBaseName, typeSuffix].filter(Boolean).join('_');
spectralDataList.push({
name: datay?.name || '',
name: seriesName,
datax: datax,
datay: datay.normalArray,
datay: y,
environmentData: { ...environmentData, fileName: element.name }
});
}
@ -72,7 +168,7 @@ export class SpectralDataService {
return {
spectralDataList,
imageList,
processedData: this.applyProcessing(spectralDataList, processType)
processedData: this.applyProcessing(spectralDataList, processType, processOptions)
};
}
@ -80,7 +176,7 @@ export class SpectralDataService {
* 应用数据处理D1/D2等
* 注意:该方法需要在类内部,避免顶层出现 static 关键字导致语法错误
*/
static applyProcessing(spectralDataList, processType) {
static applyProcessing(spectralDataList, processType, processOptions = {}) {
if (!processType) return spectralDataList;
switch (processType) {
@ -98,6 +194,15 @@ export class SpectralDataService {
datay: D2(item.datay),
environmentData: item.environmentData
}));
case 'MA': {
const movingAverageOptions = normalizeMovingAverageOptions(processOptions.movingAverage);
return spectralDataList.map(item => ({
name: item.name + '_MA',
datax: item.datax,
datay: movingAverage(item.datay, movingAverageOptions),
environmentData: item.environmentData
}));
}
default:
return spectralDataList;
}
@ -106,11 +211,11 @@ export class SpectralDataService {
/**
* 批量处理选定文件的光谱数据
*/
static processSelectedFiles(fileData, selectedFileNames, spectralType, processType = '', useDarkDn = true) {
static processSelectedFiles(fileData, selectedFileNames, spectralType, processType = '', useDarkDn = true, processOptions = {}) {
const filteredData = fileData.filter(file =>
selectedFileNames.includes(file.name)
);
// 透传暗噪开关
return this.processSpectralData(filteredData, spectralType, processType, useDarkDn);
return this.processSpectralData(filteredData, spectralType, processType, useDarkDn, processOptions);
}
}
}