AnalysisSystemForRadionucli.../src/views/spectrumAnalysis/components/Modals/AnalyzeInteractiveToolModal/index.vue

1996 lines
54 KiB
Vue
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.

<template>
<custom-modal
v-model="visible"
:width="1280"
class="interactive-analysis-tools-dialog"
:enableFullScreen="true"
title="Interactive Analyse Tools"
:footer="null"
destroy-on-close
@fullscreen="handleFullScreenChange"
>
<div class="interactive-analysis-tools">
<div class="interactive-analysis-tools-left">
<div class="chart">
<CustomChart
ref="chartRef"
:option="option"
:opts="opts"
@zr:mousedown="handleMouseDown"
@zr:mouseup="handleMouseUp"
@brushEnd="handleBrushEnd"
@zr:click="handleChartClick"
/>
<rect-list
ref="rectListRef"
:chartRef="$refs.chartRef"
:baseControls="baseCtrls_Copy"
:draggable="isModifying"
@move="handleMove"
@moveEnd="handleMoveEnd"
/>
</div>
<!-- 缩略图 -->
<div class="thumbnail">
<CustomChart ref="thumbnailRef" :option="thumbnailOption" />
</div>
<!-- 缩略图结束 -->
<!-- 表格 -->
<div class="table">
<p class="title">
<span @click="handleChangeMarkLine('prev')">&lt; </span>
6 Peaks with Anthro.Nuclides
<span @click="handleChangeMarkLine('next')">&gt;</span>
</p>
<custom-table
ref="tableRef"
size="small"
:class="list.length ? 'has-data' : ''"
:list="list"
:columns="columns"
:scroll="{ y: 288 }"
:selectedRowKeys.sync="selectedKeys"
rowKey="index"
:canDeselect="false"
@rowClick="handleTableRowClick"
>
</custom-table>
<div class="operators">
<a-button type="primary" @click="nuclideReviewModalVisible = true">Nuclide Review Window</a-button>
<a-button type="primary" @click="handleAddPeakComment()">Add Peak Comment</a-button>
<a-button type="primary" @click="handleAddGeneralComment()">Add General Comment</a-button>
</div>
</div>
<!-- 表格结束 -->
<resize-observer @notify="handleResize" />
</div>
<!-- 右侧 -->
<div class="interactive-analysis-tools-right">
<title-over-border class="peak-box-container" :title="btnGroupType == 1 ? 'Peak' : 'Baseline Control Points'">
<div class="peak-box">
<!-- 按钮组1 -->
<template v-if="btnGroupType == 1">
<div class="peak-box-item">
<a-button type="primary" @click="handleInsert">Insert</a-button>
</div>
<div class="peak-box-item">
<a-button type="primary" @click="handleDel">Delete</a-button>
</div>
<div class="peak-box-item">
<a-button type="primary" :class="{ 'is-fitting': isFitting }" @click="handleFit">Fit</a-button>
</div>
<div class="peak-box-item symbol" :key="4">
<a-button type="primary" @click="handleChangeMarkLine('prev')">&lt;</a-button>
<a-button type="primary" @click="handleChangeMarkLine('next')">&gt;</a-button>
</div>
<div class="peak-box-item base-line">
<a-button type="primary" @click="handleSwitchOperation">BaseLine</a-button>
</div>
</template>
<!-- 按钮组2 -->
<template v-if="btnGroupType == 2">
<div class="peak-box-item">
<a-button type="primary" @click="handleAddCP">(A)dd CP</a-button>
</div>
<div class="peak-box-item">
<a-button type="primary" @click="handleRemoveCP">(R)emove CP</a-button>
</div>
<div class="peak-box-item">
<a-button type="primary" key="modify-btn" :class="{ 'is-modify': isModifying }" @click="handleModifyCP"
>(M)odify CP</a-button
>
</div>
<div class="peak-box-item">
<a-button type="primary" @click="handleEditSlope">Edit (S)lope</a-button>
</div>
<div class="peak-box-item">
<a-button type="primary" :disabled="isOperationStackEmpty" @click="handleUndo">Undo</a-button>
</div>
<div class="peak-box-item">
<a-button type="primary" :loading="isReploting" @click="handleReplot">Replot</a-button>
</div>
<div class="peak-box-item">
<a-button type="primary" :loading="isAccepting" @click="handleAccept">Accept</a-button>
</div>
<div class="peak-box-item">
<a-button type="primary" @click="handleSwitchOperation">Cancel</a-button>
</div>
</template>
</div>
</title-over-border>
<div class="reset-btn-box">
<a-button type="primary" @click="handleResetChart">Reset Chart</a-button>
</div>
<div class="identify-box">
<title-over-border title="Nuclide Identify">
<a-form-model class="tolerance">
<a-form-model-item label="Tolerance">
<a-input-number
v-model="model.tolerance"
:step="0.1"
:min="0"
@change="handleToleranceChange"
></a-input-number>
</a-form-model-item>
</a-form-model>
<div class="identify-item">
<div class="title">Possible Nuclide</div>
<a-spin :spinning="!!(selectedTableItem && selectedTableItem._loading)">
<div class="content">
<template v-if="selectedTableItem && selectedTableItem._possible">
<div
class="item"
:class="{ active: possible == model.possibleNuclide }"
v-for="(possible, index) in selectedTableItem._possible"
:key="index"
@click="model.possibleNuclide = possible"
>
{{ possible }}
</div>
</template>
</div>
</a-spin>
</div>
<div class="identify-item">
<div class="title">Nuclide Identified</div>
<div class="content">
<template v-if="selectedTableItem">
<div
class="item"
:class="{ active: identified == model.identifiedNuclide }"
v-for="(identified, index) in selectedTableItem.nuclides"
:key="index"
@click="model.identifiedNuclide = identified"
>
{{ identified }}
</div>
</template>
</div>
</div>
<div class="identify-operators">
<span class="text">{{ model.possibleNuclide }}</span>
<a-button type="primary" :disabled="!model.possibleNuclide" @click="handleAddNuclide">Add</a-button>
<a-button type="primary" @click="handleDelNuclide">Del</a-button>
</div>
</title-over-border>
</div>
</div>
<!-- 右侧结束 -->
</div>
<!-- Peak Comment弹窗 开始 -->
<peak-comment-modal v-model="peakCommentModalVisible" :curRow="curRow" />
<!-- Peak Comment弹窗 结束 -->
<!-- General Comment弹窗 开始 -->
<general-comment-modal v-model="generalCommentModalVisible" />
<!-- General Comment弹窗 结束 -->
<!-- Fit Peaks and Baseline弹窗 开始 -->
<fit-peaks-and-base-line-modal
v-model="fitPeaksAndBaselineModalVisible"
:channel_1="channel_1"
:channel_2="channel_2"
:isInsertPeak="isInsertPeak"
@result="handleInsertSuccess"
@cancel="handleCancelSuccess"
/>
<!-- Fit Peaks and Baseline弹窗 结束 -->
<!-- Nuclide Review 弹窗开始 -->
<nuclide-review-modal v-model="nuclideReviewModalVisible" :sampleId="sampleId" :channel="currChannel" />
<!-- Nuclide Review 弹窗结束 -->
<!-- Edit Slope 弹窗 -->
<edit-slope-modal ref="editSlopeModal" @change="handleSlopeChange" />
<!-- Edit Slope 结束 -->
</custom-modal>
</template>
<script>
import CustomChart from '@/components/CustomChart/index.vue'
import TitleOverBorder from '../../TitleOverBorder.vue'
import PeakCommentModal from './components/PeakCommentModal.vue'
import FitPeaksAndBaseLineModal from './components/FitPeaksAndBaselineModal.vue'
import NuclideReviewModal from './components/NuclideReviewModal.vue'
import ModalMixin from '@/mixins/ModalMixin'
import { getAction, postAction } from '@/api/manage'
import { cloneDeep } from 'lodash'
import { buildLineSeries, findSeriesByName, getXAxisAndYAxisByPosition, rangeNumber } from '@/utils/chartHelper'
import SampleDataMixin from '@/views/spectrumAnalysis/SampleDataMixin'
import GeneralCommentModal from './components/GeneralCommentModal.vue'
import EditSlopeModal from './components/EditSlopeModal.vue'
// import Response from './Response.json'
import { updateBaseLine } from '@/utils/WasmHelper'
import RectList from './components/RectList.vue'
import { isNullOrUndefined } from '@/utils/util'
import { findNearPeak, getLineData, transformPointListData } from '@/utils/sampleHelper'
import { getSampleData } from '@/utils/SampleStore'
// 初始配置
const initialOption = {
grid: {
top: 40,
left: 80,
right: 30,
bottom: 30,
},
title: {
text: '',
left: 'center',
bottom: 10,
textStyle: {
color: '#8FD4F8',
rich: {
a: {
padding: [0, 20, 0, 0],
fontSize: 16,
},
},
},
},
tooltip: {
trigger: 'axis',
axisPointer: {
lineStyle: {
color: '#3CAEBB',
width: 1,
},
},
formatter: undefined,
className: 'figure-chart-option-tooltip',
},
xAxis: {
axisLine: {
lineStyle: {
color: '#ade6ee',
},
},
splitLine: {
show: false,
},
axisLabel: {
textStyle: {
color: '#ade6ee',
},
},
min: 1,
max: 'dataMax',
animation: false,
},
yAxis: {
type: 'log',
name: 'Counts',
nameLocation: 'center',
nameGap: 40,
nameTextStyle: {
color: '#8FD4F8',
fontSize: 16,
},
axisLine: {
show: true,
lineStyle: {
color: '#ade6ee',
},
},
splitLine: {
show: true,
lineStyle: {
color: 'rgba(173, 230, 238, .2)',
},
},
axisLabel: {
textStyle: {
color: '#ade6ee',
},
},
min: 0.1,
max: 'dataMax',
animation: false,
},
series: [],
brush: {},
}
const columns = [
{
title: 'ID',
customRender: (_, __, index) => {
return index + 1
},
width: 60,
},
{
title: 'Energy (keV)',
dataIndex: 'energy',
width: 120,
customRender: (text) => {
return text.toFixed(3)
},
},
{
title: 'Centroid (C)',
dataIndex: 'peakCentroid',
width: 120,
customRender: (text) => {
return text.toFixed(3)
},
},
{
title: 'FWHM (keV)',
dataIndex: 'fwhm',
width: 120,
customRender: (text) => {
return text.toFixed(3)
},
},
{
title: 'Area',
dataIndex: 'area',
width: 120,
customRender: (text) => {
return text.toFixed(3)
},
},
{
title: 'Detectability',
dataIndex: 'significance',
width: 120,
customRender: (text) => {
return text == 'Infinity' ? 'inf' : text.toFixed(3)
},
},
{
title: '#Cmnt',
dataIndex: 'comments',
width: 120,
},
{
title: 'Nuclides',
dataIndex: 'nuclides',
width: 120,
ellipsis: true,
customRender: (text) => {
return text && text.join(';')
},
},
]
// 缩略图配置
const thumbnailOption = {
grid: {
top: 0,
left: 0,
right: 0,
bottom: 0,
},
xAxis: {
type: 'category',
axisLine: {
lineStyle: {
color: '#fff',
},
},
splitLine: {
show: false,
},
axisLabel: {
show: false,
},
axisTick: {
show: false,
},
min: 1,
max: 'dataMax',
},
yAxis: {
type: 'value',
axisLine: {
show: false,
},
splitLine: {
show: false,
},
axisLabel: {
show: false,
},
max: 0,
min: 0,
},
series: null,
}
const nuclideIdentifyModal = {
possibleNuclide: '',
tolerance: 0.5,
identifiedNuclide: '',
}
// 操作类型
const Operators = {
ADD: 1, // 新增
REMOVE: 2, // 移除
MODIFY: 3, // 改变
SLOPE_CHANGE: 4, // 改变slope
}
export default {
mixins: [ModalMixin, SampleDataMixin],
components: {
CustomChart,
TitleOverBorder,
PeakCommentModal,
FitPeaksAndBaseLineModal,
NuclideReviewModal,
GeneralCommentModal,
EditSlopeModal,
RectList,
},
props: {
colorConfig: {
type: Object,
default: () => ({}),
},
},
data() {
return {
columns,
searchParam: {
energy: '',
tolerance: '',
},
option: cloneDeep(initialOption),
opts: { notMerge: false },
thumbnailOption: cloneDeep(thumbnailOption),
channelBaseCPChart: [],
channelBaseLineChart: [],
channelCountChart: [],
channelPeakChart: [],
barChart: [],
energy: [],
list: [],
BaseCtrls: {},
baseCtrls_Copy: {},
sampleId: -1,
peakCommentModalVisible: false, // Comment 弹窗是否显示
generalCommentModalVisible: false, // Comment 弹窗是否显示
btnGroupType: 1, // 右侧 Peak 中的按钮组切换
selectedKeys: [], // 选中的列表
fitPeaksAndBaselineModalVisible: false, // Fit Peaks And Base Line 弹窗
nuclideReviewModalVisible: false, // Nuclide Review 弹窗
model: cloneDeep(nuclideIdentifyModal),
currChannel: undefined, // 当currChannel前选中的channel
channel_1: undefined, // 用于Fit Peaks And Baseline Modal的道值
channel_2: undefined,
isInsertPeak: false, // 是否是插入Peak
selectedTableItem: undefined, // 当前选中的表格项
isModifying: false, // 正在修改控制点
isFitting: false, // 正在进行Fit操作
firstFittingChannel: null, // Fit操作时点击的第一个channel
isAccepting: false,
isReploting: false,
operationStack: [], // 操作记录
replotNeeded: false,
}
},
created() {
this.option.tooltip.formatter = (params) => {
const channel = parseInt(params[0].value[0])
const energy = this.energy.pointlist ? this.energy.pointlist[channel - 1].x : 0
return `<div class="channel">Channel: ${channel}</div>
<div class="energy">${isNullOrUndefined(energy) ? '' : `Energy: ${energy.toFixed(2)}`}</div>`
}
},
methods: {
getInfo() {
this.option.series = []
this.thumbnailOption.series = []
this.list = []
this.model = cloneDeep(nuclideIdentifyModal)
const { inputFileName } = this.sampleData
const currSampleDetailInfo = getSampleData(inputFileName)
const {
data: { allData, shadowChannelChart, shapeChannelData, peak, BaseCtrls, barChart },
} = currSampleDetailInfo
const channelBaseLine = getLineData(allData, 'BaseLine', 'channel')
const channelPeakGroup = getLineData(allData, 'Peak', 'channel', true)
const allEnergy = getLineData(allData, 'Energy', 'energy')
this.channelBaseCPChart = shapeChannelData
this.channelBaseLineChart = channelBaseLine
this.channelCountChart = shadowChannelChart
this.channelPeakChart = channelPeakGroup
this.energy = allEnergy
this.BaseCtrls = BaseCtrls
this.barChart = barChart
this.setChartOption(channelBaseLine, shadowChannelChart, channelPeakGroup, shapeChannelData, barChart)
this.list = peak
},
setChartOption(baseLine, count, peaks, baseCP, bar) {
const series = []
// 推入BaseLine
series.push(this.buildBaseLine(baseLine))
// 推入Count
series.push(this.buildCountLine(count))
// 推入Peak
series.push(...this.buildPeaks(peaks))
// 推入基线控制点
series.push(this.buildCtrlPoint(baseCP))
this.option.series = series
this.thumbnailOption.series = this.buildBarChart(bar)
this.setThumbnailRange(1, bar.length)
},
reset() {
this.currChannel = undefined
this.btnGroupType = 1
this.opts.notMerge = false
this.isFitting = false
this.replotNeeded = false
this.selectedTableItem = null
this.$nextTick(() => {
this.option.brush = { toolbox: [] }
const firstLine = this.list[0]
if (firstLine) {
this.handleTableRowClick(firstLine)
this.selectedKeys = [firstLine.index]
}
})
this.clearRect()
this.handleResetChart()
},
beforeModalOpen() {
this.getInfo()
this.reset()
},
// 点击图表,设置红线,改变表格的选中项
handleChartClick(param) {
const { offsetX, offsetY } = param
const point = getXAxisAndYAxisByPosition(this.$refs.chartRef.getChartInstance(), offsetX, offsetY)
if (point) {
const xAxis = Math.round(point[0])
this.setMarkLineXAxis(xAxis)
this.currChannel = xAxis
const { index } = findNearPeak(xAxis, this.list)
if (this.list.length) {
const selectedRow = this.list[index]
this.selectTableRow(selectedRow.index, index)
this.getSelPosNuclide(selectedRow)
this.selectedTableItem = selectedRow
}
// 如果点击了Fit按钮
if (this.isFitting) {
this.handleFittingChannel(xAxis)
}
}
},
// 处理Fit中的两个channel
handleFittingChannel(channel) {
// 第一个channel存在又点了一次则是第二次点击
if (this.firstFittingChannel !== null) {
// 查找两个channel之间的Peak峰
let left = this.firstFittingChannel
let right = channel
if (left > right) {
right = left
left = channel
}
const peaksBetweenChannel = this.list.filter((peak) => {
const centroidId = peak.peakCentroid
return centroidId >= left && centroidId <= right
})
if (!peaksBetweenChannel.length) {
this.$message.warn(`There are 0 peak between channel ${left} and ${right}`)
this.isFitting = false
return
}
this.channel_1 = left
this.channel_2 = right
this.isInsertPeak = false
this.fitPeaksAndBaselineModalVisible = true
this.isFitting = false
this.firstFittingChannel = null
} else {
this.firstFittingChannel = channel
}
},
// 切换图表上的红色竖线及表格选中
handleChangeMarkLine(direction) {
const prevAxis = this.getMarkLineXAxis()
let i,
size = this.list.length
if (direction == 'next') {
for (i = 0; i < size; i++) {
const centroid = Math.round(this.list[i].peakCentroid)
if (centroid > prevAxis) {
this.setMarkLineXAxis(centroid)
const selectedRow = this.list[i]
this.selectedTableItem = selectedRow
this.selectTableRow(selectedRow.index, i)
this.getSelPosNuclide(selectedRow)
return
}
}
} else if (direction == 'prev') {
for (i = size - 1; i >= 0; i--) {
const centroid = Math.round(this.list[i].peakCentroid)
if (centroid < prevAxis) {
this.setMarkLineXAxis(centroid)
const selectedRow = this.list[i]
this.selectedTableItem = selectedRow
this.selectTableRow(selectedRow.index, i)
this.getSelPosNuclide(selectedRow)
return
}
}
}
},
/**
* 根据当前Peak调整缩放范围
*/
adjustArea() {
const {
xAxis: { max, min },
} = this.option
// 找到最高点在这个范围内的峰
const peaks = []
for (let i = 0; i < this.channelPeakChart.length; i++) {
const peak = this.channelPeakChart[i]
const pointlist = peak.pointlist
// 如果 第一个/最后一个 都 大于/小于 范围,跳过
if (pointlist[0].x > max || pointlist[pointlist.length - 1].x < min) {
continue
}
// 找到峰最大值
const peakMaxY = Math.max(...pointlist.map(({ y }) => y))
const find = pointlist.find(({ y }) => y == peakMaxY)
// 如果最大值在范围内
if (find.x >= min && find.x <= max) {
peaks.push({
max: peakMaxY,
min: Math.min(pointlist[0].y, pointlist[pointlist.length - 1].y),
})
}
}
const peaksMax = Math.max(...peaks.map(({ max }) => max))
const peaksMin = Math.min(...peaks.map(({ min }) => min))
const {
yAxis: { max: yAxisMax, min: yAxisMin },
} = this.option
if (peaksMax > yAxisMax) {
this.option.yAxis.max = Math.ceil(peaksMax)
}
if (peaksMin < yAxisMin) {
this.option.yAxis.min = Math.floor(peaksMin)
}
},
selectTableRow(key, index) {
this.selectedKeys = [key]
this.$refs.tableRef.scrollIntoView(index)
},
// 设置红色标记线的位置
setMarkLineXAxis(xAxis) {
const markLineOption = this.option.series[0].markLine.data[0]
markLineOption.xAxis = xAxis
const { xAxis: chartXAxisOption } = this.option
const { max, min } = chartXAxisOption
// 如果不在范围内
if (xAxis >= max || xAxis <= min) {
const halfDiff = (max - min) / 2
const lastChannel = this.channelCountChart.pointlist[this.channelCountChart.pointlist.length - 1].x
let nextMax = xAxis + halfDiff
let nextMin = xAxis - halfDiff
chartXAxisOption.max = nextMax > lastChannel ? lastChannel : nextMax
chartXAxisOption.min = nextMin < 1 ? 1 : nextMin
this.setThumbnailRange(chartXAxisOption.min, chartXAxisOption.max)
this.adjustArea()
}
},
getMarkLineXAxis() {
const markLineOption = this.option.series[0].markLine.data[0]
return markLineOption.xAxis
},
// 获取右下角possible nuclide 和 identified nuclide
async getSelPosNuclide(row) {
this.model.possibleNuclide = ''
this.model.identifiedNuclide = ''
this.model.tolerance = 0.5
if (!row._possible) {
this.$set(row, '_loading', true)
try {
const { sampleId, inputFileName: fileName } = this.sampleData
const { success, result, message } = await getAction('/gamma/getSelPosNuclide', {
sampleId,
channel: Math.round(row.peakCentroid),
fileName,
})
if (success) {
const { possible } = result
this.$set(row, '_possible', possible)
} else {
this.$message.error(message)
}
} catch (error) {
console.error(error)
} finally {
row._loading = false
}
}
},
async handleToleranceChange(val) {
// this.selectedTableItem._loading = true
this.searchParam.energy = this.selectedTableItem.energy
this.searchParam.tolerance = val
try {
const { sampleId, inputFileName: fileName } = this.sampleData
const { success, result, message } = await getAction('/gamma/searchNuclide', {
sampleId,
fileName,
...this.searchParam,
})
if (success) {
const { list } = result
this.selectedTableItem._possible = list
} else {
this.$message.error(message)
}
} catch (error) {
console.error(error)
} finally {
// this.selectedTableItem._loading = false
}
},
// 显示peak comment弹窗
handleAddPeakComment() {
if (!this.selectedKeys.length) {
this.$message.warn('Please Select a Peak that You Want to Add Comment!')
return
}
this.peakCommentModalVisible = true
},
// 显示general comment弹窗
handleAddGeneralComment() {
this.generalCommentModalVisible = true
},
// Insert按钮
handleInsert() {
this.isFitting = false
const { rg_high, rg_low } = this.BaseCtrls
if (!this.currChannel || this.currChannel <= rg_low + 1 || this.currChannel >= rg_high - 1) {
this.$message.warn("Couldn't insert peak, maybe out of range")
return
}
this.channel_1 = this.currChannel
this.fitPeaksAndBaselineModalVisible = true
this.isInsertPeak = true
},
// 点击 Fit Peak XXX 弹窗中的 Peaks 按钮
handleInsertSuccess(result) {
const {
allData,
barChart,
channelBaseLineChart,
channelPeakChart,
shadowChannelChart,
shadowEnergyChart,
shapeChannelData,
shapeEnergyData,
table,
} = result
this.$bus.$emit('gammaRefresh', {
allData,
channelPeakChart,
shadowChannelChart,
shadowEnergyChart,
shapeChannelData,
shapeEnergyData,
peak: table,
barChart: this.barChart,
})
this.channelPeakChart = channelPeakChart
this.channelBaseLineChart = channelBaseLineChart
this.barChart = barChart
this.setChartOption(
channelBaseLineChart,
this.channelCountChart,
channelPeakChart,
this.channelBaseCPChart,
barChart
)
this.list = table
this.handleTableRowClick(this.list[this.curRow])
},
// 点击 Fit Peak XXX 弹窗中的 Cancel 按钮
handleCancelSuccess(result) {
const { channelPeakChart, table } = result
this.channelPeakChart = channelPeakChart
this.setChartOption(
this.channelBaseLineChart,
this.channelCountChart,
channelPeakChart,
this.channelBaseCPChart,
this.barChart
)
this.list = table
},
// 删除
handleDel() {
this.isFitting = false
if (!this.selectedKeys.length) {
this.$message.warn('No peak to delete.')
return
}
this.$confirm({
title: 'Warning',
content: 'Are you sure to delete this peak?',
cancelButtonProps: {
props: {
type: 'warn',
},
},
onOk: async () => {
// this.list.splice(findIndex, 1)
// this.selectedKeys = []
// const seriesIndex = this.option.series.findIndex(item => {
// return item.name == 'Peak_' + willDelKey
// })
// this.opts.notMerge = true
// this.option.series.splice(seriesIndex, 1)
// this.channelPeakChart.splice(findIndex, 1)
// this.$nextTick(() => {
// this.resetChartOpts()
// })
try {
const { inputFileName: fileName } = this.sampleData
const { success, result, message } = await getAction('/gamma/deletePeak', {
fileName,
curRow: this.curRow,
})
if (success) {
const {
allData,
channelPeakChart,
shadowChannelChart,
shadowEnergyChart,
shapeChannelData,
shapeEnergyData,
table,
} = result
this.$bus.$emit('gammaRefresh', {
allData,
channelPeakChart,
shadowChannelChart,
shadowEnergyChart,
shapeChannelData,
shapeEnergyData,
peak: table,
barChart: this.barChart,
})
this.opts.notMerge = true
this.channelPeakChart = channelPeakChart
const series = []
// 推入旧的BaseLine
series.push(this.buildBaseLine(this.channelBaseLineChart))
// 推入旧的Count
series.push(this.buildCountLine(this.channelCountChart))
// 推入Peak
series.push(...this.buildPeaks(channelPeakChart))
// 推入旧的基线控制点
series.push(this.buildCtrlPoint(this.channelBaseCPChart))
this.list = table
this.option.series = series
this.$nextTick(() => {
this.resetChartOpts()
})
this.selectedKeys = []
this.selectedTableItem = null
} else {
this.$message.error(message)
}
} catch (error) {
console.error(error)
}
},
})
},
// 重置图表配置
resetChartOpts() {
this.opts.notMerge = false
this.option.brush = { toolbox: [] }
},
// 匹配
handleFit() {
if (!this.channelPeakChart || !this.channelPeakChart.length) {
this.$message.warn('No peak to fit.')
return
}
this.isFitting = true
this.firstFittingChannel = null
},
// 表格的行点击
handleTableRowClick(row, index) {
if (this.selectedTableItem == row) {
return
}
const channel = Math.round(row.peakCentroid)
this.currChannel = channel
this.option.series[0].markLine.data[0].xAxis = channel
const { xAxis: chartXAxisOption } = this.option
const { max, min } = chartXAxisOption
// 如果不在范围内
if (channel >= max || channel <= min) {
const halfDiff = (max - min) / 2
const lastChannel = this.channelCountChart.pointlist[this.channelCountChart.pointlist.length - 1].x
let nextMax = channel + halfDiff
let nextMin = channel - halfDiff
chartXAxisOption.max = nextMax > lastChannel ? lastChannel : nextMax
chartXAxisOption.min = nextMin < 1 ? 1 : nextMin
this.setThumbnailRange(chartXAxisOption.min, chartXAxisOption.max)
}
this.getSelPosNuclide(row)
this.selectedTableItem = row
this.adjustArea()
},
// 鼠标按下时开启可刷选状态
handleMouseDown() {
if (this.isModifying) {
return
}
const chart = this.$refs.chartRef.getChartInstance()
chart.dispatchAction({
type: 'takeGlobalCursor',
// 如果想变为“可刷选状态”,必须设置。不设置则会关闭“可刷选状态”。
key: 'brush',
brushOption: {
// 参见 brush 组件的 brushType。如果设置为 false 则关闭“可刷选状态”。
brushType: 'rect',
},
})
},
handleMouseUp() {
setTimeout(() => {
const chart = this.$refs.chartRef.getChartInstance()
this.clearBrush(chart)
}, 0)
},
clearBrush(chart) {
// 清理刷选的范围
chart.dispatchAction({
type: 'brush',
areas: [],
})
// 改为不可刷选状态
chart.dispatchAction({
type: 'takeGlobalCursor',
})
},
// 刷选完毕时
handleBrushEnd(param) {
const chart = this.$refs.chartRef.getChartInstance()
const areas = param.areas[0]
if (areas) {
const range = areas.range
const [[minX, maxX], [minY, maxY]] = range
const point1 = chart.convertFromPixel({ seriesIndex: 0 }, [minX, minY]).map((num) => parseInt(num.toFixed()))
const point2 = chart.convertFromPixel({ seriesIndex: 0 }, [maxX, maxY]).map((num) => parseInt(num.toFixed()))
const xAxisMax = chart.getModel().getComponent('xAxis').axis.scale._extent[1]
const yAxisMax = this.option.yAxis.max
let [x1, y2, x2, y1] = [...point1, ...point2] // 根据解析出的数据确定真实的范围
const xAxisLimit = rangeNumber(1, xAxisMax)
const yAxisLimit = rangeNumber(0.1, yAxisMax)
x1 = xAxisLimit(x1)
x2 = xAxisLimit(x2)
y1 = yAxisLimit(y1)
y2 = yAxisLimit(y2)
this.option.xAxis.min = x1
this.option.xAxis.max = x2
this.option.yAxis.min = y1
this.option.yAxis.max = y2
this.adjustArea()
this.setThumbnailRange(x1, x2)
if (this.btnGroupType == 2) {
this.buildRect()
}
}
this.clearBrush(chart)
},
// 查找barChart范围内的最大值
setThumbnailRange(x1, x2) {
const slicedArr = this.barChart.slice(x1 - 1, x2)
const yData = slicedArr.map(({ y }) => y)
const max = Math.max(...yData)
const min = Math.min(...yData)
const thumbnailYMax = Math.max(Math.abs(max), Math.abs(min))
this.thumbnailOption.xAxis.min = x1
this.thumbnailOption.xAxis.max = x2
this.thumbnailOption.yAxis.max = thumbnailYMax
this.thumbnailOption.yAxis.min = -thumbnailYMax
},
handleResetChart() {
this.option.xAxis.min = 1
this.option.xAxis.max = 'dataMax'
this.option.yAxis.min = 0.1
this.option.yAxis.max = 'dataMax'
this.setThumbnailRange(1, this.barChart.length)
if (this.btnGroupType == 2) {
this.buildRect()
}
},
// 切换操作
handleSwitchOperation() {
// 切换到Base Line 和 Control Point 操作
if (this.btnGroupType == 1) {
this.btnGroupType = 2
this.baseCtrls_Copy = cloneDeep(this.BaseCtrls)
this.replotNeeded = false
// 供编辑的白色基线
const baseLineEditSeries = buildLineSeries(
'BaseLine_Edit',
this.baseCtrls_Copy.baseline.map((val, index) => [index + 1, val]),
this.colorConfig.Color_Fitbase || '#fff',
{
zlevel: 21,
}
)
this.option.series.push(baseLineEditSeries)
this.$nextTick(() => {
this.buildRect()
})
}
// 切换回 Peak 操作
else {
this.btnGroupType = 1
this.opts.notMerge = true
const baseLineEditSeries = findSeriesByName(this.option.series, 'BaseLine_Edit')
const index = this.option.series.findIndex((item) => item == baseLineEditSeries)
this.option.series.splice(index, 1)
this.clearRect()
const baseLineSeries = findSeriesByName(this.option.series, 'BaseLine')
baseLineSeries.data = transformPointListData(this.channelBaseLineChart.pointlist) // 恢复基线
const baseLineCP = findSeriesByName(this.option.series, 'BaseLine_Ctrl_Point')
baseLineCP.data = this.buildCPPointData(this.channelBaseCPChart)
this.redrawPeaks(this.channelPeakChart)
this.$nextTick(() => {
this.resetChartOpts()
})
}
this.isModifying = false
this.isFitting = false
this.clearOperationStack()
},
// 根据数据绘制小方块
buildRect() {
this.$refs.rectListRef.init()
},
clearRect() {
const rectListRef = this.$refs.rectListRef
if (rectListRef) {
rectListRef.clear()
}
},
// 小方块移动
handleMove(yctrl) {
this.baseCtrls_Copy.yctrl = yctrl
this.redrawBaseLine()
},
// 小方块移动完毕
handleMoveEnd(prevYAxis, index) {
this.isModifying = false
this.pushOperationStack(Operators.MODIFY, {
index,
prevYAxis,
})
},
// 重新生成基线
redrawBaseLine() {
this.replotNeeded = true
try {
console.time('updateBaseLine')
const res = updateBaseLine(JSON.stringify(this.baseCtrls_Copy))
console.timeEnd('updateBaseLine')
const parsed = JSON.parse(res)
const { baseline } = parsed
const baseLineEditSeries = findSeriesByName(this.option.series, 'BaseLine_Edit')
baseLineEditSeries.data = baseline.map((val, index) => [index + 1, val])
this.baseCtrls_Copy.baseline = baseline
} catch (error) {
console.error(error)
}
},
// 重绘Peaks
redrawPeaks(peakList) {
this.option.series = this.option.series.filter((item) => {
return !item.name.includes('Peak_')
})
this.option.series.push(...this.buildPeaks(peakList))
},
/**
* 设置小方块可拖拽
*/
setGraphicDraggable(draggable) {
this.isModifying = draggable
},
// 在当前选中的红线位置新增控制点
handleAddCP() {
this.setGraphicDraggable(false)
const { rg_high, rg_low } = this.BaseCtrls
if (!this.currChannel || this.currChannel < rg_low || this.currChannel > rg_high) {
this.$message.warn("Can't insert Control Point out of range")
return
}
const { xctrl, yctrl, baseline, yslope } = this.baseCtrls_Copy
let i = 0 // 记录新控制点在列表中的位置
for (; i < xctrl.length; ++i) {
const currCP = xctrl[i]
if (currCP >= this.currChannel) {
if (currCP == this.currChannel) {
this.$message.warn(`The new control point in channel ${this.currChannel} exists, can't introduce twice`)
return
}
break
}
}
// 新增的控制点跟基线的值平齐
const yAxis = baseline[this.currChannel - 1]
xctrl.splice(i, 0, this.currChannel)
yctrl.splice(i, 0, yAxis)
yslope.splice(i, 0, 0)
this.buildRect()
this.pushOperationStack(Operators.ADD, { index: i })
},
// 移除控制点
handleRemoveCP() {
this.setGraphicDraggable(false)
const { xctrl, yctrl, yslope } = this.baseCtrls_Copy
// find nearest control-point
let i = 1
for (; i < xctrl.length; ++i) {
const currXAxis = xctrl[i]
if (currXAxis >= this.currChannel) {
const prevX = xctrl[i - 1]
if (currXAxis - this.currChannel > this.currChannel - prevX) --i
break
}
}
if (i == 0 || i >= xctrl.length - 1) {
this.$message.warn("Can't remove first/last control point")
return
}
const [removeXAxis] = xctrl.splice(i, 1)
const [removeYAxis] = yctrl.splice(i, 1)
const [removeYSlope] = yslope.splice(i, 1)
this.buildRect()
this.redrawBaseLine()
this.pushOperationStack(Operators.REMOVE, {
index: i,
removeXAxis,
removeYAxis,
removeYSlope,
})
},
// 修改控制点
handleModifyCP() {
this.setGraphicDraggable(!this.isModifying)
},
// 编辑斜率
handleEditSlope() {
this.setGraphicDraggable(false)
const { xctrl, yslope } = this.baseCtrls_Copy
if (!xctrl.length) {
this.$message.warn('No control points to be edited')
return
}
// find nearest control-point
let i = 1,
n = xctrl.length
for (; i < n; ++i) {
const currXCtrl = xctrl[i]
const prevXCtrl = xctrl[i - 1]
if (currXCtrl >= this.currChannel) {
if (currXCtrl - this.currChannel > this.currChannel - prevXCtrl) {
--i
}
break
}
}
if (i == n) i = n - 1
this.$refs.editSlopeModal.open({
index: i,
value: yslope[i],
allowNaN: !(i == 0 || i == n - 1),
})
},
// 确认编辑斜率
handleSlopeChange(slope, index, prevSlope) {
if (slope === prevSlope) {
return
}
const { yslope } = this.baseCtrls_Copy
yslope[index] = slope
this.pushOperationStack(Operators.SLOPE_CHANGE, {
index,
slope: prevSlope,
})
this.redrawBaseLine()
this.buildRect()
},
// 撤销
handleUndo() {
this.setGraphicDraggable(false)
this.popOperationStack()
},
// 将原先的基线和控制点移动到新位置
async handleReplot() {
if (!this.replotNeeded) {
return
}
try {
const { inputFileName: fileName } = this.sampleData
this.isReploting = true
const { success, result, message } = await postAction('/gamma/replotBaseLine', {
...this.baseCtrls_Copy,
fileName,
replotNeeded: this.replotNeeded,
})
if (success) {
const { chartData, peakSet, shapeData } = result
const { xctrl, yctrl, yslope, baseline } = this.baseCtrls_Copy
const baseLineSeries = findSeriesByName(this.option.series, 'BaseLine')
baseLineSeries.data = baseline.map((val, index) => [index + 1, val])
const baseLineCP = findSeriesByName(this.option.series, 'BaseLine_Ctrl_Point')
// 第一个控制点(因为第一个和最后一个不会被删除)
const firstCP = this.channelBaseCPChart[0]
const { color, size } = firstCP
const baseCPPoints = xctrl.map((xAxis, index) => {
return {
size,
color,
point: {
x: xAxis,
y: yctrl[index],
},
}
})
baseLineCP.data = this.buildCPPointData(baseCPPoints)
this.opts.notMerge = true
this.redrawPeaks(peakSet)
this.$nextTick(() => {
this.resetChartOpts()
})
this.replotNeeded = false
} else {
this.$message.error(message)
}
} catch (error) {
console.error(error)
} finally {
this.isReploting = false
}
},
/**
* 构建基线控制点数据
* @param {Array} controlPointList
**/
buildCPPointData(controlPointList) {
return controlPointList.map(({ size, color, point: { x, y } }) => {
return {
value: [x, y],
itemStyle: {
color: 'transparent',
borderColor: color,
borderWidth: size / 2,
},
}
})
},
// 确定对Baseline Control Points 的操作
async handleAccept() {
// this.BaseCtrls = cloneDeep(this.baseCtrls_Copy)
// const { baseline, xctrl, yctrl } = this.BaseCtrls
// this.channelBaseLineChart.pointlist = baseline.map((val, index) => {
// return {
// x: index + 1,
// y: val
// }
// })
// this.channelBaseCPChart = xctrl.map((val, index) => {
// return {
// color: this.channelBaseCPChart[0].color,
// name: index.toString(),
// point: {
// x: val,
// y: yctrl[index]
// },
// size: 4
// }
// })
const { inputFileName: fileName } = this.sampleData
try {
this.isAccepting = true
const { success, result, message } = await postAction('/gamma/acceptBaseLine', {
...this.baseCtrls_Copy,
fileName,
})
if (success) {
this.BaseCtrls = cloneDeep(this.baseCtrls_Copy)
const {
allData,
barChart,
channelBaseLineChart,
peakSet,
shadowChannelChart,
shadowEnergyChart,
shapeChannelData,
shapeData,
shapeEnergyData,
} = result
this.channelBaseLineChart = channelBaseLineChart
this.channelPeakChart = peakSet
this.shadowChannelChart = shadowChannelChart
this.channelBaseCPChart = shapeChannelData
this.barChart = barChart
this.btnGroupType = 1
this.opts.notMerge = true
this.clearRect()
this.setChartOption(channelBaseLineChart, this.channelCountChart, peakSet, this.channelBaseCPChart, barChart)
this.$nextTick(() => {
this.resetChartOpts()
})
this.$bus.$emit('accept', { ...result, BaseCtrls: cloneDeep(this.baseCtrls_Copy) })
} else {
this.$message.error(message)
}
} catch (error) {
console.error(error)
} finally {
this.isAccepting = false
}
},
// 右下角添加当前选中的nuclide
async handleAddNuclide() {
const nuclides = this.selectedTableItem.nuclides
const possibleNuclide = this.model.possibleNuclide
if (!nuclides.includes(possibleNuclide)) {
if (this.selectedTableItem._adding) {
return
}
try {
this.$set(this.selectedTableItem, '_adding', true)
const { inputFileName: fileName } = this.sampleData
const { success, message } = await postAction('/gamma/addNuclide', {
curRow: this.curRow,
nuclideName: possibleNuclide,
fileName,
list_identify: nuclides,
energyTolerance: this.model.tolerance
})
if (success) {
nuclides.push(possibleNuclide)
} else {
this.$message.error(message)
}
} catch (error) {
console.error(error)
} finally {
this.selectedTableItem._adding = false
}
}
},
// 右下角删除当前选中的nuclide
async handleDelNuclide() {
if (!this.model.identifiedNuclide) {
return
}
const nuclides = this.selectedTableItem.nuclides
if (this.selectedTableItem._deleting) {
return
}
const findIndex = nuclides.findIndex((nuclide) => nuclide == this.model.identifiedNuclide)
if (-1 !== findIndex) {
try {
this.$set(this.selectedTableItem, '_deleting', true)
const { inputFileName: fileName } = this.sampleData
const { success, result, message } = await postAction('/gamma/deleteNuclide', {
curRow: this.curRow,
nuclideName: this.model.identifiedNuclide,
fileName,
list_identify: nuclides,
})
if (success) {
const { identify, table } = result
this.selectedTableItem.nuclides = identify
this.list = table
} else {
this.$message.error(message)
}
} catch (error) {
console.error(error)
} finally {
this.selectedTableItem._deleting = false
}
}
},
// 构建baseline
buildBaseLine(channelBaseLineChart) {
return buildLineSeries(
'BaseLine',
transformPointListData(channelBaseLineChart.pointlist),
channelBaseLineChart.color,
{
markLine: {
silent: true,
symbol: 'none',
label: {
show: false,
},
lineStyle: {
color: 'red',
width: 1,
},
data: [{ xAxis: -1 }],
},
zlevel: 10,
}
)
},
// 构建count
buildCountLine(channelCountChart) {
return buildLineSeries('CountChart', transformPointListData(channelCountChart.pointlist), channelCountChart.color)
},
// 构建Peaks
buildPeaks(channelPeakChart) {
return channelPeakChart.map((item, index) => {
return buildLineSeries('Peak_' + (index + 1), transformPointListData(item.pointlist), item.color)
})
},
// 构建基线控制点
buildCtrlPoint(channelBaseCPChart) {
return {
name: 'BaseLine_Ctrl_Point',
type: 'scatter',
data: this.buildCPPointData(channelBaseCPChart),
silent: true,
animation: false,
zlevel: 20,
}
},
// 构建缩略图
buildBarChart(barChart) {
return {
name: 'BarChart',
type: 'bar',
data: barChart.map(({ x, y }) => [x, y]),
itemStyle: {
color: '#fff',
},
lineStyle: {
width: 1,
},
symbol: 'none',
symbolSize: 1,
emphasis: {
disabled: true,
},
animation: false,
silent: true,
}
},
/**
* 推入操作
* @param {*} operator 操作符
* @param {*} operand 操作数
*/
pushOperationStack(operator, operand) {
this.operationStack.push({
operator,
operand,
})
},
/**
* 弹出操作
*/
popOperationStack() {
const { operator, operand } = this.operationStack.pop()
const { index } = operand
const { xctrl, yctrl, yslope } = this.baseCtrls_Copy
switch (operator) {
case Operators.ADD:
xctrl.splice(index, 1)
yctrl.splice(index, 1)
yslope.splice(index, 1)
break
case Operators.MODIFY:
const { prevYAxis } = operand
// 恢复点的y轴位置
yctrl[index] = prevYAxis
break
case Operators.REMOVE:
const { removeXAxis, removeYAxis, removeYSlope } = operand
xctrl.splice(index, 0, removeXAxis)
yctrl.splice(index, 0, removeYAxis)
yslope.splice(index, 0, removeYSlope)
break
case Operators.SLOPE_CHANGE:
const { slope } = operand
yslope[index] = slope
break
}
this.buildRect()
// 恢复基线位置
this.redrawBaseLine()
},
/**
* 清理操作栈
*/
clearOperationStack() {
this.operationStack = []
},
handleResize() {
this.$refs.chartRef.resize()
this.$refs.thumbnailRef.resize()
},
handleFullScreenChange(isFullScreen) {
this.columns[7].width = isFullScreen ? 180 : 120
},
},
computed: {
curRow() {
const [selectedKey] = this.selectedKeys
const findIndex = this.list.findIndex((item) => item.index == selectedKey)
return findIndex
},
isOperationStackEmpty() {
return this.operationStack.length == 0
},
},
}
</script>
<style lang="less" scoped>
.interactive-analysis-tools {
display: flex;
padding-top: 5px;
gap: 20px;
&-left {
flex: 1;
overflow: hidden;
.chart {
height: 331px;
position: relative;
}
.thumbnail {
height: 50px;
margin: 10px 30px 35px 80px;
background-color: #255369;
}
.table {
.title {
color: #0cebc9;
font-size: 20px;
text-align: center;
margin-bottom: 10px;
user-select: none;
span {
cursor: pointer;
margin: 0 5px;
}
}
.custom-table {
&.has-data {
::v-deep {
.ant-table-body {
height: 288px;
background-color: #06282a;
}
}
}
::v-deep {
.ant-table-placeholder {
height: 289px;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.operators {
display: flex;
margin-top: 10px;
gap: 10px;
.ant-btn {
flex: 1;
}
}
}
}
&-right {
width: 290px;
flex-shrink: 0;
.peak-box {
height: 326px;
&-item:not(:last-child) {
margin-bottom: 10px;
}
.symbol {
display: flex;
.ant-btn {
flex: 1;
&:first-child {
margin-right: 10px;
}
}
}
}
.base-line {
margin-top: 136px;
}
.reset-btn-box {
margin-top: 20px;
margin-bottom: 30px;
}
.identify-box {
.tolerance {
::v-deep {
.ant-form-item {
margin-bottom: 10px;
&-control-wrapper {
flex: 1;
}
&-control {
width: 100%;
}
}
}
.ant-input-number {
width: 100%;
}
}
.identify-item {
margin-bottom: 10px;
.title {
background-color: #497e9d;
height: 30px;
line-height: 30px;
text-align: center;
font-size: 16px;
}
.content {
height: 80px;
background-color: #275466;
margin-top: 10px;
padding: 5px;
overflow: auto;
.item {
cursor: pointer;
line-height: 26px;
padding: 0 5px;
&.active {
background: #296d81;
}
}
}
}
.identify-operators {
display: flex;
gap: 10px;
.text {
flex: 1;
line-height: 32px;
background-color: #285366;
padding: 0 10px;
}
.ant-btn {
width: 50px;
padding-left: 5px;
padding-right: 5px;
}
}
}
}
.ant-btn {
width: 100%;
}
}
::v-deep {
.ant-modal {
top: 5px;
padding-bottom: 7px;
}
}
.is-modify,
.is-fitting {
color: #f00;
}
</style>
<style lang="less">
.interactive-analysis-tools-dialog.fullscreen {
.interactive-analysis-tools {
height: calc(100vh - 90px);
&-left {
display: flex;
flex-direction: column;
.chart {
flex: 1;
}
.thumbnail {
height: 60px;
}
.table {
height: 397px;
.custom-table {
height: calc(100% - 82px);
.ant-spin-nested-loading,
.ant-spin-container,
.ant-table,
.ant-table-content,
.ant-table-scroll {
height: 100%;
}
.ant-table-body {
height: calc(100% - 25px) !important;
max-height: calc(100% - 25px) !important;
}
}
}
}
&-right {
display: flex;
flex-direction: column;
.identify-box {
flex: 1;
padding-top: 5px;
overflow: hidden;
.title-over-border {
height: 100%;
&-content {
display: flex;
flex-direction: column;
.identify-item {
flex: 1;
overflow: auto;
.ant-spin-nested-loading,
.content {
height: calc(100% - 40px);
.ant-spin-container,
.content {
height: 100%;
}
}
}
}
}
}
}
}
}
</style>