blob: 3465d685ce90429dff4a86f15ebedbe51142b0e7 [file] [log] [blame]
/********************************************************************************
* Copyright (c) 2015-2018 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
********************************************************************************/
import { Component, OnInit, Input, OnDestroy, EventEmitter, Output, OnChanges, SimpleChanges } from '@angular/core';
import { ChannelSelectionRow, MeasuredValues} from '../../model/chartviewer.model';
import { Subscription, of } from 'rxjs';
import { ChartViewerDataService } from '../../services/chart-viewer-data.service';
import { ChartViewerService } from '../../services/chart-viewer.service';
import { ArrayUtilService } from 'src/app/core/services/array-util.service';
import { MDMNotificationService } from 'src/app/core/mdm-notification.service';
import { TYPE_MEASUREMENT, TYPE_CHANNELGROUP, TYPE_CHANNEL } from '../../model/constants';
import { tap } from 'rxjs/operators';
import { Node } from '../../../navigator/node';
@Component({
selector: 'mdm5-xy-chart-data-selection-panel',
templateUrl: './xy-chart-data-selection-panel.component.html',
styleUrls: ['../../chart-viewer.style.css']
})
export class XyChartDataSelectionPanelComponent implements OnInit, OnDestroy, OnChanges {
@Input()
public channelGroups: Node[];
@Input()
public channels: Map<string, Node[]>;
@Input()
public xyMode: boolean;
@Output()
public onSelectionChanged = new EventEmitter<ChannelSelectionRow[]>();
// select options for channels
public yChannelOptions: Node[] = [];
public xChannelOptions: {[key: string]: Node[]} = {};
// channel(group) selection for chart
// !! change via setter to update cache for workarround !!
public selectedChannelGroups: Node[] = [];
public selectedYChannels: Node[] = [];
private xYcache = new Map<string, MeasuredValues[]>();
private selectionChanged = false;
// listbox workarround to determine toggled item(s)
private lastChannelGroupSelection: Node[] = [];
private lastYChannelSelection: Node[] = [];
public selectedChannelRows: ChannelSelectionRow[] = [];
// Toggle selection panel visibility
public hiddenGroups = false;
public hiddenYChannels = false;
// for x-channel default value
private independentChannels = new Map<string, Node>();
/********** subscriptions */
private chartViewerSub: Subscription;
constructor(private chartviewerDataService: ChartViewerDataService,
private chartViewerService: ChartViewerService,
private arrayUtil: ArrayUtilService,
private notificationService: MDMNotificationService) { }
/**************** Angular lifecycle-hooks ****************************/
ngOnInit() {
this.chartViewerSub = this.chartViewerService.onNodeMetaChange().subscribe(node => this.handleNodeSelectionInNavTree(node));
}
ngOnDestroy() {
this.chartViewerSub.unsubscribe();
}
ngOnChanges(changes: SimpleChanges) {
if (changes['xyMode']) {
this.reloadAxisSelectOptions().subscribe(() => {
if (this.selectionChanged) {
this.fireSelectionChanged();
this.selectionChanged = false;
}
});
}
}
/**************** Html-template listeners *****************************/
// x-axis
public onSelectedXChannelChanged(event: any) {
if (event.value == undefined) {
// work arround: if x-axis is deselected for an y-channel, plat over independent channel.
// should force selection on dropdown instead.
this.selectedChannelRows
.filter(row => row.xChannel == undefined)
.map(row => row.xChannel = this.independentChannels.get(row.channelGroup.id));
}
this.fireSelectionChanged();
}
// y-axis
public onSelectedYChannelsChanged(event: any) {
let channel: Node;
// Deselect
if (this.selectedYChannels.length < this.lastYChannelSelection.length) {
channel = this.lastYChannelSelection.find(yChannel => !this.selectedYChannels.some(c => yChannel.id === c.id));
// .forEach(yChannel => this.addOrRemoveRow(this.findChannelGroup(yChannel), yChannel));
// Select
} else {
channel = this.selectedYChannels.find(group => !this.lastYChannelSelection.some(g => group.id === g.id));
}
this.addOrRemoveRow(this.findChannelGroup(channel), channel);
this.lastYChannelSelection = this.selectedYChannels;
this.fireSelectionChanged();
}
// // y-axis
// public onSelectedYChannelsChanged(event: any) {
// const yChannel = event.itemValue;
// this.addOrRemoveRow(this.findChannelGroup(yChannel), );
// this.fireSelectionChanged();
// }
// channelgroups
// public onSelectedChannelGroupsChanged(event: any) {
// const item = event.itemValue;
// // item is undefined, if select-all check box is clicked.
// if (item == undefined) {
// if (this.arrayUtil.isNotEmpty(this.selectedChannelGroups)) {
// this.selectedChannelGroups.forEach(g => this.handleOnSelectChannelGroup(g));
// } else {
// this.resetChannelData();
// this.fireSelectionChanged();
// }
// } else {
// // item contains toggled channelGroup if single select box is clicked
// this.handleOnSelectChannelGroup(item);
// }
// }
public onSelectedChannelGroupsChanged(event: any) {
// All deselected
if (this.selectedChannelGroups.length === 0) {
this.resetChannelData();
this.fireSelectionChanged();
} else {
// Deselect
if (this.selectedChannelGroups.length < this.lastChannelGroupSelection.length) {
this.lastChannelGroupSelection
.filter(group => !this.selectedChannelGroups.some(g => group.id === g.id))
.forEach(group => this.handleDeselectChannelGroup(group));
// Select
} else {
this.selectedChannelGroups
.filter(group => !this.lastChannelGroupSelection.some(g => group.id === g.id))
.forEach(group => this.handleSelectChannelGroup(group));
}
}
this.lastChannelGroupSelection = this.selectedChannelGroups;
}
// private handleOnSelectChannelGroup(channelGroup: any) {
// // the currently toggled group is selected (ngModel is updated faster then event.)
// if (this.isNodeIn(channelGroup, this.selectedChannelGroups)) {
// this.handleSelectChannelGroup(channelGroup);
// // the currently toggled group is deselected
// } else {
// this.handleDeselectChannelGroup(channelGroup);
// }
// }
public onToggleChannelGroupPanel(event: Event) {
this.hiddenGroups = !this.hiddenGroups;
}
public onToggleYChannelPanel(event: Event) {
this.hiddenYChannels = !this.hiddenYChannels;
}
private handleDeselectChannelGroup(channelGroup: Node) {
// remove rows
if (this.arrayUtil.isNotEmpty(this.selectedChannelRows)) {
this.selectedChannelRows = this.selectedChannelRows.filter(row => row.channelGroup.id !== channelGroup.id);
}
// remove selection
if (this.arrayUtil.isNotEmpty(this.selectedYChannels)) {
this.setSelectedYChannels(this.selectedYChannels.filter(channel =>
this.channels.get(channelGroup.id).findIndex(c => channel.id === c.id) === -1));
}
this.reloadAxisSelectOptions().subscribe(() => this.fireSelectionChanged());
}
/**
* Handle channelGroup selection via multiselect in html template
* @param channelGroup
*/
private handleSelectChannelGroup(channelGroup: Node) {
const cached = this.xYcache.get(channelGroup.id);
if (cached != undefined) {
this.setChannelOptions(cached, channelGroup);
this.fireSelectionChanged();
} else {
this.chartviewerDataService.readAxisType(channelGroup)
.subscribe(mvls => {
this.setChannelOptions(mvls, channelGroup);
this.fireSelectionChanged();
});
}
}
private handleNodeSelectionInNavTree(node: Node) {
if (node != undefined) {
switch (node.type) {
case TYPE_MEASUREMENT:
this.xYcache.clear();
this.resetChannelData();
this.fireSelectionChanged();
break;
case TYPE_CHANNELGROUP:
this.cleanOldState();
this.addChannelGroupToSelection(node.id, node);
break;
case TYPE_CHANNEL:
this.cleanOldState();
// add related channel group to selection
if (this.arrayUtil.isNotEmpty(this.channelGroups) && this.channels != undefined) {
const channelGroup = this.findChannelGroup(node);
// this.addChannelGroupToSelection(channelGroupId, this.channelGroups.find(cg => cg.id === channelGroupId));
if (!this.isNodeIn(channelGroup, this.selectedChannelGroups)) {
this.setSelectedChannelGroups(this.selectedChannelGroups.concat([channelGroup]));
this.chartviewerDataService.readAxisType(channelGroup)
.subscribe(mvls => {
this.setChannelOptions(mvls, channelGroup);
this.addChannelToSelection(node, channelGroup);
});
} else {
// if channel is in axis type y, set as selected, if not selected already
this.addChannelToSelection(node, channelGroup);
}
}
break;
}
}
}
/**
* reset chart data, select options and selection, if selected channelgroups
* contain a channelGroup which does not belongto this measurement.
*
* notice: this works for channels aswell, since a channel cannot be selected without
* selecting the parent channelGroup aswell.
*/
private cleanOldState() {
if (this.arrayUtil.isNotEmpty(this.selectedChannelGroups)
&& this.selectedChannelGroups.some(selected => !this.isNodeIn(selected, this.channelGroups))) {
this.xYcache.clear();
this.resetChannelData();
}
}
/**
* Handle channel selection via nav-tree
* @param channel
* @param channelGroup
*/
private addChannelToSelection(channel: Node, channelGroup: Node) {
// adds channel to selected y-channels if possible and creates row in table
if (this.isNodeIn(channel, this.yChannelOptions)) {
const index = this.selectedYChannels.findIndex(c => c.id === channel.id);
if (index === -1) {
this.setSelectedYChannels(this.selectedYChannels.concat([channel]));
this.addRow(channelGroup, channel);
this.fireSelectionChanged();
}
/**
* @TODO -> expected behaviour on selecting x-channel in tree.
* suggestion: set it as x-channel for all selected y-channels of that channelgroup.
*/
// if channel is of axis type x, set ... ?
} else if (this.isNodeIn(channel, this.xChannelOptions[channelGroup.id])) {
this.notificationService.notifyWarn('Selected X-Channel', 'Selected channel is of type x-axis. Change x/y mode.');
} else {
this.notificationService.notifyError('Unknown channel option', 'Not sure where you found this..');
}
}
/**
* Handle channelGroup selection via nav-tree
* @param id
* @param channelGroup
*/
private addChannelGroupToSelection(id: string, channelGroup: Node) {
if (this.selectedChannelGroups == undefined) {
this.setSelectedChannelGroups([]);
}
if (!this.isNodeIn(channelGroup, this.selectedChannelGroups)) {
this.setSelectedChannelGroups(this.selectedChannelGroups.concat([channelGroup]));
this.handleSelectChannelGroup(channelGroup);
}
}
/**
* Adds options for x- and y-channels depending on given channelGroup
*
* @param measuredValues
* @param channelGroup
*/
private setChannelOptions(measuredValues: MeasuredValues[], channelGroup: Node) {
if (channelGroup != undefined) {
// cache measuredValues to avoid reloading
this.xYcache.set(channelGroup.id, measuredValues);
// create empty array if property not exists
this.xChannelOptions[channelGroup.id] = this.xChannelOptions[channelGroup.id] || [];
if (this.xyMode) {
measuredValues.forEach(mvl => {
const tmpChannel = this.channels.get(channelGroup.id).filter(c => c.name === mvl.name)[0];
if (mvl.axisType === 'X_AXIS' || ( mvl.axisType == undefined && mvl.independent)) {
this.xChannelOptions[channelGroup.id] = this.xChannelOptions[channelGroup.id].concat(tmpChannel);
// cache independent channels for default x-axis.
if (mvl.independent) {
this.independentChannels.set(channelGroup.id, tmpChannel);
}
} else if (mvl.axisType === 'Y_AXIS') {
this.yChannelOptions = this.yChannelOptions.concat(tmpChannel);
}
});
} else {
// cache independent channel for default x-axis.
const value = measuredValues.find(mvl => mvl.independent);
const tmpChannel = this.channels.get(channelGroup.id).filter(c => c.name === value.name)[0];
this.independentChannels.set(channelGroup.id, tmpChannel);
const tmpChannels = this.channels.get(channelGroup.id);
this.xChannelOptions[channelGroup.id] = this.xChannelOptions[channelGroup.id].concat(tmpChannels);
this.yChannelOptions = this.yChannelOptions.concat(tmpChannels);
}
}
}
/**
* Resets selections and select options to empty state.
*/
private resetChannelData() {
this.setSelectedChannelGroups([]);
this.selectedChannelRows = [];
this.setSelectedYChannels([]);
this.channelGroups = [].concat(this.channelGroups);
this.xChannelOptions = {};
this.yChannelOptions = [];
}
// /**
// * Reloads the select options for the x- and y-axis dropdowns
// */
// private reloadAxisSelectOptions() {
// this.xChannelOptions = {};
// this.yChannelOptions = [];
// if (this.arrayUtil.isNotEmpty(this.selectedChannelGroups)) {
// const uncached = this.selectedChannelGroups.filter(g => this.xYcache.get(g.id) === undefined);
// const cached = this.selectedChannelGroups.filter(g => this.xYcache.get(g.id) !== undefined);
// const obs = uncached.map(cg => this.chartviewerDataService.readAxisType(cg));
// return forkJoin(
// forkJoin(obs)
// .pipe(tap(array => array.forEach((mvls, index) => this.setChannelOptions(mvls, uncached[index])))),
// of(cached.map((group, index) => this.setChannelOptions(this.xYcache.get(group.id), cached[index]))))
// .pipe(tap(() => this.removeSelectionsNotMatchingXYMode()));
// }
// return of();
// }
/**
* Reloads the select options for the x- and y-axis dropdowns
*/
private reloadAxisSelectOptions() {
this.xChannelOptions = {};
this.yChannelOptions = [];
if (this.arrayUtil.isNotEmpty(this.selectedChannelGroups)) {
const cached = this.selectedChannelGroups.filter(g => this.xYcache.get(g.id) !== undefined);
return of(cached.map((group, index) => this.setChannelOptions(this.xYcache.get(group.id), cached[index])))
.pipe(tap(() => this.removeSelectionsNotMatchingXYMode()));
}
return of();
}
private removeSelectionsNotMatchingXYMode() {
if (this.selectedYChannels.length > 0) {
const l = this.selectedChannelRows.length;
this.selectedChannelRows = this.selectedChannelRows.filter(row => this.isNodeIn(row.yChannel, this.yChannelOptions));
this.setSelectedYChannels(this.selectedYChannels.filter(channel => this.isNodeIn(channel, this.yChannelOptions)));
this.selectionChanged = l !== this.selectedChannelRows.length;
}
}
/**
* Row control for table with x-channel selection.
*
* @param channelGroup
* @param yChannel
*/
private addOrRemoveRow(channelGroup: Node, yChannel: Node) {
const index = this.selectedChannelRows.findIndex(row => row.yChannel.id === yChannel.id);
if (index > -1) {
this.selectedChannelRows.splice(index, 1);
this.selectedChannelRows = [].concat(this.selectedChannelRows);
} else {
this.addRow(channelGroup, yChannel);
}
}
/**
* Add row to selection
* @param channelGroup
* @param yChannel
*/
private addRow(channelGroup: Node, yChannel: Node) {
const row = new ChannelSelectionRow(channelGroup, yChannel, this.independentChannels.get(channelGroup.id));
this.selectedChannelRows.push(row);
}
/**
* Returns true if node with given id is in the given array.
*
* @param array the array
* @param id the id
*/
private isNodeIn(node: Node, array: Node[]) {
let response = false;
if (node != undefined && this.arrayUtil.isNotEmpty(array)) {
response = array.findIndex(n => n.id === node.id) > -1;
}
return response;
}
/**
* Looks up parent channelGroup for channel via the channel map.
*
* @param channel
*/
private findChannelGroup(channel: Node) {
const channelGroupId = Array.from(this.channels.keys())
.find(key => this.isNodeIn(channel, this.channels.get(key)));
return this.channelGroups.find(cg => cg.id === channelGroupId);
}
private fireSelectionChanged() {
this.onSelectionChanged.emit(this.selectedChannelRows);
}
private setSelectedYChannels(channels: Node[]) {
this.selectedYChannels = channels;
this.lastYChannelSelection = channels;
}
private setSelectedChannelGroups(channelGroups: Node[]) {
this.selectedChannelGroups = channelGroups;
this.lastChannelGroupSelection = channelGroups;
}
}