Загрузка данных
import { IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { EventManager } from '@core';
import { DOMModel } from '@core/DOMModel';
import { Drawing } from '@core/Drawings';
import { ISeriesDrawing } from '@core/Drawings/common';
import { Ray } from '@core/Drawings/ray';
import { Text } from '@core/Drawings/text/text';
import { TrendLine } from '@core/Drawings/trendLine';
import { VolumeProfile } from '@core/Drawings/volumeProfile';
import { drawingLabelById, DrawingsNames } from '@src/constants';
import { AxisLine } from '@src/core/Drawings/axisLine';
import { Diapson } from '@src/core/Drawings/diapson';
import { Rectangle } from '@src/core/Drawings/rectangle';
import { Ruler } from '@src/core/Drawings/ruler';
import { SliderPosition } from '@src/core/Drawings/sliderPosition';
import { Traectory } from '@src/core/Drawings/traectory';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { ActiveDrawingTool } from '@src/types';
interface DrawingsManagerParams {
eventManager: EventManager;
mainSeries$: Observable<SeriesStrategies | null>;
lwcChart: IChartApi;
DOM: DOMModel;
container: HTMLElement;
openDrawingSettings: (drawing: Drawing) => void;
}
export interface DrawingParams {
chart: IChartApi;
series: ISeriesApi<SeriesType>;
eventManager: EventManager;
container: HTMLElement;
removeSelf: () => void;
openSettings: () => void;
}
export interface DrawingConfig {
singleInstance?: boolean;
construct: (params: DrawingParams) => ISeriesDrawing;
}
export interface SerializedDrawing {
id: string;
drawingName: DrawingsNames;
state: unknown;
}
export type DrawingsManagerState = SerializedDrawing[];
export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
[DrawingsNames.trendLine]: {
construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
return new TrendLine(chart, series, {
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
});
},
},
[DrawingsNames.ray]: {
construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
return new Ray(chart, series, {
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
});
},
},
[DrawingsNames.horizontalLine]: {
construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new AxisLine(chart, series, {
direction: 'horizontal',
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
});
},
},
[DrawingsNames.horizontalRay]: {
construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
return new TrendLine(chart, series, {
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
});
},
},
[DrawingsNames.verticalLine]: {
construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new AxisLine(chart, series, {
direction: 'vertical',
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
});
},
},
[DrawingsNames.sliderLong]: {
construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new SliderPosition(chart, series, {
side: 'long',
container,
formatObservable: eventManager.getChartOptionsModel(),
resetTriggers: [eventManager.getTimeframeObs(), eventManager.getInterval()],
removeSelf,
openSettings,
});
},
},
[DrawingsNames.sliderShort]: {
construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new SliderPosition(chart, series, {
side: 'short',
container,
formatObservable: eventManager.getChartOptionsModel(),
resetTriggers: [eventManager.getTimeframeObs(), eventManager.getInterval()],
removeSelf,
openSettings,
});
},
},
[DrawingsNames.diapsonDates]: {
construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
return new Diapson(chart, series, {
rangeMode: 'date',
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
});
},
},
[DrawingsNames.diapsonPrices]: {
construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
return new Diapson(chart, series, {
rangeMode: 'price',
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
});
},
},
[DrawingsNames.fixedProfile]: {
construct: ({ chart, series, container, eventManager, removeSelf, openSettings }) => {
return new VolumeProfile(chart, series, {
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
});
},
},
[DrawingsNames.rectangle]: {
construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new Rectangle(chart, series, {
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
openSettings,
});
},
},
[DrawingsNames.ruler]: {
singleInstance: true,
construct: ({ chart, series, eventManager, removeSelf }) => {
return new Ruler(chart, series, {
formatObservable: eventManager.getChartOptionsModel(),
resetTriggers: [eventManager.getTimeframeObs(), eventManager.getInterval()],
removeSelf,
});
},
},
[DrawingsNames.traectory]: {
construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new Traectory(chart, series, {
formatObservable: eventManager.getChartOptionsModel(),
container,
removeSelf,
openSettings,
});
},
},
[DrawingsNames.text]: {
construct: ({ chart, series, eventManager, container, removeSelf, openSettings }) => {
return new Text(chart, series, {
formatObservable: eventManager.getChartOptionsModel(),
container,
removeSelf,
openSettings,
});
},
},
};
export class DrawingsManager {
private eventManager: EventManager;
private lwcChart: IChartApi;
private drawingsQty = 0;
private DOM: DOMModel;
private container: HTMLElement;
private openDrawingSettings: (drawing: Drawing) => void;
private mainSeries: SeriesStrategies | null = null;
private subscriptions = new Subscription();
private drawings$ = new BehaviorSubject<Drawing[]>([]);
private activeTool$ = new BehaviorSubject<ActiveDrawingTool>('crosshair');
private endlessMode$ = new BehaviorSubject(false);
private recreateScheduled = false;
private pendingState: DrawingsManagerState | null = null;
constructor({ eventManager, mainSeries$, lwcChart, DOM, container, openDrawingSettings }: DrawingsManagerParams) {
this.DOM = DOM;
this.eventManager = eventManager;
this.lwcChart = lwcChart;
this.container = container;
this.openDrawingSettings = openDrawingSettings;
this.subscriptions.add(
mainSeries$.subscribe((series) => {
if (!series) {
return;
}
this.mainSeries = series;
this.drawings$.value.forEach((drawing) => drawing.rebind(series));
if (this.pendingState) {
const state = this.pendingState;
this.pendingState = null;
this.setState(state);
}
}),
);
window.addEventListener('pointerup', this.handlePointerUp);
this.container.addEventListener('click', this.handleClick);
this.container.addEventListener('pointerdown', this.handlePointerDown);
}
private handlePointerDown = (): void => {
this.DOM.refreshEntities();
};
private handlePointerUp = (): void => {
this.DOM.refreshEntities();
this.updateActiveTool();
};
private handleClick = (): void => {
this.DOM.refreshEntities();
this.updateActiveTool();
};
private updateActiveTool = (): void => {
const hasPendingDrawing = this.drawings$.value.some((drawing) => drawing.isCreationPending());
if (hasPendingDrawing) {
return;
}
const activeTool = this.activeTool$.value;
const isSingleInstanceTool = activeTool !== 'crosshair' && drawingsMap[activeTool]?.singleInstance;
if (activeTool !== 'crosshair' && this.endlessMode$.value && !isSingleInstanceTool) {
if (this.recreateScheduled) {
return;
}
this.recreateScheduled = true;
queueMicrotask(() => {
this.recreateScheduled = false;
const currentTool = this.activeTool$.value;
const hasPendingAfterTick = this.drawings$.value.some((drawing) => drawing.isCreationPending());
if (currentTool === 'crosshair') {
return;
}
if (!this.endlessMode$.value) {
return;
}
if (drawingsMap[currentTool]?.singleInstance) {
return;
}
if (hasPendingAfterTick) {
return;
}
this.createDrawing(currentTool);
});
return;
}
this.activeTool$.next('crosshair');
};
private removeDrawing = (id: string): void => {
const drawing = this.drawings$.value.find((item) => item.id === id);
if (!drawing) {
return;
}
this.removeDrawings([drawing]);
};
private removeDrawingsByName(name: DrawingsNames, shouldUpdateTool = true): void {
const drawingsToRemove = this.drawings$.value.filter((drawing) => drawing.getDrawingName() === name);
this.removeDrawings(drawingsToRemove, shouldUpdateTool);
}
private removePendingDrawings(shouldUpdateTool = true): void {
const drawingsToRemove = this.drawings$.value.filter((drawing) => drawing.isCreationPending());
this.removeDrawings(drawingsToRemove, shouldUpdateTool);
}
private removeDrawings(drawingsToRemove: Drawing[], shouldUpdateTool = true): void {
if (!drawingsToRemove.length) {
return;
}
drawingsToRemove.forEach((drawing) => {
drawing.destroy();
this.DOM.removeEntity(drawing);
});
this.drawings$.next(this.drawings$.value.filter((drawing) => !drawingsToRemove.includes(drawing)));
if (shouldUpdateTool) {
this.updateActiveTool();
}
this.DOM.refreshEntities();
}
public addDrawingForce = (name: DrawingsNames): void => {
this.removePendingDrawings(false);
if (drawingsMap[name].singleInstance) {
this.removeDrawingsByName(name, false);
}
this.activeTool$.next(name);
this.createDrawing(name);
this.DOM.refreshEntities();
};
private createDrawing(name: DrawingsNames, id?: string, state?: unknown, emit = true): Drawing {
if (!this.mainSeries) {
throw new Error('[Drawings] main series is not defined');
}
const config = drawingsMap[name];
const drawingId = id ?? `${name}${this.drawingsQty}`;
let createdDrawing: Drawing | null = null;
const construct = (chart: IChartApi, series: ISeriesApi<SeriesType>) =>
config.construct({
chart,
series,
eventManager: this.eventManager,
container: this.container,
removeSelf: () => this.removeDrawing(drawingId),
openSettings: () => {
if (createdDrawing) {
this.openDrawingSettings(createdDrawing);
}
},
});
const drawingFactory = (zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) =>
new Drawing({
lwcChart: this.lwcChart,
mainSeries: this.mainSeries as SeriesStrategies,
id: drawingId,
drawingName: name,
name: drawingLabelById[name],
onDelete: this.removeDrawing,
zIndex,
moveDown,
moveUp,
construct,
});
const entity = this.DOM.setEntity<Drawing>(drawingFactory);
createdDrawing = entity;
if (state !== undefined) {
entity.setState(state);
}
if (!id) {
this.drawingsQty++;
}
if (emit) {
this.drawings$.next([...this.drawings$.value, entity]);
}
return entity;
}
public getState(): DrawingsManagerState {
return this.drawings$.value
.filter((drawing) => !drawing.isCreationPending())
.map((drawing) => ({
id: drawing.id,
drawingName: drawing.getDrawingName(),
state: drawing.getState(),
}));
}
public setState(state: DrawingsManagerState): void {
if (!Array.isArray(state)) {
return;
}
if (!this.mainSeries) {
this.pendingState = state;
return;
}
this.removeDrawings(this.drawings$.value, false);
const restoredDrawings = state.reduce<Drawing[]>((drawings, item) => {
if (!drawingsMap[item.drawingName]) {
return drawings;
}
drawings.push(this.createDrawing(item.drawingName, item.id, item.state, false));
return drawings;
}, []);
this.drawingsQty = getNextDrawingsQty(state);
this.drawings$.next(restoredDrawings);
this.activeTool$.next('crosshair');
this.DOM.refreshEntities();
}
public setEndlessDrawingMode = (value: boolean): void => {
this.endlessMode$.next(value);
};
public isEndlessDrawingsMode(): Observable<boolean> {
return this.endlessMode$.asObservable();
}
public getActiveTool(): Observable<ActiveDrawingTool> {
return this.activeTool$.asObservable();
}
public activateCrosshair(): void {
this.removePendingDrawings(false);
this.activeTool$.next('crosshair');
this.DOM.refreshEntities();
}
public entities(): Observable<Drawing[]> {
return this.drawings$.asObservable();
}
public openSettings(drawing: Drawing): void {
this.openDrawingSettings(drawing);
}
public getDrawings(): Drawing[] {
return this.drawings$.value;
}
public hideAll(): void {
this.drawings$.value.forEach((drawing) => drawing.hide());
this.DOM.refreshEntities();
}
public destroy(): void {
window.removeEventListener('pointerup', this.handlePointerUp);
this.container.removeEventListener('click', this.handleClick);
this.container.removeEventListener('pointerdown', this.handlePointerDown);
this.drawings$.value.forEach((drawing) => drawing.destroy());
this.subscriptions.unsubscribe();
this.drawings$.complete();
this.activeTool$.complete();
this.endlessMode$.complete();
}
}
function getNextDrawingsQty(drawings: DrawingsManagerState): number {
let maxIndex = -1;
drawings.forEach((drawing) => {
if (!drawing.id.startsWith(drawing.drawingName)) {
return;
}
const index = Number(drawing.id.slice(drawing.drawingName.length));
if (Number.isInteger(index)) {
maxIndex = Math.max(maxIndex, index);
}
});
return maxIndex + 1;
}