import React from "react";
import {
observable,
computed,
runInAction,
action,
toJS,
when,
reaction,
makeObservable,
IReactionDisposer
} from "mobx";
import { observer } from "mobx-react";
import { objectEqual } from "eez-studio-shared/util";
import {
beginTransaction,
commitTransaction,
IStore
} from "eez-studio-shared/store";
import { UNITS, TIME_UNIT } from "eez-studio-shared/units";
import { scheduleTask, Priority } from "eez-studio-shared/scheduler";
import { Point } from "eez-studio-shared/geometry";
import type * as I10nModule from "eez-studio-shared/i10n";
import { Dialog, showDialog } from "eez-studio-ui/dialog";
import { PropertyList } from "eez-studio-ui/properties";
import {
ChartController,
ChartMode,
IAxisModel,
LineController,
ChartsController,
IChartsController,
MeasurementsModel,
getNearestValuePoint,
IAxisController,
IMeasurementsModel
} from "eez-studio-ui/chart/chart";
import { RulersModel, IRulersModel } from "eez-studio-ui/chart/rulers";
import { WaveformLineView } from "eez-studio-ui/chart/WaveformLineView";
import { WaveformFormat } from "eez-studio-ui/chart/WaveformFormat";
import { initValuesAccesor } from "eez-studio-ui/chart/value-accesor";
import * as notification from "eez-studio-ui/notification";
import {
logUpdate,
IActivityLogEntry
} from "instrument/window/history/activity-log";
import { checkMime } from "instrument/connection/file-type";
import { ChartPreview } from "instrument/window/chart-preview";
import { FileHistoryItem } from "instrument/window/history/items/file";
import { WaveformTimeAxisModel } from "instrument/window/waveform/time-axis";
import {
IToolbarOptions,
WaveformToolbar
} from "instrument/window/waveform/toolbar";
import type { ChartsDisplayOption } from "instrument/window/lists/common-tools";
import { ViewOptions } from "instrument/window/waveform/ViewOptions";
import { WaveformAxisModel } from "instrument/window/waveform/WaveformAxisModel";
import { WaveformDefinitionProperties } from "instrument/window/waveform/WaveformDefinitionProperties";
import type { IAppStore } from "instrument/window/history/history";
import { capitalize } from "eez-studio-shared/string";
////////////////////////////////////////////////////////////////////////////////
export interface IWaveformDefinition {
samplingRate: number;
format: WaveformFormat;
unitName: keyof typeof UNITS;
color?: string;
colorInverse?: string;
label?: string;
offset: number;
scale: number;
cachedMinValue: number;
cachedMaxValue: number;
}
export interface IWaveformHistoryItemMessage {
waveformDefinition: IWaveformDefinition;
viewOptions: ViewOptions;
rulers: RulersModel;
measurements: RulersModel;
horizontalScale?: number;
verticalScale?: number;
}
////////////////////////////////////////////////////////////////////////////////
export function isWaveform(activityLogEntry: IActivityLogEntry) {
return (
(activityLogEntry as any).waveformDefinition ||
checkMime(activityLogEntry.message, [
"application/eez-binary-list",
"application/eez-raw",
"text/csv"
])
);
}
////////////////////////////////////////////////////////////////////////////////
export class WaveformChartsController extends ChartsController {
constructor(
public waveform: Waveform,
mode: ChartMode,
xAxisModel: IAxisModel
) {
super(mode, xAxisModel, waveform.viewOptions);
}
get chartViewOptionsProps() {
return {
showRenderAlgorithm: true,
showShowSampledDataOption: false
};
}
get supportRulers() {
return true;
}
}
////////////////////////////////////////////////////////////////////////////////
export class Waveform extends FileHistoryItem {
dispose1: IReactionDisposer | undefined;
dispose2: IReactionDisposer | undefined;
dispose3: IReactionDisposer | undefined;
dispose4: IReactionDisposer | undefined;
dispose5: IReactionDisposer | undefined;
canBePartOfMultiChart = true;
constructor(
store: IStore,
activityLogEntry: IActivityLogEntry | FileHistoryItem,
private options?: { toolbar?: IToolbarOptions }
) {
super(store, activityLogEntry);
makeObservable(this, {
initWaveformDefinition: action.bound,
values: computed,
waveformHistoryItemMessage: computed,
waveformDefinition: observable.shallow,
length: observable,
xAxisUnit: computed,
samplingRate: computed,
yAxisDefaultSubdivisionOffsetAndScale: computed
});
const message = JSON.parse(this.message);
this.viewOptions = new ViewOptions(message.viewOptions);
this.xAxisModel.dynamic.zoomMode = this.xAxisModel.fixed.zoomMode =
this.viewOptions.axesLines.defaultZoomMode || "all";
this.rulers = new RulersModel(message.rulers);
this.rulers.initYRulers(1);
this.measurements = new MeasurementsModel(message.measurements);
when(
() => this.transferSucceeded,
() => {
scheduleTask(
`Load waveform ${this.id}`,
Priority.Lowest,
async () => this.initWaveformDefinition()
);
}
);
// save waveformDefinition when changed
this.dispose1 = reaction(
() => toJS(this.waveformDefinition),
waveformDefinition => {
const message = JSON.parse(this.message);
if (
!objectEqual(message.waveformDefinition, waveformDefinition)
) {
const messageStr = JSON.stringify(
Object.assign(message, {
waveformDefinition
})
);
runInAction(() => (this.message = messageStr));
logUpdate(
this.store,
{
id: this.id,
oid: this.oid,
message: messageStr
},
{
undoable: false
}
);
}
}
);
// save viewOptions when changed
this.dispose2 = reaction(
() => toJS(this.viewOptions),
viewOptions => {
const message = JSON.parse(this.message);
if (!objectEqual(message.viewOptions, viewOptions)) {
const messageStr = JSON.stringify(
Object.assign(message, {
viewOptions
})
);
runInAction(() => (this.message = messageStr));
logUpdate(
this.store,
{
id: this.id,
oid: this.oid,
message: messageStr
},
{
undoable: false
}
);
}
}
);
// save rulers when changed
this.dispose3 = reaction(
() => toJS(this.rulers),
rulers => {
if (rulers.pauseDbUpdate) {
return;
}
delete rulers.pauseDbUpdate;
const message = JSON.parse(this.message);
if (!objectEqual(message.rulers, rulers)) {
const messageStr = JSON.stringify(
Object.assign(message, {
rulers
})
);
runInAction(() => (this.message = messageStr));
logUpdate(
this.store,
{
id: this.id,
oid: this.oid,
message: messageStr
},
{
undoable: false
}
);
}
}
);
// save measurements when changed
this.dispose4 = reaction(
() => toJS(this.measurements),
measurements => {
const message = JSON.parse(this.message);
if (!objectEqual(message.measurements, measurements)) {
const messageStr = JSON.stringify(
Object.assign(message, {
measurements
})
);
runInAction(() => (this.message = messageStr));
logUpdate(
this.store,
{
id: this.id,
oid: this.oid,
message: messageStr
},
{
undoable: false
}
);
}
}
);
//
this.dispose5 = reaction(
() => JSON.parse(this.message),
message => {
const waveformDefinition = toJS(this.waveformDefinition);
if (
!objectEqual(message.waveformDefinition, waveformDefinition)
) {
this.initWaveformDefinition();
}
}
);
}
initValuesAccesor() {
initValuesAccesor(this);
}
findRange() {
let minValue = Number.POSITIVE_INFINITY;
let maxValue = Number.NEGATIVE_INFINITY;
if (this.length > 0) {
minValue = this.waveformData(0);
maxValue = this.waveformData(0);
for (let i = 1; i < this.length; i++) {
const value = this.waveformData(i);
if (value < minValue) {
minValue = value;
} else if (value > maxValue) {
maxValue = value;
}
}
} else {
minValue = 0;
maxValue = 0;
}
this.waveformDefinition.cachedMinValue = minValue;
this.waveformDefinition.cachedMaxValue = maxValue;
}
guessWaveformFormat() {
let format: WaveformFormat = WaveformFormat.UNKNOWN;
if (this.fileTypeAsDisplayString === "text/csv") {
format = WaveformFormat.CSV_STRING;
} else {
format = WaveformFormat.RIGOL_BYTE;
}
return format;
}
getDefaultWaveformDefinition(): IWaveformDefinition {
return {
samplingRate: 1,
format: this.guessWaveformFormat(),
unitName: "unknown",
offset: 0,
scale: 1,
cachedMinValue: 0,
cachedMaxValue: 0
};
}
migrateWaveformDefinition() {
let migrated = false;
if (this.waveformDefinition.samplingRate === undefined) {
this.waveformDefinition.samplingRate = 1000000;
migrated = true;
}
if (this.waveformDefinition.offset === undefined) {
this.waveformDefinition.offset = 0;
migrated = true;
}
if (this.waveformDefinition.scale === undefined) {
this.waveformDefinition.scale = 1;
migrated = true;
}
if (
this.waveformDefinition.cachedMinValue == null ||
this.waveformDefinition.cachedMaxValue == null
) {
migrated = true;
}
return migrated;
}
initWaveformDefinition() {
let migrated = false;
if (this.waveformHistoryItemMessage.waveformDefinition) {
const oldFormat =
this.waveformDefinition && this.waveformDefinition.format;
const oldOffset =
this.waveformDefinition && this.waveformDefinition.offset;
const oldScale =
this.waveformDefinition && this.waveformDefinition.scale;
this.waveformDefinition =
this.waveformHistoryItemMessage.waveformDefinition;
migrated = this.migrateWaveformDefinition();
if (!migrated) {
if (
oldFormat !== this.waveformDefinition.format ||
oldOffset !== this.waveformDefinition.offset ||
oldScale !== this.waveformDefinition.scale
) {
// recalculate range
migrated = true;
}
}
} else {
this.waveformDefinition = this.getDefaultWaveformDefinition();
migrated = true;
}
this.initValuesAccesor();
if (migrated) {
this.findRange();
}
}
get values(): any {
if (typeof this.data === "string") {
return new Uint8Array(Buffer.from(this.data, "binary").buffer);
}
return this.data;
}
get waveformHistoryItemMessage(): IWaveformHistoryItemMessage {
return JSON.parse(this.message);
}
waveformDefinition = this.getDefaultWaveformDefinition();
length: number = 0;
get format() {
return this.waveformDefinition.format;
}
get offset() {
return this.waveformDefinition.offset;
}
set offset(value: number) {
this.waveformDefinition.offset = value;
}
get scale() {
return this.waveformDefinition.scale;
}
set scale(value: number) {
this.waveformDefinition.scale = value;
}
get xAxisUnit() {
return TIME_UNIT;
}
get samplingRate() {
return this.waveformDefinition.samplingRate;
}
viewOptions: ViewOptions;
rulers: IRulersModel;
measurements: IMeasurementsModel;
xAxisModel = new WaveformTimeAxisModel(this);
chartsController: IChartsController;
createChartsController(
appStore: IAppStore,
displayOption: ChartsDisplayOption,
mode: ChartMode
): IChartsController {
if (
this.chartsController &&
this.chartsController.mode === mode &&
this.chartsController.xAxisController.axisModel ===
this.xAxisModel &&
this.chartsController.chartControllers[0].yAxisController
.axisModel === this.yAxisModel
) {
return this.chartsController;
}
if (this.chartsController) {
this.chartsController.destroy();
}
const chartsController = new WaveformChartsController(
this,
mode,
this.xAxisModel
);
this.chartsController = chartsController;
this.xAxisModel.chartsController = chartsController;
chartsController.chartControllers = [
this.createChartController(
chartsController,
"unknown",
this.yAxisModel
)
];
if (!chartsController.isMultiWaveformChartsController) {
chartsController.createRulersController(this.rulers);
chartsController.createMeasurementsController(this.measurements);
}
return chartsController;
}
createChartController(
chartsController: IChartsController,
id: string,
axisModel: IAxisModel
) {
const chartController = new ChartController(chartsController, id);
chartController.createYAxisController(axisModel);
chartController.lineControllers.push(
new WaveformLineController(
"waveform-" + chartController.yAxisController.position,
this,
chartController.yAxisController
)
);
return chartController;
}
yAxisModel = new WaveformAxisModel(this, undefined);
value(index: number) {
return 0;
}
waveformData(index: number) {
return 0;
}
waveformDataToValue(waveformDataValue: number) {
return this.offset + waveformDataValue * this.scale;
}
get minValue() {
if (
this.waveformDefinition.format === WaveformFormat.RIGOL_BYTE ||
this.waveformDefinition.format === WaveformFormat.RIGOL_WORD
) {
return this.waveformDataToValue(
this.waveformDefinition.cachedMinValue
);
} else {
return this.waveformDefinition.cachedMinValue;
}
}
get maxValue() {
if (
this.waveformDefinition.format === WaveformFormat.RIGOL_BYTE ||
this.waveformDefinition.format === WaveformFormat.RIGOL_WORD
) {
return this.waveformDataToValue(
this.waveformDefinition.cachedMaxValue
);
} else {
return this.waveformDefinition.cachedMaxValue;
}
}
renderToolbar(chartsController: IChartsController): JSX.Element {
return (
);
}
openConfigurationDialog() {
showDialog();
}
get xAxisDefaultSubdivisionOffset(): number | undefined {
return this.waveformHistoryItemMessage.horizontalScale !== undefined
? 0
: undefined;
}
get xAxisDefaultSubdivisionScale() {
return this.waveformHistoryItemMessage.horizontalScale;
}
get yAxisDefaultSubdivisionOffsetAndScale() {
if (this.waveformHistoryItemMessage.verticalScale) {
const verticalScale = this.waveformHistoryItemMessage.verticalScale;
const min =
Math.floor(this.yAxisModel.minValue / verticalScale) *
verticalScale;
const max =
Math.ceil(this.yAxisModel.maxValue / verticalScale) *
verticalScale;
const subdivision =
this.waveformHistoryItemMessage.viewOptions.axesLines
.majorSubdivision.vertical;
return {
offset: (min + max) / 2 - (verticalScale * subdivision) / 2,
scale: verticalScale
};
}
return {
offset: undefined,
scale: undefined
};
}
get yAxisDefaultSubdivisionOffset(): number | undefined {
return this.yAxisDefaultSubdivisionOffsetAndScale.offset;
}
get yAxisDefaultSubdivisionScale() {
return this.yAxisDefaultSubdivisionOffsetAndScale.scale;
}
getPreviewElement(appStore: IAppStore) {
return ;
}
get valueUnit() {
return this.waveformDefinition.unitName;
}
isZoomable = true;
convertToCsv = () => {
return convertToCsv(this);
};
override dispose() {
super.dispose();
if (this.dispose1) {
this.dispose1();
}
if (this.dispose2) {
this.dispose2();
}
if (this.dispose3) {
this.dispose3();
}
if (this.dispose4) {
this.dispose4();
}
if (this.dispose5) {
this.dispose5();
}
}
}
////////////////////////////////////////////////////////////////////////////////
class WaveformLineController extends LineController {
constructor(
public id: string,
public waveform: Waveform,
yAxisController: IAxisController
) {
super(id, yAxisController);
makeObservable(this, {
yMin: computed,
yMax: computed
});
}
get yMin(): number {
return this.yAxisController.axisModel.minValue;
}
get yMax(): number {
return this.yAxisController.axisModel.maxValue;
}
getWaveformModel() {
return this.waveform;
}
getNearestValuePoint(point: Point): Point {
return getNearestValuePoint(
point,
this.xAxisController,
this.yAxisController,
this.waveform
);
}
render(): JSX.Element {
return ;
}
}
////////////////////////////////////////////////////////////////////////////////
const WaveformConfigurationDialog = observer(
class WaveformConfigurationDialog extends React.Component<{
waveform: Waveform;
}> {
waveformProperties: WaveformDefinitionProperties =
new WaveformDefinitionProperties(
this.props.waveform.waveformDefinition
);
handleSubmit = async () => {
const newWaveformDefinition =
await this.waveformProperties.checkValidity();
if (!newWaveformDefinition) {
return false;
}
if (
!objectEqual(
this.props.waveform.waveformDefinition,
newWaveformDefinition
)
) {
const message = JSON.stringify(
Object.assign({}, this.props.waveform.fileState, {
waveformDefinition: newWaveformDefinition
})
);
beginTransaction("Edit waveform configuration");
logUpdate(
this.props.waveform.store,
{
id: this.props.waveform.id,
oid: this.props.waveform.oid,
message
},
{
undoable: true
}
);
commitTransaction();
}
return true;
};
render() {
return (
);
}
}
);
export async function convertToCsv(waveform: Waveform) {
const { getLocale } =
require("eez-studio-shared/i10n") as typeof I10nModule;
const locale = getLocale();
// determine CSV separator depending of locale usage of ","
let separator;
if ((0.1).toLocaleString(locale).indexOf(",") != -1) {
separator = ";";
} else {
separator = ",";
}
const numberFormat = new Intl.NumberFormat(locale, {
useGrouping: false,
maximumFractionDigits: 19
});
let csv = "";
let progressToastId = notification.info("Exporting to CSV ...", {
autoClose: false,
closeButton: false,
closeOnClick: false,
hideProgressBar: false,
progressStyle: {
transition: "none"
}
});
await new Promise(resolve => setTimeout(resolve, 0));
csv += `Time,${
waveform.waveformDefinition.label ||
capitalize(waveform.waveformDefinition.unitName)
}\n`;
csv += `(ms),(${capitalize(
UNITS[waveform.waveformDefinition.unitName].unitSymbol
)})\n`;
csv += "\n";
for (let i = 0; i < waveform.length; i++) {
csv += numberFormat.format(i / waveform.samplingRate / 1000);
csv += separator;
const value = waveform.value(i);
csv += numberFormat.format(value);
csv += "\n";
if (i > 0 && i % 100000 === 0) {
const progress = i / waveform.length;
notification.update(progressToastId, {
progress
});
await new Promise(resolve => setTimeout(resolve, 0));
}
}
notification.dismiss(progressToastId);
return csv;
}