<commit_before>import Utils from "../../../../services/utils.service";
import IWindowService = angular.IWindowService;
import WorkflowGraphService from "./workflowgraph.service";
import IScope = angular.IScope;
import Job from "../../../../model/session/job";
import Dataset from "../../../../model/session/dataset";
import Module from "../../../../model/session/module";
import Node from "./node"
import {IChipsterFilter} from "../../../../common/filter/chipsterfilter";
import {ChangeDetector, Comparison} from "../../../../services/changedetector.service";
import {MapChangeDetector} from "../../../../services/changedetector.service";
import {ArrayChangeDetector} from "../../../../services/changedetector.service";
import SessionDataService from "../sessiondata.service";
import SelectionService from "../selection.service";
import * as d3 from "d3";

interface DatasetNode extends Node {
	dataset: Dataset;
	datasetId?: string;
	name: string;
	extension: string;
}

interface JobNode extends Node {
	job: Job;
	fgColor: string;
	spin: boolean;
}

interface Link {
	source: DatasetNode;
	target: Node;
}

class WorkflowGraphController {

	static $inject = ['$scope', '$window', '$log', '$filter', 'SessionDataService', 'SelectionService'];

	constructor(
		private $scope: IScope,
		private $window: IWindowService,
		private $log: ng.ILogService,
		private $filter: IChipsterFilter,
		private SessionDataService: SessionDataService,
		private SelectionService: SelectionService) {

		this.callback = {
			clearSelection: () => this.SelectionService.clearSelection(),
			toggleDatasetSelection: ($event: any, data: Dataset) => this.SelectionService.toggleDatasetSelection($event, data),
			selectJob: ($event: any, job: Job) => this.SelectionService.selectJob($event, job),
			showDefaultVisualization: () => this.showDefaultVisualization(),
			updateDataset: (dataset: Dataset) => this.SessionDataService.updateDataset(dataset)
		};
	}

	d3: any = this.$window['d3'];
	//var shiftKey, ctrlKey;
	svg: d3.Selection<any>;
	outerSvg: d3.Selection<any>;

	d3DatasetNodesGroup: d3.Selection<DatasetNode>;
	d3JobNodesGroup: d3.Selection<JobNode>;
	d3LinksGroup: d3.Selection<Link>;
	d3LinksDefsGroup: d3.Selection<any>;
	d3LabelsGroup: d3.Selection<DatasetNode>;
	background: d3.Selection<any>;

	d3Links: d3.selection.Update<Link>;
	d3Labels: d3.selection.Update<DatasetNode>;
	d3DatasetNodes: d3.selection.Update<DatasetNode>;
	d3JobNodes: d3.selection.Update<JobNode>;

	menu:any;
	nodeWidth:number = WorkflowGraphService.nodeWidth;
	nodeHeight:number = WorkflowGraphService.nodeHeight;
	fontSize = 14;
	nodeRadius = 4;
	width:number;
	height:number;
	zoom: number; // default zoom level
	lastScale: number; // last zoom level
	zoomer: d3.behavior.Zoom<any>;

	datasetNodes: Array<DatasetNode>;
	jobNodes: Array<JobNode>;
	links: Array<Link>;
	filter: Map<string, Dataset>;

	datasetsMap: Map<string, Dataset>;
	jobsMap: Map<string, Job>;
	modulesMap: Map<string, Module>;
	selectedDatasets: Array<Dataset>;
	selectedJobs: Array<Job>;
	datasetSearch: string;
	callback: any;
	enabled: boolean;
	dragStarted: boolean;

	changeDetectors: Array<ChangeDetector> = [];

	$onInit() {

		var element = WorkflowGraphController.getElement();

		this.zoomer = this.d3.behavior.zoom()
			.scaleExtent([ 0.2, 1 ])
			.scale(this.zoom)
			.on('zoom', this.zoomAndPan.bind(this));

		this.outerSvg = this.d3.select(element).append('svg').call(this.zoomer);

		// draw background on outerSvg, so that it won't pan or zoom
		this.renderBackground(this.outerSvg);

		this.svg = this.outerSvg.append('g');

		this.updateSvgSize();

		// order of these appends will determine the drawing order

		this.d3DatasetNodesGroup = this.svg.append('g').attr('class', 'dataset node');
		this.d3JobNodesGroup = this.svg.append('g').attr('class', 'job node');
		this.d3LinksGroup = this.svg.append('g').attr('class', 'link');
		this.d3LinksDefsGroup = this.d3LinksGroup.append('defs');
		this.d3LabelsGroup = this.svg.append('g').attr('class', 'label');

		this.createShadowFilter();

		// initialize the comparison of input collections

		// shallow comparison is enough for noticing when the array is changed
		this.changeDetectors.push(new ArrayChangeDetector(() => this.selectedDatasets, () => {
			this.renderGraph();
		}, Comparison.Shallow));
		this.changeDetectors.push(new ArrayChangeDetector(() => this.selectedJobs, () => {
			this.renderGraph()
		}, Comparison.Shallow));

		// deep comparison is needed to notice the changes in the objects (e.g. rename)
		this.changeDetectors.push(new MapChangeDetector(() => this.datasetsMap, () => {
			this.update()
		}, Comparison.Deep));
		this.changeDetectors.push(new MapChangeDetector(() => this.jobsMap, () => {
			this.update()
		}, Comparison.Deep));
		this.changeDetectors.push(new MapChangeDetector(() => this.modulesMap, () => {
			this.update()
		}, Comparison.Deep));
	}

	$onChanges(changes: ng.IChangesObject) {

		if (!this.svg) {
			// not yet initialized
			return;
		}

		if ("datasetSearch" in changes) {

			if (this.datasetSearch) {
				let filteredDatasets = this.$filter('searchDatasetFilter')(Utils.mapValues(this.datasetsMap), this.datasetSearch);
				this.filter = Utils.arrayToMap(filteredDatasets, 'datasetId');
			} else {
				this.filter = null;
			}
			this.renderGraph();
		}
	}

	$doCheck() {
		this.changeDetectors.forEach((cd: ChangeDetector) => cd.check());
		// it seems that there is no easy way to listen for div's size changes
		// running this on every digest cycle might be close enough
		this.updateSvgSize();
	}

	updateSvgSize() {

		let element = WorkflowGraphController.getElement();

		// leave some pixels for margins, otherwise the element will grow
		this.width = Math.max(200, element.offsetWidth);
		this.height = Math.max(200, element.offsetHeight - 5);

		this.outerSvg
			.attr('width', this.width)
			.attr('height', this.height);

		this.background
			.attr('width', this.width)
			.attr('height', this.height);<commit_msg>More accurate change detection and better types<commit_after>import Utils from "../../../../services/utils.service";
import IWindowService = angular.IWindowService;
import WorkflowGraphService from "./workflowgraph.service";
import IScope = angular.IScope;
import Job from "../../../../model/session/job";
import Dataset from "../../../../model/session/dataset";
import Module from "../../../../model/session/module";
import Node from "./node"
import {IChipsterFilter} from "../../../../common/filter/chipsterfilter";
import {ChangeDetector, Comparison} from "../../../../services/changedetector.service";
import {MapChangeDetector} from "../../../../services/changedetector.service";
import {ArrayChangeDetector} from "../../../../services/changedetector.service";
import SessionDataService from "../sessiondata.service";
import SelectionService from "../selection.service";
import * as d3 from "d3";

interface DatasetNode extends Node {
	dataset: Dataset;
	datasetId?: string;
	name: string;
	extension: string;
}

interface JobNode extends Node {
	job: Job;
	fgColor: string;
	spin: boolean;
}

interface Link {
	source: DatasetNode;
	target: Node;
}

class WorkflowGraphController {

	static $inject = ['$scope', '$window', '$log', '$filter', 'SessionDataService', 'SelectionService'];

	constructor(
		private $scope: IScope,
		private $window: IWindowService,
		private $log: ng.ILogService,
		private $filter: IChipsterFilter,
		private SessionDataService: SessionDataService,
		private SelectionService: SelectionService) {

		this.callback = {
			clearSelection: () => this.SelectionService.clearSelection(),
			toggleDatasetSelection: ($event: any, data: Dataset) => this.SelectionService.toggleDatasetSelection($event, data),
			selectJob: ($event: any, job: Job) => this.SelectionService.selectJob($event, job),
			showDefaultVisualization: () => this.showDefaultVisualization(),
			updateDataset: (dataset: Dataset) => this.SessionDataService.updateDataset(dataset)
		};
	}

	d3: any = this.$window['d3'];
	//var shiftKey, ctrlKey;
	svg: d3.Selection<any>;
	outerSvg: d3.Selection<any>;

	d3DatasetNodesGroup: d3.Selection<DatasetNode>;
	d3JobNodesGroup: d3.Selection<JobNode>;
	d3LinksGroup: d3.Selection<Link>;
	d3LinksDefsGroup: d3.Selection<any>;
	d3LabelsGroup: d3.Selection<DatasetNode>;
	background: d3.Selection<any>;

	d3Links: d3.selection.Update<Link>;
	d3Labels: d3.selection.Update<DatasetNode>;
	d3DatasetNodes: d3.selection.Update<DatasetNode>;
	d3JobNodes: d3.selection.Update<JobNode>;

	menu:any;
	nodeWidth:number = WorkflowGraphService.nodeWidth;
	nodeHeight:number = WorkflowGraphService.nodeHeight;
	fontSize = 14;
	nodeRadius = 4;
	width:number;
	height:number;
	zoom: number; // default zoom level
	lastScale: number; // last zoom level
	zoomer: d3.behavior.Zoom<any>;

	datasetNodes: Array<DatasetNode>;
	jobNodes: Array<JobNode>;
	links: Array<Link>;
	filter: Map<string, Dataset>;

	datasetsMap: Map<string, Dataset>;
	jobsMap: Map<string, Job>;
	modulesMap: Map<string, Module>;
	selectedDatasets: Array<Dataset>;
	selectedJobs: Array<Job>;
	datasetSearch: string;
	callback: any;
	enabled: boolean;
	dragStarted: boolean;

	changeDetectors: Array<ChangeDetector> = [];

	$onInit() {

		var element = WorkflowGraphController.getElement();

		this.zoomer = this.d3.behavior.zoom()
			.scaleExtent([ 0.2, 1 ])
			.scale(this.zoom)
			.on('zoom', this.zoomAndPan.bind(this));

		this.outerSvg = this.d3.select(element).append('svg').call(this.zoomer);

		// draw background on outerSvg, so that it won't pan or zoom
		this.renderBackground(this.outerSvg);

		this.svg = this.outerSvg.append('g');

		this.updateSvgSize();

		// order of these appends will determine the drawing order

		this.d3DatasetNodesGroup = this.svg.append('g').attr('class', 'dataset node');
		this.d3JobNodesGroup = this.svg.append('g').attr('class', 'job node');
		this.d3LinksGroup = this.svg.append('g').attr('class', 'link');
		this.d3LinksDefsGroup = this.d3LinksGroup.append('defs');
		this.d3LabelsGroup = this.svg.append('g').attr('class', 'label');

		this.createShadowFilter();

		// initialize the comparison of input collections

		// shallow comparison is enough for noticing when the array is changed
		this.changeDetectors.push(new ArrayChangeDetector(() => this.selectedDatasets, () => {
			this.renderGraph();
		}, Comparison.Shallow));
		this.changeDetectors.push(new ArrayChangeDetector(() => this.selectedJobs, () => {
			this.renderGraph()
		}, Comparison.Shallow));

		// deep comparison is needed to notice the changes in the objects (e.g. rename)
		this.changeDetectors.push(new MapChangeDetector(() => this.datasetsMap, () => {
			this.update()
		}, Comparison.Deep));
		this.changeDetectors.push(new MapChangeDetector(() => this.jobsMap, () => {
			this.update()
		}, Comparison.Deep));
		this.changeDetectors.push(new MapChangeDetector(() => this.modulesMap, () => {
			this.update()
		}, Comparison.Deep));
	}

	$onChanges(changes: ng.IChangesObject) {

		if (!this.svg) {
			// not yet initialized
			return;
		}

		if ("datasetSearch" in changes) {

			if (this.datasetSearch) {
				let filteredDatasets = this.$filter('searchDatasetFilter')(Utils.mapValues(this.datasetsMap), this.datasetSearch);
				this.filter = Utils.arrayToMap(filteredDatasets, 'datasetId');
			} else {
				this.filter = null;
			}
			this.renderGraph();
		}
	}

	$doCheck() {

		if (this.svg) {
			this.changeDetectors.forEach((cd: ChangeDetector) => cd.check());
			// it seems that there is no easy way to listen for div's size changes
			// running this on every digest cycle might be close enough
			this.updateSvgSize();
		}
	}

	updateSvgSize() {

		let element = WorkflowGraphController.getElement();

		// leave some pixels for margins, otherwise the element will grow
		this.width = Math.max(200, element.offsetWidth);
		this.height = Math.max(200, element.offsetHeight - 5);

		this.outerSvg
			.attr('width', this.width)
			.attr('height', this.height);

		this.background
			.attr('width', this.width)
			.attr('height', this.height);
{'fraction_non_alphanumeric': 0.15336813323390505, 'fraction_numerical': 0.007788549175573784, 'mean_word_length': 4.544327055581075, 'pattern_counts': {'":': 0, '<': 49, '<?xml version=': 0, '>': 81, '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': '256242', 'n_tokens_mistral': 4052, 'n_tokens_neox': 3758, 'n_words': 847}