<commit_before>import { range } from 'd3-array';
import { scaleQuantize } from 'd3-scale';
import rbush from 'rbush';

// Services
import { pubSub } from './services';

// Factories
import {
  Annotation, AreaClusterer, KeySet, LabelCluster, LabelClusterGallery
} from './factories';

// Utils
import {
  identity,
  getClusterPropAcc,
  latToY,
  lngToX,
  positionLabels,
  scoreAtPercentile,
} from './utils';

class AnnotationsInsets {
  constructor(insetsTrack, options, getTrackByUid, animate) {
    this.getTrackByUid = getTrackByUid;
    this.animate = animate;
    this.options = options;

    this.boostContext = this.options.boostContext || 1;
    this.boostDetails = this.options.boostDetails || 1;
    this.boostLocality = this.options.boostLocality || 1;
    this.boostLayout = this.options.boostLayout || 1;
    this.bigAnnosBoost = this.options.bigAnnosBoost || 1;
    this.bigAnnosBoostArea = this.options.bigAnnosBoostArea || Infinity;

    this.insetsTrack = getTrackByUid(insetsTrack);

    if (!this.insetsTrack) {
      console.warn(`Insets track (uid: ${insetsTrack}) not found`, insetsTrack);
      return;
    }

    this.updateBoundsBound = this.updateBounds.bind(this);
    this.updateBoundsBound = this.updateBounds.bind(this);
    this.zoomHandlerBound = this.zoomHandler.bind(this);

    this.insetsTrack.subscribe('dimensions', this.updateBoundsBound);
    this.insetsTrack.subscribe('position', this.updateBoundsBound);
    this.insetsTrack.subscribe('zoom', this.zoomHandlerBound);

    this.annotationTrackIds = new Set();
    this.annotationTracks = options.annotationTracks
      .map((uid) => {
        const track = getTrackByUid(uid);

        if (!track) console.warn(`Child track (uid: ${uid}) not found`);
        else this.annotationTrackIds.add(track.uuid);

        return track;
      })
      .filter(track => track);

    this.annotationDrawnHandlerBound = this.annotationDrawnHandler.bind(this);

    // Augment annotation tracks
    this.annotationTracks.forEach((track) => {
      track.subscribe('annotationDrawn', this.annotationDrawnHandlerBound);
    });

    this.currK = 1;  // Current scale
    this.drawnAnnoIds = new Set();
    this.annosToBeDrawnAsInsets = new KeySet('id');
    this.insets = {};

    this.tracksDrawingTiles = new Set();

    this.initTree();

    this.pubSubs = [];
    this.pubSubs.push(pubSub.subscribe(
      'TiledPixiTrack.tilesDrawnEnd',
      this.tilesDrawnEndHandler.bind(this)
    ));

    // Yet another transformation (oh lord please let this be the last one...)
    // Some coordinate systems (so far only geography) displays the original
    // coordinates are projected locations. Hence, we end with view coordinates
    // (pixel location on the screen), data location (longitude and latitude),
    // and finally projected data locations. Currently we only support mercator
    // but who know... more might come. Anyway, we need this extra projection to
    // have zoom-independent data locations at the correct visual ratio.
    this.projectorX = this.insetsTrack.dataType === 'osm-image'
      ? lng => lngToX(lng, 19)
      : identity;

    this.projectorY = this.insetsTrack.dataType === 'osm-image'
      ? lat => latToY(lat, 19)
      : identity;

    this.insetsTrackWidth = 0;
    this.insetsTrackHeight = 0;

    const propChecks = [];

    this.clusterSizePropAcc = getClusterPropAcc(
      this.insetsTrack.options.scaleSizeBy
    );
    propChecks.push(['size', this.clusterSizePropAcc]);

    if (this.insetsTrack.options.scaleBorderBy) {
      this.clusterBorderPropAcc = getClusterPropAcc(
        this.insetsTrack.options.scaleBorderBy
      );
      propChecks.push(['border', this.clusterBorderPropAcc]);
    }

    this.areaClusterer = new AreaClusterer({
      gridSize: this.options.gridSize,
      maxClusterDiameter: this.options.maxClusterDiameter,
      minClusterSize: this.options.minClusterSize,
      maxClusterSize: +this.options.maxClusterSize,
      maxZoom: undefined,
      disabled: !!this.options.disableClustering,
      propCheck: propChecks,
      isBinning: true,
      clusterAmong: this.options.clusterAmong || false,
    });

    this.isRepresentatives = this.insetsTrack.dataType.indexOf('image') >= 0;

    this.minInsetSize = this.insetsTrack.insetMinSize * this.insetsTrack.insetScale;
    this.maxInsetSize = this.insetsTrack.insetMaxSize * this.insetsTrack.insetScale;
    this.midInsetSize = (this.minInsetSize + this.maxInsetSize) / 2;

    this.updateBounds();
  }

  /**
   * Handles annotation drawn events
   *
   * @param  {String}  event.uid  UID of the view that triggered the event.
   * @param  {Array}  event.viewPos  View position (i.e., [x, y, width, height])
   *   of the drawn annotation on the screen.
   * @param  {Array}  event.dataPos  Data position of the drawn annotation. For
   *   example base pairs (Hi-C), or pixels (gigapixel images), or lng-lat
   *   (geo json).
   */
  annotationDrawnHandler({ uid, viewPos, dataPos, importance, info }) {
    const dataPosProj = [
      this.projectorX(dataPos[0]),
      this.projectorX(dataPos[1]),
      this.projectorY(dataPos[2]),
      this.projectorY(dataPos[3])
    ];

    const _viewPos = [
      viewPos[0],
      viewPos[0] + viewPos[2],
      viewPos[1],
      viewPos[1] + viewPos[3],
    ];

    let annotation = this.areaClusterer.elements.get(uid);
    if (annotation) {
      annotation.setViewPosition(_viewPos);
    } else {
      annotation = new Annotation(
        uid,
        _viewPos,
        dataPos,
        dataPosProj,
        importance,
        info,
        this.options.clusterAmong || false
      );
    }

    this.drawnAnnoIds.add(uid);

    if (!this.drawnAnnoIdsPrev.has(uid)) {
      this.newAnno = true;
      this.drawnAnnoIdsNew.add(uid);
    }

    if (
      (
        viewPos[2] <= this.options.insetThreshold
        || viewPos[3] <= this.options.insetThreshold
      )
      &&
      (
        (annotation.minX >= 0 || annotation.maxX > 0)
        && (annotation.minX < this.insetsTrackWidth || annotation.maxX <= this.insetsTrackWidth)
        && (annotation.minY >= 0 || annotation.maxY > 0)
        && (annotation.minY < this.insetsTrackHeight || annotation.maxY <= this.insetsTrackHeight)
      )
    ) this.annosToBeDrawnAsInsets.add(annotation);

    this.drawnAnnotations.push(annotation);
  }

  /**
   * Build region tree of drawn annotations and trigger the creation of insets.<commit_msg>Support exclusion of tracks from insetting but regard as background annotations<commit_after>import { range } from 'd3-array';
import { scaleQuantize } from 'd3-scale';
import rbush from 'rbush';

// Services
import { pubSub } from './services';

// Factories
import {
  Annotation, AreaClusterer, KeySet, LabelCluster, LabelClusterGallery
} from './factories';

// Utils
import {
  identity,
  getClusterPropAcc,
  latToY,
  lngToX,
  positionLabels,
  scoreAtPercentile,
} from './utils';

class AnnotationsInsets {
  constructor(insetsTrack, options, getTrackByUid, animate) {
    this.getTrackByUid = getTrackByUid;
    this.animate = animate;
    this.options = options;

    this.boostContext = this.options.boostContext || 1;
    this.boostDetails = this.options.boostDetails || 1;
    this.boostLocality = this.options.boostLocality || 1;
    this.boostLayout = this.options.boostLayout || 1;
    this.bigAnnosBoost = this.options.bigAnnosBoost || 1;
    this.bigAnnosBoostArea = this.options.bigAnnosBoostArea || Infinity;

    this.insetsTrack = getTrackByUid(insetsTrack);

    if (!this.insetsTrack) {
      console.warn(`Insets track (uid: ${insetsTrack}) not found`, insetsTrack);
      return;
    }

    this.updateBoundsBound = this.updateBounds.bind(this);
    this.updateBoundsBound = this.updateBounds.bind(this);
    this.zoomHandlerBound = this.zoomHandler.bind(this);

    this.insetsTrack.subscribe('dimensions', this.updateBoundsBound);
    this.insetsTrack.subscribe('position', this.updateBoundsBound);
    this.insetsTrack.subscribe('zoom', this.zoomHandlerBound);

    this.excludeTracks = this.options.excludeTracks || [];

    this.annotationTrackIds = new Set();
    this.annotationTrackIdsExcluded = new Set();
    this.annotationTracks = options.annotationTracks
      .map((uid) => {
        const track = getTrackByUid(uid);

        if (!track) console.warn(`Child track (uid: ${uid}) not found`);
        else this.annotationTrackIds.add(track.uuid);

        if (this.excludeTracks.indexOf(uid) >= 0) {
          this.annotationTrackIdsExcluded.add(track.uuid);
        }

        return track;
      })
      .filter(track => track);

    this.annotationDrawnHandlerBound = this.annotationDrawnHandler.bind(this);

    // Augment annotation tracks
    this.annotationTracks.forEach((track) => {
      track.subscribe('annotationDrawn', this.annotationDrawnHandlerBound);
    });

    this.currK = 1;  // Current scale
    this.drawnAnnoIds = new Set();
    this.annosToBeDrawnAsInsets = new KeySet('id');
    this.insets = {};

    this.tracksDrawingTiles = new Set();

    this.initTree();

    this.pubSubs = [];
    this.pubSubs.push(pubSub.subscribe(
      'TiledPixiTrack.tilesDrawnEnd',
      this.tilesDrawnEndHandler.bind(this)
    ));

    // Yet another transformation (oh lord please let this be the last one...)
    // Some coordinate systems (so far only geography) displays the original
    // coordinates are projected locations. Hence, we end with view coordinates
    // (pixel location on the screen), data location (longitude and latitude),
    // and finally projected data locations. Currently we only support mercator
    // but who know... more might come. Anyway, we need this extra projection to
    // have zoom-independent data locations at the correct visual ratio.
    this.projectorX = this.insetsTrack.dataType === 'osm-image'
      ? lng => lngToX(lng, 19)
      : identity;

    this.projectorY = this.insetsTrack.dataType === 'osm-image'
      ? lat => latToY(lat, 19)
      : identity;

    this.insetsTrackWidth = 0;
    this.insetsTrackHeight = 0;

    const propChecks = [];

    this.clusterSizePropAcc = getClusterPropAcc(
      this.insetsTrack.options.scaleSizeBy
    );
    propChecks.push(['size', this.clusterSizePropAcc]);

    if (this.insetsTrack.options.scaleBorderBy) {
      this.clusterBorderPropAcc = getClusterPropAcc(
        this.insetsTrack.options.scaleBorderBy
      );
      propChecks.push(['border', this.clusterBorderPropAcc]);
    }

    this.areaClusterer = new AreaClusterer({
      gridSize: this.options.gridSize,
      maxClusterDiameter: this.options.maxClusterDiameter,
      minClusterSize: this.options.minClusterSize,
      maxClusterSize: +this.options.maxClusterSize,
      maxZoom: undefined,
      disabled: !!this.options.disableClustering,
      propCheck: propChecks,
      isBinning: true,
      clusterAmong: this.options.clusterAmong || false,
    });

    this.isRepresentatives = this.insetsTrack.dataType.indexOf('image') >= 0;

    this.minInsetSize = this.insetsTrack.insetMinSize * this.insetsTrack.insetScale;
    this.maxInsetSize = this.insetsTrack.insetMaxSize * this.insetsTrack.insetScale;
    this.midInsetSize = (this.minInsetSize + this.maxInsetSize) / 2;

    this.updateBounds();
  }

  /**
   * Handles annotation drawn events
   *
   * @param  {String}  event.uid  UID of the view that triggered the event.
   * @param  {Array}  event.viewPos  View position (i.e., [x, y, width, height])
   *   of the drawn annotation on the screen.
   * @param  {Array}  event.dataPos  Data position of the drawn annotation. For
   *   example base pairs (Hi-C), or pixels (gigapixel images), or lng-lat
   *   (geo json).
   */
  annotationDrawnHandler({ trackUuid, annotationUuid, viewPos, dataPos, importance, info }) {
    const dataPosProj = [
      this.projectorX(dataPos[0]),
      this.projectorX(dataPos[1]),
      this.projectorY(dataPos[2]),
      this.projectorY(dataPos[3])
    ];

    const _viewPos = [
      viewPos[0],
      viewPos[0] + viewPos[2],
      viewPos[1],
      viewPos[1] + viewPos[3],
    ];

    let annotation = this.areaClusterer.elements.get(annotationUuid);
    if (annotation) {
      annotation.setViewPosition(_viewPos);
    } else {
      annotation = new Annotation(
        annotationUuid,
        _viewPos,
        dataPos,
        dataPosProj,
        importance,
        info,
        this.options.clusterAmong || false
      );
    }

    this.drawnAnnoIds.add(annotationUuid);

    if (!this.drawnAnnoIdsPrev.has(annotationUuid)) {
      this.newAnno = true;
      this.drawnAnnoIdsNew.add(annotationUuid);
    }

    if (
      !this.annotationTrackIdsExcluded.has(trackUuid) &&
      (
        viewPos[2] <= this.options.insetThreshold
        || viewPos[3] <= this.options.insetThreshold
      )
      &&
      (
        (annotation.minX >= 0 || annotation.maxX > 0)
        && (annotation.minX < this.insetsTrackWidth || annotation.maxX <= this.insetsTrackWidth)
        && (annotation.minY >= 0 || annotation.maxY > 0)
        && (annotation.minY < this.insetsTrackHeight || annotation.maxY <= this.insetsTrackHeight)
      )
    ) this.annosToBeDrawnAsInsets.add(annotation);

    this.drawnAnnotations.push(annotation);
  }

  /**
   * Build region tree of drawn annotations and trigger the creation of insets.
{'fraction_non_alphanumeric': 0.1180082366154998, 'fraction_numerical': 0.004867090977162112, 'mean_word_length': 3.0981896287204664, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 24, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7127397', 'n_tokens_mistral': 4248, 'n_tokens_neox': 4091, 'n_words': 850}