Загрузка данных


import { getThemeStore } from '@src/theme';
import { IndicatorConfig, IndicatorSerie } from '@src/types';

type SeriesOptionsWithColor = {
  color?: string;
  visible?: boolean;
};

export function createIndicatorConfigWithUniqueColors(
  config: IndicatorConfig,
  usedColors: readonly string[],
): IndicatorConfig {
  const reservedColors = new Set(usedColors.map(normalizeColor));

  return {
    ...config,
    settings: config.settings ? [...config.settings] : undefined,
    seriesLabels: config.seriesLabels ? { ...config.seriesLabels } : undefined,
    series: config.series.map((serie) => {
      const clonedSerie = cloneIndicatorSerie(serie);

      if (!isVisibleLineSerie(serie)) {
        return clonedSerie;
      }

      const currentColor = getSerieColor(serie);
      const nextColor =
        currentColor && !reservedColors.has(normalizeColor(currentColor))
          ? currentColor
          : getNextIndicatorColor(Array.from(reservedColors));

      reservedColors.add(normalizeColor(nextColor));

      return {
        ...clonedSerie,
        seriesOptions: {
          ...clonedSerie.seriesOptions,
          color: nextColor,
        },
      };
    }),
  };
}

export function getIndicatorConfigColors(config: IndicatorConfig): string[] {
  return config.series.reduce<string[]>((colors, serie) => {
    if (!isVisibleLineSerie(serie)) {
      return colors;
    }

    const color = getSerieColor(serie);

    if (color) {
      colors.push(color);
    }

    return colors;
  }, []);
}

function getNextIndicatorColor(usedColors: readonly string[]): string {
  const normalizedUsedColors = new Set(usedColors.map(normalizeColor));

  const freeColor = getInitialIndicatorColors().find((color) => !normalizedUsedColors.has(normalizeColor(color)));

  if (freeColor) {
    return freeColor;
  }

  let index = normalizedUsedColors.size;
  let generatedColor = generateColorByIndex(index);

  while (normalizedUsedColors.has(normalizeColor(generatedColor))) {
    index += 1;
    generatedColor = generateColorByIndex(index);
  }

  return generatedColor;
}

function getInitialIndicatorColors(): string[] {
  const { colors } = getThemeStore();

  return getUniqueColors([
    colors.indicatorLineSma,
    colors.indicatorLineEma,
    colors.chartCandleWickUp,
    '#2962FF',
    '#FF6D00',
    '#00C853',
    '#D500F9',
    '#FF1744',
    '#00B8D4',
    '#FFD600',
    '#7C4DFF',
    '#64DD17',
    '#FF4081',
  ]);
}

function getUniqueColors(colors: readonly string[]): string[] {
  const uniqueColors = new Set<string>();

  return colors.filter((color) => {
    const normalizedColor = normalizeColor(color);

    if (uniqueColors.has(normalizedColor)) {
      return false;
    }

    uniqueColors.add(normalizedColor);

    return true;
  });
}

function generateColorByIndex(index: number): string {
  const goldenAngle = 137.508;
  const hue = Math.round((index * goldenAngle) % 360);

  return `hsl(${hue}deg 72% 56%)`;
}

function isVisibleLineSerie(serie: IndicatorSerie): boolean {
  const seriesOptions = serie.seriesOptions as SeriesOptionsWithColor | undefined;

  return serie.name === 'Line' && seriesOptions?.visible !== false;
}

function getSerieColor(serie: IndicatorSerie): string | null {
  const seriesOptions = serie.seriesOptions as SeriesOptionsWithColor | undefined;
  const color = seriesOptions?.color;

  return typeof color === 'string' && color.trim() ? color : null;
}

function cloneIndicatorSerie(serie: IndicatorSerie): IndicatorSerie {
  return {
    ...serie,
    seriesOptions: serie.seriesOptions ? { ...serie.seriesOptions } : undefined,
    priceScaleOptions: serie.priceScaleOptions ? { ...serie.priceScaleOptions } : undefined,
  };
}

function normalizeColor(color: string): string {
  return color.trim().toLowerCase();
}






import { IChartApi } from 'lightweight-charts';
import { BehaviorSubject, map, Observable } from 'rxjs';

import { DataSource } from '@core/DataSource';
import { DOMModel } from '@core/DOMModel';
import { EventManager } from '@core/EventManager';
import { Indicator } from '@core/Indicator';
import { PaneManager } from '@core/PaneManager';
import { DOMObject } from '@src/core/DOMObject';
import { indicatorsMap as indicatorsConfigMap } from '@src/core/Indicators';
import {
  createIndicatorConfigWithUniqueColors,
  getIndicatorConfigColors,
} from '@src/core/Indicators/colors';
import { ChartTypeOptions, IndicatorConfig } from '@src/types';
import { IndicatorSnapshot } from '@src/types/snapshot';

interface SeriesParams {
  eventManager: EventManager;
  dataSource: DataSource;
  lwcChart: IChartApi;
  paneManager: PaneManager;
  DOM: DOMModel;
  initialIndicators?: IndicatorSnapshot[];
  chartOptions?: ChartTypeOptions;
}

export class IndicatorManager {
  private eventManager: EventManager;
  private lwcChart: IChartApi;
  private chartOptions?: ChartTypeOptions;

  private entities$: BehaviorSubject<Indicator[]> = new BehaviorSubject<Indicator[]>([]);
  private indicatorsMap$: BehaviorSubject<Map<string, Indicator>> = new BehaviorSubject(new Map());
  private DOM: DOMModel;
  private dataSource: DataSource;
  private paneManager: PaneManager;

  constructor({ eventManager, dataSource, lwcChart, DOM, chartOptions, initialIndicators, paneManager }: SeriesParams) {
    this.eventManager = eventManager;
    this.lwcChart = lwcChart;
    this.chartOptions = chartOptions;
    this.DOM = DOM;
    this.dataSource = dataSource;
    this.paneManager = paneManager;

    this.indicatorsMap$ = new BehaviorSubject<Map<string, Indicator>>(new Map());

    initialIndicators?.forEach((ind) => {
      this.addIndicator(ind);
    });
  }

  public addEntity<T extends Indicator>(
    factory: (zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) => T,
  ): T {
    return this.DOM.setEntity(factory);
  }

  public addIndicator(snap: Partial<IndicatorSnapshot>): void {
    if (!snap.indicatorType) {
      console.error('[IndicatorManager] Не был получен тип индиктора');
      return;
    }

    const indicatorsMap = new Map(this.indicatorsMap$.value);

    const id = snap.id ?? `${snap.indicatorType}-${crypto.randomUUID()}`;
    const defaultConfig = indicatorsConfigMap[snap.indicatorType];

    if (!snap.config && !defaultConfig) {
      console.error(`[IndicatorManager] Не был получен конфиг индикатора ${snap.indicatorType}`);
      return;
    }

    const baseConfig = snap.config ?? (defaultConfig as IndicatorConfig);
    const config = snap.config
      ? baseConfig
      : createIndicatorConfigWithUniqueColors(baseConfig, this.getUsedIndicatorColors());

    const associatedPane =
      snap.paneId !== undefined
        ? (this.paneManager.getPaneById(snap.paneId) ?? this.paneManager.addPane())
        : config.newPane
          ? this.paneManager.addPane()
          : this.paneManager.getMainPane();

    const indicatorToSet = this.addEntity<Indicator>(
      (zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) =>
        new Indicator({
          id,
          paneId: associatedPane.getId(),
          zIndex,
          onDelete: this.deleteIndicator,
          moveUp,
          moveDown,

          mainSymbol$: this.eventManager.getSymbol(),
          lwcChart: this.lwcChart,
          dataSource: this.dataSource,
          associatedPane,
          config,
          type: snap.indicatorType,
          chartOptions: this.chartOptions,
        }),
    );

    indicatorsMap.set(id, indicatorToSet);

    this.indicatorsMap$.next(indicatorsMap);
    this.entities$.next(Array.from(indicatorsMap.values()));
  }

  public getIndicators() {
    return this.indicatorsMap$.pipe(map((indicators) => Array.from(indicators.keys())));
  }

  public removeEntity(entity: DOMObject): void {
    this.DOM.removeEntity(entity);
  }

  public deleteIndicator = (id: string) => {
    const indicatorsMap = new Map(this.indicatorsMap$.value);
    const entity = indicatorsMap.get(id);

    if (!entity) {
      return;
    }

    this.removeEntity(entity);
    entity.destroy();
    indicatorsMap.delete(id);

    this.indicatorsMap$.next(indicatorsMap);
    this.entities$.next(Array.from(indicatorsMap.values()));
  };

  public entities(): Observable<Indicator[]> {
    return this.entities$.asObservable();
  }

  private getUsedIndicatorColors(): string[] {
    const usedColors: string[] = [];

    this.indicatorsMap$.value.forEach((indicator) => {
      usedColors.push(...getIndicatorConfigColors(indicator.getConfig()));
    });

    return usedColors;
  }
}