aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts14
-rw-r--r--src/client/northstar/core/BaseObject.ts12
-rw-r--r--src/client/northstar/core/attribute/AttributeModel.ts111
-rw-r--r--src/client/northstar/core/attribute/AttributeTransformationModel.ts47
-rw-r--r--src/client/northstar/core/attribute/CalculatedAttributeModel.ts42
-rw-r--r--src/client/northstar/core/brusher/BrushLinkModel.ts40
-rw-r--r--src/client/northstar/core/brusher/IBaseBrushable.ts13
-rw-r--r--src/client/northstar/core/brusher/IBaseBrusher.ts11
-rw-r--r--src/client/northstar/core/filter/FilterModel.ts81
-rw-r--r--src/client/northstar/core/filter/FilterOperand.ts5
-rw-r--r--src/client/northstar/core/filter/FilterType.ts6
-rw-r--r--src/client/northstar/core/filter/IBaseFilterConsumer.ts10
-rw-r--r--src/client/northstar/core/filter/IBaseFilterProvider.ts8
-rw-r--r--src/client/northstar/core/filter/ValueComparision.ts74
-rw-r--r--src/client/northstar/model/ModelExtensions.ts48
-rw-r--r--src/client/northstar/model/ModelHelpers.ts215
-rw-r--r--src/client/northstar/model/binRanges/AlphabeticVisualBinRange.ts52
-rw-r--r--src/client/northstar/model/binRanges/DateTimeVisualBinRange.ts105
-rw-r--r--src/client/northstar/model/binRanges/NominalVisualBinRange.ts52
-rw-r--r--src/client/northstar/model/binRanges/QuantitativeVisualBinRange.ts90
-rw-r--r--src/client/northstar/model/binRanges/VisualBinRange.ts36
-rw-r--r--src/client/northstar/model/binRanges/VisualBinRangeHelper.ts71
-rw-r--r--src/client/northstar/operations/BaseOperation.ts174
-rw-r--r--src/client/northstar/operations/HistogramOperation.ts111
-rw-r--r--src/client/northstar/utils/ArrayUtil.ts90
-rw-r--r--src/client/northstar/utils/Extensions.ts20
-rw-r--r--src/client/northstar/utils/GeometryUtil.ts129
-rw-r--r--src/client/northstar/utils/IDisposable.ts3
-rw-r--r--src/client/northstar/utils/IEquatable.ts3
-rw-r--r--src/client/northstar/utils/KeyCodes.ts137
-rw-r--r--src/client/northstar/utils/MathUtil.ts236
-rw-r--r--src/client/northstar/utils/PartialClass.ts7
-rw-r--r--src/client/northstar/utils/Utils.ts75
-rw-r--r--src/client/views/Main.tsx47
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx4
-rw-r--r--src/client/views/nodes/FieldView.tsx7
-rw-r--r--src/client/views/nodes/HistogramBox.scss8
-rw-r--r--src/client/views/nodes/HistogramBox.tsx67
-rw-r--r--src/fields/KeyStore.ts1
39 files changed, 2248 insertions, 14 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 4ed1fc3ed..b1e1f8c7b 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -22,6 +22,7 @@ import { KeyValueBox } from "../views/nodes/KeyValueBox";
import { PDFBox } from "../views/nodes/PDFBox";
import { VideoBox } from "../views/nodes/VideoBox";
import { WebBox } from "../views/nodes/WebBox";
+import { HistogramBox } from "../views/nodes/HistogramBox";
export interface DocumentOptions {
x?: number;
@@ -44,6 +45,7 @@ export interface DocumentOptions {
export namespace Documents {
let textProto: Document;
+ let histoProto: Document;
let imageProto: Document;
let webProto: Document;
let collProto: Document;
@@ -52,6 +54,7 @@ export namespace Documents {
let audioProto: Document;
let pdfProto: Document;
const textProtoId = "textProto";
+ const histoProtoId = "histoProto";
const pdfProtoId = "pdfProto";
const imageProtoId = "imageProto";
const webProtoId = "webProto";
@@ -62,9 +65,10 @@ export namespace Documents {
export function initProtos(callback: () => void) {
Server.GetFields([collProtoId, textProtoId, imageProtoId], (fields) => {
+ textProto = fields[textProtoId] as Document;
+ histoProto = fields[histoProtoId] as Document;
collProto = fields[collProtoId] as Document;
imageProto = fields[imageProtoId] as Document;
- textProto = fields[textProtoId] as Document;
webProto = fields[webProtoId] as Document;
kvpProto = fields[kvpProtoId] as Document;
callback();
@@ -114,6 +118,11 @@ export namespace Documents {
}
return imageProto;
}
+ function GetHistogramPrototype(): Document {
+ return histoProto ? histoProto :
+ histoProto = setupPrototypeOptions(textProtoId, "HISTO PROTOTYPE", HistogramBox.LayoutString(),
+ { x: 0, y: 0, width: 300, height: 300, layoutKeys: [KeyStore.Data] });
+ }
function GetTextPrototype(): Document {
return textProto ? textProto :
textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
@@ -176,6 +185,9 @@ export namespace Documents {
return assignToDelegate(SetInstanceOptions(GetAudioPrototype(), options, [new URL(url), AudioField]), options);
}
+ export function HistogramDocument(options: DocumentOptions = {}) {
+ return assignToDelegate(SetInstanceOptions(GetHistogramPrototype(), options, ["", TextField]).MakeDelegate(), options);
+ }
export function TextDocument(options: DocumentOptions = {}) {
return assignToDelegate(SetInstanceOptions(GetTextPrototype(), options, ["", TextField]).MakeDelegate(), options);
}
diff --git a/src/client/northstar/core/BaseObject.ts b/src/client/northstar/core/BaseObject.ts
new file mode 100644
index 000000000..e9e766e31
--- /dev/null
+++ b/src/client/northstar/core/BaseObject.ts
@@ -0,0 +1,12 @@
+import { IEquatable } from '../utils/IEquatable'
+import { IDisposable } from '../utils/IDisposable'
+
+export class BaseObject implements IEquatable, IDisposable {
+
+ public Equals(other: Object): boolean {
+ return this == other;
+ }
+
+ public Dispose(): void {
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/core/attribute/AttributeModel.ts b/src/client/northstar/core/attribute/AttributeModel.ts
new file mode 100644
index 000000000..124a5b45a
--- /dev/null
+++ b/src/client/northstar/core/attribute/AttributeModel.ts
@@ -0,0 +1,111 @@
+import { Attribute, DataType, VisualizationHint } from '../../model/idea/idea'
+import { BaseObject } from '../BaseObject'
+import { observable } from "mobx";
+
+export abstract class AttributeModel extends BaseObject {
+ public abstract get DisplayName(): string;
+ public abstract get CodeName(): string;
+ public abstract get DataType(): DataType;
+ public abstract get VisualizationHints(): VisualizationHint[];
+}
+
+export class ColumnAttributeModel extends AttributeModel {
+ public Attribute: Attribute;
+
+ constructor(attribute: Attribute) {
+ super();
+ this.Attribute = attribute;
+ }
+
+ public get DataType(): DataType {
+ return this.Attribute.dataType ? this.Attribute.dataType : DataType.Undefined;
+ }
+
+ public get DisplayName(): string {
+ return this.Attribute.displayName ? this.Attribute.displayName.ReplaceAll("_", " ") : "";
+ }
+
+ public get CodeName(): string {
+ return this.Attribute.rawName ? this.Attribute.rawName : "";
+ }
+
+ public get VisualizationHints(): VisualizationHint[] {
+ return this.Attribute.visualizationHints ? this.Attribute.visualizationHints : [];
+ }
+
+ public Equals(other: ColumnAttributeModel): boolean {
+ return this.Attribute.rawName == other.Attribute.rawName;
+ }
+}
+
+export class CodeAttributeModel extends AttributeModel {
+ private _visualizationHints: VisualizationHint[];
+
+ public CodeName: string;
+
+ @observable
+ public Code: string;
+
+ constructor(code: string, codeName: string, displayName: string, visualizationHints: VisualizationHint[]) {
+ super();
+ this.Code = code;
+ this.CodeName = codeName;
+ this.DisplayName = displayName;
+ this._visualizationHints = visualizationHints;
+ }
+
+ public get DataType(): DataType {
+ return DataType.Undefined;
+ }
+
+ @observable
+ public DisplayName: string;
+
+ public get VisualizationHints(): VisualizationHint[] {
+ return this._visualizationHints;
+ }
+
+ public Equals(other: CodeAttributeModel): boolean {
+ return this.CodeName === other.CodeName;
+ }
+
+}
+
+export class BackendAttributeModel extends AttributeModel {
+ private _dataType: DataType;
+ private _displayName: string;
+ private _codeName: string;
+ private _visualizationHints: VisualizationHint[];
+
+ public Id: string;
+
+ constructor(id: string, dataType: DataType, displayName: string, codeName: string, visualizationHints: VisualizationHint[]) {
+ super();
+ this.Id = id;
+ this._dataType = dataType;
+ this._displayName = displayName;
+ this._codeName = codeName;
+ this._visualizationHints = visualizationHints;
+ }
+
+ public get DataType(): DataType {
+ return this._dataType;
+ }
+
+ public get DisplayName(): string {
+ return this._displayName.ReplaceAll("_", " ");;
+ }
+
+ public get CodeName(): string {
+ return this._codeName;
+ }
+
+ public get VisualizationHints(): VisualizationHint[] {
+ return this._visualizationHints;
+ }
+
+ public Equals(other: BackendAttributeModel): boolean {
+ return this.Id == other.Id;
+ }
+
+} \ No newline at end of file
diff --git a/src/client/northstar/core/attribute/AttributeTransformationModel.ts b/src/client/northstar/core/attribute/AttributeTransformationModel.ts
new file mode 100644
index 000000000..cc5aa7154
--- /dev/null
+++ b/src/client/northstar/core/attribute/AttributeTransformationModel.ts
@@ -0,0 +1,47 @@
+;
+import { computed, observable } from "mobx";
+import { AggregateFunction } from "../../model/idea/idea";
+import { AttributeModel } from "./AttributeModel";
+import { IEquatable } from "../../utils/IEquatable";
+
+export class AttributeTransformationModel implements IEquatable {
+
+ @observable public AggregateFunction: AggregateFunction;
+ @observable public AttributeModel: AttributeModel;
+
+ constructor(attributeModel: AttributeModel, aggregateFunction: AggregateFunction = AggregateFunction.None) {
+ this.AttributeModel = attributeModel;
+ this.AggregateFunction = aggregateFunction;
+ }
+
+ @computed
+ public get PresentedName(): string {
+ var displayName = this.AttributeModel.DisplayName;
+ if (this.AggregateFunction === AggregateFunction.Count) {
+ return "count";
+ }
+ if (this.AggregateFunction === AggregateFunction.Avg)
+ displayName = "avg(" + displayName + ")";
+ else if (this.AggregateFunction === AggregateFunction.Max)
+ displayName = "max(" + displayName + ")";
+ else if (this.AggregateFunction === AggregateFunction.Min)
+ displayName = "min(" + displayName + ")";
+ else if (this.AggregateFunction === AggregateFunction.Sum)
+ displayName = "sum(" + displayName + ")";
+ else if (this.AggregateFunction === AggregateFunction.SumE)
+ displayName = "sumE(" + displayName + ")";
+
+ return displayName;
+ }
+
+ public clone(): AttributeTransformationModel {
+ var clone = new AttributeTransformationModel(this.AttributeModel);
+ clone.AggregateFunction = this.AggregateFunction;
+ return clone;
+ }
+
+ public Equals(other: AttributeTransformationModel): boolean {
+ return this.AggregateFunction == other.AggregateFunction &&
+ this.AttributeModel.Equals(other.AttributeModel);
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/core/attribute/CalculatedAttributeModel.ts b/src/client/northstar/core/attribute/CalculatedAttributeModel.ts
new file mode 100644
index 000000000..ab96c794d
--- /dev/null
+++ b/src/client/northstar/core/attribute/CalculatedAttributeModel.ts
@@ -0,0 +1,42 @@
+import { BackendAttributeModel, AttributeModel, CodeAttributeModel } from "./AttributeModel";
+import { DataType, VisualizationHint } from '../../model/idea/idea'
+
+export class CalculatedAttributeManager {
+ public static AllCalculatedAttributes: Array<AttributeModel> = new Array<AttributeModel>();
+
+ public static Clear() {
+ this.AllCalculatedAttributes = new Array<AttributeModel>();
+ }
+
+ public static CreateBackendAttributeModel(id: string, dataType: DataType, displayName: string, codeName: string, visualizationHints: VisualizationHint[]): BackendAttributeModel {
+ var filtered = this.AllCalculatedAttributes.filter(am => {
+ if (am instanceof BackendAttributeModel &&
+ am.Id == id) {
+ return true;
+ }
+ return false;
+ });
+ if (filtered.length > 0) {
+ return filtered[0] as BackendAttributeModel;
+ }
+ var newAttr = new BackendAttributeModel(id, dataType, displayName, codeName, visualizationHints);
+ this.AllCalculatedAttributes.push(newAttr);
+ return newAttr;
+ }
+
+ public static CreateCodeAttributeModel(code: string, codeName: string, visualizationHints: VisualizationHint[]): CodeAttributeModel {
+ var filtered = this.AllCalculatedAttributes.filter(am => {
+ if (am instanceof CodeAttributeModel &&
+ am.CodeName == codeName) {
+ return true;
+ }
+ return false;
+ });
+ if (filtered.length > 0) {
+ return filtered[0] as CodeAttributeModel;
+ }
+ var newAttr = new CodeAttributeModel(code, codeName, codeName.ReplaceAll("_", " "), visualizationHints);
+ this.AllCalculatedAttributes.push(newAttr);
+ return newAttr;
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/core/brusher/BrushLinkModel.ts b/src/client/northstar/core/brusher/BrushLinkModel.ts
new file mode 100644
index 000000000..e3ac43367
--- /dev/null
+++ b/src/client/northstar/core/brusher/BrushLinkModel.ts
@@ -0,0 +1,40 @@
+import { IBaseBrushable } from '../brusher/IBaseBrushable'
+import { IBaseBrusher } from '../brusher/IBaseBrusher'
+import { Utils } from '../../utils/Utils'
+import { IEquatable } from '../../utils/IEquatable';
+
+export class BrushLinkModel<T> implements IEquatable {
+
+ public From: IBaseBrusher<T>;
+
+ public To: IBaseBrushable<T>;
+
+ public Color: number = 0;
+
+ constructor(from: IBaseBrusher<T>, to: IBaseBrushable<T>) {
+ this.From = from;
+ this.To = to;
+ }
+
+ public static overlaps(start: number, end: number, otherstart: number, otherend: number): boolean {
+ if (start > otherend || otherstart > end)
+ return false;
+ return true;
+ }
+ public static Connected<T>(from: IBaseBrusher<T>, to: IBaseBrushable<T>): boolean {
+ var connected = (Math.abs(from.Position.x + from.Size.x - to.Position.x) <= 60 &&
+ this.overlaps(from.Position.y, from.Position.y + from.Size.y, to.Position.y, to.Position.y + to.Size.y)
+ ) ||
+ (Math.abs(to.Position.x + to.Size.x - from.Position.x) <= 60 &&
+ this.overlaps(to.Position.y, to.Position.y + to.Size.y, from.Position.y, from.Position.y + from.Size.y)
+ );
+ return connected;
+ }
+
+ public Equals(other: Object): boolean {
+ if (!Utils.EqualityHelper(this, other)) return false;
+ if (!this.From.Equals((other as BrushLinkModel<T>).From)) return false;
+ if (!this.To.Equals((other as BrushLinkModel<T>).To)) return false;
+ return true;
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/core/brusher/IBaseBrushable.ts b/src/client/northstar/core/brusher/IBaseBrushable.ts
new file mode 100644
index 000000000..07d4e7580
--- /dev/null
+++ b/src/client/northstar/core/brusher/IBaseBrushable.ts
@@ -0,0 +1,13 @@
+import { BrushLinkModel } from '../brusher/BrushLinkModel'
+import { PIXIPoint } from '../../utils/MathUtil'
+import { IEquatable } from '../../utils/IEquatable';
+
+export interface IBaseBrushable<T> extends IEquatable {
+ BrusherModels: Array<BrushLinkModel<T>>;
+ BrushColors: Array<number>;
+ Position: PIXIPoint;
+ Size: PIXIPoint;
+}
+export function instanceOfIBaseBrushable<T>(object: any): object is IBaseBrushable<T> {
+ return 'BrusherModels' in object;
+} \ No newline at end of file
diff --git a/src/client/northstar/core/brusher/IBaseBrusher.ts b/src/client/northstar/core/brusher/IBaseBrusher.ts
new file mode 100644
index 000000000..d7ae65464
--- /dev/null
+++ b/src/client/northstar/core/brusher/IBaseBrusher.ts
@@ -0,0 +1,11 @@
+import { PIXIPoint } from '../../utils/MathUtil'
+import { IEquatable } from '../../utils/IEquatable';
+
+
+export interface IBaseBrusher<T> extends IEquatable {
+ Position: PIXIPoint;
+ Size: PIXIPoint;
+}
+export function instanceOfIBaseBrusher<T>(object: any): object is IBaseBrusher<T> {
+ return 'BrushableModels' in object;
+} \ No newline at end of file
diff --git a/src/client/northstar/core/filter/FilterModel.ts b/src/client/northstar/core/filter/FilterModel.ts
new file mode 100644
index 000000000..3c4cfc4a7
--- /dev/null
+++ b/src/client/northstar/core/filter/FilterModel.ts
@@ -0,0 +1,81 @@
+import { ValueComparison } from "./ValueComparision";
+import { Utils } from "../../utils/Utils";
+
+export class FilterModel {
+ public ValueComparisons: ValueComparison[];
+ constructor() {
+ this.ValueComparisons = new Array<ValueComparison>();
+ }
+
+ public Equals(other: FilterModel): boolean {
+ if (!Utils.EqualityHelper(this, other)) return false;
+ if (!this.isSame(this.ValueComparisons, (other as FilterModel).ValueComparisons)) return false;
+ return true;
+ }
+
+ private isSame(a: ValueComparison[], b: ValueComparison[]): boolean {
+ if (a.length !== b.length) {
+ return false;
+ }
+ for (let i = 0; i < a.length; i++) {
+ let valueComp = a[i];
+ if (!valueComp.Equals(b[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public ToPythonString(): string {
+ let ret = "(" + this.ValueComparisons.map(vc => vc.ToPythonString()).join("&&") + ")";
+ return ret;
+ }
+
+ public static And(filters: string[]): string {
+ let ret = filters.filter(f => f !== "").join(" && ");
+ return ret;
+ }
+
+ // public static GetFilterModelsRecursive(filterGraphNode: GraphNode<BaseOperationViewModel, FilterLinkViewModel>,
+ // visitedFilterProviders: Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>, filterModels: FilterModel[], isFirst: boolean): string {
+ // let ret = "";
+ // if (Utils.isBaseFilterProvider(filterGraphNode.Data)) {
+ // visitedFilterProviders.add(filterGraphNode);
+ // let filtered = filterGraphNode.Data.FilterModels.filter(fm => fm && fm.ValueComparisons.length > 0);
+ // if (!isFirst && filtered.length > 0) {
+ // filterModels.push(...filtered);
+ // ret = "(" + filterGraphNode.Data.FilterModels.filter(fm => fm != null).map(fm => fm.ToPythonString()).join(" || ") + ")";
+ // }
+ // }
+ // if (Utils.isBaseFilterConsumer(filterGraphNode.Data) && filterGraphNode.Links != null) {
+ // let children = new Array<string>();
+ // let linkedGraphNodes = filterGraphNode.Links.get(LinkType.Filter);
+ // if (linkedGraphNodes != null) {
+ // for (let i = 0; i < linkedGraphNodes.length; i++) {
+ // let linkVm = linkedGraphNodes[i].Data;
+ // let linkedGraphNode = linkedGraphNodes[i].Target;
+ // if (!visitedFilterProviders.has(linkedGraphNode)) {
+ // let child = FilterModel.GetFilterModelsRecursive(linkedGraphNode, visitedFilterProviders, filterModels, false);
+ // if (child !== "") {
+ // if (linkVm.IsInverted) {
+ // child = "! " + child;
+ // }
+ // children.push(child);
+ // }
+ // }
+ // }
+ // }
+
+ // let childrenJoined = children.join(filterGraphNode.Data.FilterOperand === FilterOperand.AND ? " && " : " || ");
+ // if (children.length > 0) {
+ // if (ret !== "") {
+ // ret = "(" + ret + " && (" + childrenJoined + "))";
+ // }
+ // else {
+ // ret = "(" + childrenJoined + ")";
+ // }
+ // }
+ // }
+ // return ret;
+ // }
+} \ No newline at end of file
diff --git a/src/client/northstar/core/filter/FilterOperand.ts b/src/client/northstar/core/filter/FilterOperand.ts
new file mode 100644
index 000000000..2e8e8d6a0
--- /dev/null
+++ b/src/client/northstar/core/filter/FilterOperand.ts
@@ -0,0 +1,5 @@
+export enum FilterOperand
+{
+ AND,
+ OR
+} \ No newline at end of file
diff --git a/src/client/northstar/core/filter/FilterType.ts b/src/client/northstar/core/filter/FilterType.ts
new file mode 100644
index 000000000..9adbc087f
--- /dev/null
+++ b/src/client/northstar/core/filter/FilterType.ts
@@ -0,0 +1,6 @@
+export enum FilterType
+{
+ Filter,
+ Brush,
+ Slice
+} \ No newline at end of file
diff --git a/src/client/northstar/core/filter/IBaseFilterConsumer.ts b/src/client/northstar/core/filter/IBaseFilterConsumer.ts
new file mode 100644
index 000000000..e687acb8a
--- /dev/null
+++ b/src/client/northstar/core/filter/IBaseFilterConsumer.ts
@@ -0,0 +1,10 @@
+import { FilterOperand } from '../filter/FilterOperand'
+import { IEquatable } from '../../utils/IEquatable'
+
+export interface IBaseFilterConsumer extends IEquatable {
+ FilterOperand: FilterOperand;
+}
+
+export function instanceOfIBaseFilterConsumer(object: any): object is IBaseFilterConsumer {
+ return 'FilterOperand' in object;
+} \ No newline at end of file
diff --git a/src/client/northstar/core/filter/IBaseFilterProvider.ts b/src/client/northstar/core/filter/IBaseFilterProvider.ts
new file mode 100644
index 000000000..d082bfe12
--- /dev/null
+++ b/src/client/northstar/core/filter/IBaseFilterProvider.ts
@@ -0,0 +1,8 @@
+import { FilterModel } from '../filter/FilterModel'
+
+export interface IBaseFilterProvider {
+ FilterModels: Array<FilterModel>;
+}
+export function instanceOfIBaseFilterProvider(object: any): object is IBaseFilterProvider {
+ return 'FilterModels' in object;
+} \ No newline at end of file
diff --git a/src/client/northstar/core/filter/ValueComparision.ts b/src/client/northstar/core/filter/ValueComparision.ts
new file mode 100644
index 000000000..1e729d06e
--- /dev/null
+++ b/src/client/northstar/core/filter/ValueComparision.ts
@@ -0,0 +1,74 @@
+import { Predicate } from '../../model/idea/idea'
+import { Utils } from '../../utils/Utils'
+import { AttributeModel } from '../attribute/AttributeModel';
+
+export class ValueComparison {
+
+ public attributeModel: AttributeModel;
+ public Value: any;
+ public Predicate: Predicate;
+
+ public constructor(attributeModel: AttributeModel, predicate: Predicate, value: any) {
+ this.attributeModel = attributeModel;
+ this.Value = value;
+ this.Predicate = predicate;
+ }
+
+ public Equals(other: Object): boolean {
+ if (!Utils.EqualityHelper(this, other))
+ return false;
+ if (this.Predicate !== (other as ValueComparison).Predicate)
+ return false;
+ let isComplex = (typeof this.Value === "object");
+ if (!isComplex && this.Value != (other as ValueComparison).Value)
+ return false;
+ if (isComplex && !this.Value.Equals((other as ValueComparison).Value))
+ return false;
+ return true;
+ }
+
+ public ToPythonString(): string {
+ var op = "";
+ switch (this.Predicate) {
+ case Predicate.EQUALS:
+ op = "==";
+ break;
+ case Predicate.GREATER_THAN:
+ op = ">";
+ break;
+ case Predicate.GREATER_THAN_EQUAL:
+ op = ">=";
+ break;
+ case Predicate.LESS_THAN:
+ op = "<";
+ break;
+ case Predicate.LESS_THAN_EQUAL:
+ op = "<=";
+ break;
+ default:
+ op = "==";
+ break;
+ }
+
+ var val = this.Value.toString();
+ if (typeof this.Value === 'string' || this.Value instanceof String) {
+ val = "\"" + val + "\"";
+ }
+ var ret = " ";
+ var rawName = this.attributeModel.CodeName;
+ switch (this.Predicate) {
+ case Predicate.STARTS_WITH:
+ ret += rawName + " != null && " + rawName + ".StartsWith(" + val + ") ";
+ return ret;
+ case Predicate.ENDS_WITH:
+ ret += rawName + " != null && " + rawName + ".EndsWith(" + val + ") ";
+ return ret;
+ case Predicate.CONTAINS:
+ ret += rawName + " != null && " + rawName + ".Contains(" + val + ") ";
+ return ret;
+ default:
+ ret += rawName + " " + op + " " + val + " ";
+ return ret;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/model/ModelExtensions.ts b/src/client/northstar/model/ModelExtensions.ts
new file mode 100644
index 000000000..9fcba7f1c
--- /dev/null
+++ b/src/client/northstar/model/ModelExtensions.ts
@@ -0,0 +1,48 @@
+import { AttributeParameters, Brush, MarginAggregateParameters, SingleDimensionAggregateParameters, Solution } from '../model/idea/idea'
+import { Utils } from '../utils/Utils'
+
+import { FilterModel } from '../core/filter/FilterModel'
+
+(SingleDimensionAggregateParameters as any).prototype["Equals"] = function (other: Object) {
+ if (!Utils.EqualityHelper(this, other)) return false;
+ if (!Utils.EqualityHelper((this as SingleDimensionAggregateParameters).attributeParameters!,
+ (other as SingleDimensionAggregateParameters).attributeParameters!)) return false;
+ if (!((this as SingleDimensionAggregateParameters).attributeParameters! as any)["Equals"]((other as SingleDimensionAggregateParameters).attributeParameters)) return false;
+ return true;
+}
+
+{
+ (AttributeParameters as any).prototype["Equals"] = function (other: AttributeParameters) {
+ return (<any>this).constructor.name === (<any>other).constructor.name &&
+ this.rawName === other.rawName;
+ }
+}
+
+{
+ (Solution as any).prototype["Equals"] = function (other: Object) {
+ if (!Utils.EqualityHelper(this, other)) return false;
+ if ((this as Solution).solutionId !== (other as Solution).solutionId) return false;
+ return true;
+ }
+}
+
+{
+ (MarginAggregateParameters as any).prototype["Equals"] = function (other: Object) {
+ if (!Utils.EqualityHelper(this, other)) return false;
+ if (!Utils.EqualityHelper((this as SingleDimensionAggregateParameters).attributeParameters!,
+ (other as SingleDimensionAggregateParameters).attributeParameters!)) return false;
+ if (!((this as SingleDimensionAggregateParameters).attributeParameters! as any)["Equals"]((other as SingleDimensionAggregateParameters).attributeParameters!)) return false;
+
+ if ((this as MarginAggregateParameters).aggregateFunction !== (other as MarginAggregateParameters).aggregateFunction) return false;
+ return true;
+ }
+}
+
+{
+ (Brush as any).prototype["Equals"] = function (other: Object) {
+ if (!Utils.EqualityHelper(this, other)) return false;
+ if ((this as Brush).brushEnum !== (other as Brush).brushEnum) return false;
+ if ((this as Brush).brushIndex !== (other as Brush).brushIndex) return false;
+ return true;
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/model/ModelHelpers.ts b/src/client/northstar/model/ModelHelpers.ts
new file mode 100644
index 000000000..e1241b3ef
--- /dev/null
+++ b/src/client/northstar/model/ModelHelpers.ts
@@ -0,0 +1,215 @@
+
+import { action } from "mobx";
+import { AggregateFunction, AggregateKey, AggregateParameters, AttributeColumnParameters, AttributeParameters, AverageAggregateParameters, Bin, BinningParameters, Brush, BrushEnum, CountAggregateParameters, DataType, EquiWidthBinningParameters, HistogramResult, MarginAggregateParameters, SingleBinBinningParameters, SingleDimensionAggregateParameters, SumAggregateParameters, AggregateBinRange, NominalBinRange, AlphabeticBinRange, Predicate, Schema, Attribute, AttributeGroup, Exception, AttributeBackendParameters, AttributeCodeParameters } from '../model/idea/idea';
+import { ValueComparison } from "../core/filter/ValueComparision";
+import { ArrayUtil } from "../utils/ArrayUtil";
+import { AttributeModel, ColumnAttributeModel, BackendAttributeModel, CodeAttributeModel } from "../core/attribute/AttributeModel";
+import { FilterModel } from "../core/filter/FilterModel";
+import { AlphabeticVisualBinRange } from "./binRanges/AlphabeticVisualBinRange";
+import { NominalVisualBinRange } from "./binRanges/NominalVisualBinRange";
+import { VisualBinRangeHelper } from "./binRanges/VisualBinRangeHelper";
+import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
+import { Main } from "../../views/Main";
+
+export class ModelHelpers {
+
+ public static CreateAggregateKey(atm: AttributeTransformationModel, histogramResult: HistogramResult,
+ brushIndex: number, aggParameters?: SingleDimensionAggregateParameters): AggregateKey {
+ {
+ if (aggParameters == undefined) {
+ aggParameters = ModelHelpers.GetAggregateParameter(atm);
+ }
+ else {
+ aggParameters.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel);
+ }
+ return new AggregateKey(
+ {
+ aggregateParameterIndex: ModelHelpers.GetAggregateParametersIndex(histogramResult, aggParameters),
+ brushIndex: brushIndex
+ });
+ }
+ }
+
+ public static GetAggregateParametersIndex(histogramResult: HistogramResult, aggParameters?: AggregateParameters): number {
+ return ArrayUtil.IndexOfWithEqual(histogramResult.aggregateParameters!, aggParameters);
+ }
+
+ public static GetAggregateParameter(atm: AttributeTransformationModel): AggregateParameters | undefined {
+ var aggParam: AggregateParameters | undefined;
+ if (atm.AggregateFunction === AggregateFunction.Avg) {
+ var avg = new AverageAggregateParameters();
+ avg.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel);
+ avg.distinctAttributeParameters = Main.Instance.ActiveSchema!.distinctAttributeParameters;
+ aggParam = avg;
+ }
+ else if (atm.AggregateFunction === AggregateFunction.Count) {
+ var cnt = new CountAggregateParameters();
+ cnt.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel);
+ cnt.distinctAttributeParameters = Main.Instance.ActiveSchema!.distinctAttributeParameters;
+ aggParam = cnt;
+ }
+ else if (atm.AggregateFunction === AggregateFunction.Sum) {
+ var sum = new SumAggregateParameters();
+ sum.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel);
+ sum.distinctAttributeParameters = Main.Instance.ActiveSchema!.distinctAttributeParameters;
+ aggParam = sum;
+ }
+ return aggParam;
+ }
+
+ public static GetAggregateParametersWithMargins(atms: Array<AttributeTransformationModel>): Array<AggregateParameters> {
+ var aggregateParameters = new Array<AggregateParameters>();
+ atms.forEach(agg => {
+ var aggParams = ModelHelpers.GetAggregateParameter(agg);
+ if (aggParams) {
+ aggregateParameters.push(aggParams);
+
+ var margin = new MarginAggregateParameters();
+ margin.attributeParameters = ModelHelpers.GetAttributeParameters(agg.AttributeModel);
+ margin.distinctAttributeParameters = Main.Instance.ActiveSchema!.distinctAttributeParameters;
+ margin.aggregateFunction = agg.AggregateFunction;
+ aggregateParameters.push(margin);
+ }
+ });
+
+ return aggregateParameters;
+ }
+
+ public static GetBinningParameters(attr: AttributeTransformationModel, nrOfBins: number, minvalue?: number, maxvalue?: number): BinningParameters {
+ if (attr.AggregateFunction === AggregateFunction.None) {
+ return new EquiWidthBinningParameters(
+ {
+ attributeParameters: ModelHelpers.GetAttributeParameters(attr.AttributeModel),
+ requestedNrOfBins: nrOfBins,
+ minValue: minvalue,
+ maxValue: maxvalue
+ });
+ }
+ else {
+ return new SingleBinBinningParameters(
+ {
+ attributeParameters: ModelHelpers.GetAttributeParameters(attr.AttributeModel)
+ });
+ }
+ }
+
+ public static GetAttributeParametersFromAttributeModel(am: AttributeModel): AttributeParameters {
+ if (am instanceof ColumnAttributeModel) {
+ return new AttributeColumnParameters(
+ {
+ rawName: am.CodeName,
+ visualizationHints: am.VisualizationHints
+ });
+ }
+ else if (am instanceof BackendAttributeModel) {
+ return new AttributeBackendParameters(
+ {
+ rawName: am.CodeName,
+ visualizationHints: am.VisualizationHints,
+ id: (am as BackendAttributeModel).Id
+ });
+ }
+ else if (am instanceof CodeAttributeModel) {
+ return new AttributeCodeParameters(
+ {
+ rawName: am.CodeName,
+ visualizationHints: am.VisualizationHints,
+ code: (am as CodeAttributeModel).Code
+ });
+ }
+ else {
+ throw new Exception()
+ }
+ }
+
+ public static GetAttributeParameters(am: AttributeModel): AttributeParameters {
+ return this.GetAttributeParametersFromAttributeModel(am);
+ }
+
+ public static OverlapBrushIndex(histogramResult: HistogramResult): number {
+ var brush = ArrayUtil.First(histogramResult.brushes!, (b: any) => b.brushEnum === BrushEnum.Overlap);
+ return ModelHelpers.GetBrushIndex(histogramResult, brush);
+ }
+
+ public static AllBrushIndex(histogramResult: HistogramResult): number {
+ var brush = ArrayUtil.First(histogramResult.brushes!, (b: any) => b.brushEnum === BrushEnum.All);
+ return ModelHelpers.GetBrushIndex(histogramResult, brush);
+ }
+
+ public static RestBrushIndex(histogramResult: HistogramResult): number {
+ var brush = ArrayUtil.First(histogramResult.brushes!, (b: Brush) => b.brushEnum === BrushEnum.Rest);
+ return ModelHelpers.GetBrushIndex(histogramResult, brush);
+ }
+
+ public static GetBrushIndex(histogramResult: HistogramResult, brush: Brush): number {
+ return ArrayUtil.IndexOfWithEqual(histogramResult.brushes!, brush);
+ }
+
+ public static GetAggregateResult(bin: Bin, aggregateKey: AggregateKey) {
+ if (aggregateKey.aggregateParameterIndex == -1 || aggregateKey.brushIndex == -1) {
+ return null;
+ }
+ return bin.aggregateResults![aggregateKey.aggregateParameterIndex! * bin.ySize! + aggregateKey.brushIndex!];
+ }
+
+ @action
+ public static PossibleAggegationFunctions(atm: AttributeTransformationModel): Array<AggregateFunction> {
+ var ret = new Array<AggregateFunction>();
+ ret.push(AggregateFunction.None);
+ ret.push(AggregateFunction.Count);
+ if (atm.AttributeModel.DataType == DataType.Float ||
+ atm.AttributeModel.DataType == DataType.Double ||
+ atm.AttributeModel.DataType == DataType.Int) {
+ ret.push(AggregateFunction.Avg);
+ ret.push(AggregateFunction.Sum);
+ }
+ return ret;
+ }
+
+ public static GetBinFilterModel(
+ bin: Bin, brushIndex: number, histogramResult: HistogramResult,
+ xAom: AttributeTransformationModel, yAom: AttributeTransformationModel): FilterModel {
+ var dimensions: Array<AttributeTransformationModel> = [xAom, yAom];
+ var filterModel = new FilterModel();
+
+ for (var i = 0; i < histogramResult.binRanges!.length; i++) {
+ if (!(histogramResult.binRanges![i] instanceof AggregateBinRange)) {
+ var binRange = VisualBinRangeHelper.GetNonAggregateVisualBinRange(histogramResult.binRanges![i]);
+ var dataFrom = binRange.GetValueFromIndex(bin.binIndex!.indices![i]);
+ var dataTo = binRange.AddStep(dataFrom);
+
+ if (binRange instanceof NominalVisualBinRange) {
+ var tt = binRange.GetLabel(dataFrom);
+ filterModel.ValueComparisons.push(new ValueComparison(dimensions[i].AttributeModel, Predicate.EQUALS, tt));
+ }
+ else if (binRange instanceof AlphabeticVisualBinRange) {
+ filterModel.ValueComparisons.push(new ValueComparison(dimensions[i].AttributeModel, Predicate.STARTS_WITH,
+ binRange.GetLabel(dataFrom)));
+ }
+ else {
+ filterModel.ValueComparisons.push(new ValueComparison(dimensions[i].AttributeModel, Predicate.GREATER_THAN_EQUAL, dataFrom));
+ filterModel.ValueComparisons.push(new ValueComparison(dimensions[i].AttributeModel, Predicate.LESS_THAN, dataTo));
+ }
+ }
+ }
+
+ return filterModel;
+ }
+
+ public GetAllAttributes(schema: Schema) {
+ if (!schema || !schema.rootAttributeGroup) {
+ return [];
+ }
+ const recurs = (attrs: Attribute[], g: AttributeGroup) => {
+ if (g.attributes) {
+ attrs.push.apply(attrs, g.attributes);
+ if (g.attributeGroups) {
+ g.attributeGroups.forEach(ng => recurs(attrs, ng));
+ }
+ }
+ };
+ const allAttributes: Attribute[] = new Array<Attribute>();
+ recurs(allAttributes, schema.rootAttributeGroup);
+ return allAttributes;
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/model/binRanges/AlphabeticVisualBinRange.ts b/src/client/northstar/model/binRanges/AlphabeticVisualBinRange.ts
new file mode 100644
index 000000000..995bf4e0b
--- /dev/null
+++ b/src/client/northstar/model/binRanges/AlphabeticVisualBinRange.ts
@@ -0,0 +1,52 @@
+import { AlphabeticBinRange, BinLabel } from '../../model/idea/idea'
+import { VisualBinRange } from './VisualBinRange'
+
+export class AlphabeticVisualBinRange extends VisualBinRange {
+ public DataBinRange: AlphabeticBinRange;
+
+ constructor(dataBinRange: AlphabeticBinRange) {
+ super();
+ this.DataBinRange = dataBinRange;
+ }
+
+ public AddStep(value: number): number {
+ return value + 1;
+ }
+
+ public GetValueFromIndex(index: number): number {
+ return index;
+ }
+
+ public GetBins(): number[] {
+ var bins = new Array<number>();
+ var idx = 0;
+ for (var key in this.DataBinRange.labelsValue) {
+ if (this.DataBinRange.labelsValue.hasOwnProperty(key)) {
+ bins.push(idx);
+ idx++;
+ }
+ }
+ return bins;
+ }
+
+ public GetLabel(value: number): string {
+ return this.DataBinRange.prefix + this.DataBinRange.valuesLabel![value];
+ }
+
+ public GetLabels(): Array<BinLabel> {
+ var labels = new Array<BinLabel>();
+ var count = 0;
+ for (var key in this.DataBinRange.valuesLabel) {
+ if (this.DataBinRange.valuesLabel.hasOwnProperty(key)) {
+ var value = this.DataBinRange.valuesLabel[key];
+ labels.push(new BinLabel({
+ value: parseFloat(key),
+ minValue: count++,
+ maxValue: count,
+ label: this.DataBinRange.prefix + value
+ }));
+ }
+ }
+ return labels;
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/model/binRanges/DateTimeVisualBinRange.ts b/src/client/northstar/model/binRanges/DateTimeVisualBinRange.ts
new file mode 100644
index 000000000..9313fb1a7
--- /dev/null
+++ b/src/client/northstar/model/binRanges/DateTimeVisualBinRange.ts
@@ -0,0 +1,105 @@
+import { DateTimeBinRange, DateTimeStep, DateTimeStepGranularity } from '../idea/idea'
+import { VisualBinRange } from './VisualBinRange'
+
+export class DateTimeVisualBinRange extends VisualBinRange {
+ public DataBinRange: DateTimeBinRange;
+
+ constructor(dataBinRange: DateTimeBinRange) {
+ super();
+ this.DataBinRange = dataBinRange;
+ }
+
+ public AddStep(value: number): number {
+ return DateTimeVisualBinRange.AddToDateTimeTicks(value, this.DataBinRange.step!);
+ }
+
+ public GetValueFromIndex(index: number): number {
+ var v = this.DataBinRange.minValue!;
+ for (var i = 0; i < index; i++) {
+ v = this.AddStep(v);
+ }
+ return v;
+ }
+
+ public GetBins(): number[] {
+ var bins = new Array<number>();
+ for (var v: number = this.DataBinRange.minValue!;
+ v < this.DataBinRange.maxValue!;
+ v = DateTimeVisualBinRange.AddToDateTimeTicks(v, this.DataBinRange.step!)) {
+ bins.push(v);
+ }
+ return bins;
+ }
+
+ private pad(n: number, size: number) {
+ var sign = n < 0 ? '-' : '';
+ return sign + new Array(size).concat([Math.abs(n)]).join('0').slice(-size);
+ }
+
+
+ public GetLabel(value: number): string {
+ var dt = DateTimeVisualBinRange.TicksToDate(value);
+ if (this.DataBinRange.step!.dateTimeStepGranularity == DateTimeStepGranularity.Second ||
+ this.DataBinRange.step!.dateTimeStepGranularity == DateTimeStepGranularity.Minute) {
+ return ("" + this.pad(dt.getMinutes(), 2) + ":" + this.pad(dt.getSeconds(), 2));
+ //return dt.ToString("mm:ss");
+ }
+ else if (this.DataBinRange.step!.dateTimeStepGranularity == DateTimeStepGranularity.Hour) {
+ return (this.pad(dt.getHours(), 2) + ":" + this.pad(dt.getMinutes(), 2));
+ //return dt.ToString("HH:mm");
+ }
+ else if (this.DataBinRange.step!.dateTimeStepGranularity == DateTimeStepGranularity.Day) {
+ return ((dt.getMonth() + 1) + "/" + dt.getDate() + "/" + dt.getFullYear());
+ //return dt.ToString("MM/dd/yyyy");
+ }
+ else if (this.DataBinRange.step!.dateTimeStepGranularity == DateTimeStepGranularity.Month) {
+ //return dt.ToString("MM/yyyy");
+ return ((dt.getMonth() + 1) + "/" + dt.getFullYear());
+ }
+ else if (this.DataBinRange.step!.dateTimeStepGranularity == DateTimeStepGranularity.Year) {
+ return "" + dt.getFullYear();
+ }
+ return "n/a";
+ }
+
+ public static TicksToDate(ticks: number): Date {
+ var dd = new Date((ticks - 621355968000000000) / 10000);
+ dd.setMinutes(dd.getMinutes() + dd.getTimezoneOffset());
+ return dd;
+ }
+
+
+ public static DateToTicks(date: Date): number {
+ var copiedDate = new Date(date.getTime());
+ copiedDate.setMinutes(copiedDate.getMinutes() - copiedDate.getTimezoneOffset());
+ var t = copiedDate.getTime() * 10000 + 621355968000000000;
+ /*var dd = new Date((ticks - 621355968000000000) / 10000);
+ dd.setMinutes(dd.getMinutes() + dd.getTimezoneOffset());
+ return dd;*/
+ return t;
+ }
+
+ public static AddToDateTimeTicks(ticks: number, dateTimeStep: DateTimeStep): number {
+ var copiedDate = DateTimeVisualBinRange.TicksToDate(ticks);
+ var returnDate: Date = new Date(Date.now());
+ if (dateTimeStep.dateTimeStepGranularity == DateTimeStepGranularity.Second) {
+ returnDate = new Date(copiedDate.setSeconds(copiedDate.getSeconds() + dateTimeStep.dateTimeStepValue!));
+ }
+ else if (dateTimeStep.dateTimeStepGranularity == DateTimeStepGranularity.Minute) {
+ returnDate = new Date(copiedDate.setMinutes(copiedDate.getMinutes() + dateTimeStep.dateTimeStepValue!));
+ }
+ else if (dateTimeStep.dateTimeStepGranularity == DateTimeStepGranularity.Hour) {
+ returnDate = new Date(copiedDate.setHours(copiedDate.getHours() + dateTimeStep.dateTimeStepValue!));
+ }
+ else if (dateTimeStep.dateTimeStepGranularity == DateTimeStepGranularity.Day) {
+ returnDate = new Date(copiedDate.setDate(copiedDate.getDate() + dateTimeStep.dateTimeStepValue!));
+ }
+ else if (dateTimeStep.dateTimeStepGranularity == DateTimeStepGranularity.Month) {
+ returnDate = new Date(copiedDate.setMonth(copiedDate.getMonth() + dateTimeStep.dateTimeStepValue!));
+ }
+ else if (dateTimeStep.dateTimeStepGranularity == DateTimeStepGranularity.Year) {
+ returnDate = new Date(copiedDate.setFullYear(copiedDate.getFullYear() + dateTimeStep.dateTimeStepValue!));
+ }
+ return DateTimeVisualBinRange.DateToTicks(returnDate);
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/model/binRanges/NominalVisualBinRange.ts b/src/client/northstar/model/binRanges/NominalVisualBinRange.ts
new file mode 100644
index 000000000..407ff3ea6
--- /dev/null
+++ b/src/client/northstar/model/binRanges/NominalVisualBinRange.ts
@@ -0,0 +1,52 @@
+import { NominalBinRange, BinLabel } from '../../model/idea/idea'
+import { VisualBinRange } from './VisualBinRange'
+
+export class NominalVisualBinRange extends VisualBinRange {
+ public DataBinRange: NominalBinRange;
+
+ constructor(dataBinRange: NominalBinRange) {
+ super();
+ this.DataBinRange = dataBinRange;
+ }
+
+ public AddStep(value: number): number {
+ return value + 1;
+ }
+
+ public GetValueFromIndex(index: number): number {
+ return index;
+ }
+
+ public GetBins(): number[] {
+ var bins = new Array<number>();
+ var idx = 0;
+ for (var key in this.DataBinRange.labelsValue) {
+ if (this.DataBinRange.labelsValue.hasOwnProperty(key)) {
+ bins.push(idx);
+ idx++;
+ }
+ }
+ return bins;
+ }
+
+ public GetLabel(value: number): string {
+ return this.DataBinRange.valuesLabel![value];
+ }
+
+ public GetLabels(): Array<BinLabel> {
+ var labels = new Array<BinLabel>();
+ var count = 0;
+ for (var key in this.DataBinRange.valuesLabel) {
+ if (this.DataBinRange.valuesLabel.hasOwnProperty(key)) {
+ var value = this.DataBinRange.valuesLabel[key];
+ labels.push(new BinLabel({
+ value: parseFloat(key),
+ minValue: count++,
+ maxValue: count,
+ label: value
+ }));
+ }
+ }
+ return labels;
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/model/binRanges/QuantitativeVisualBinRange.ts b/src/client/northstar/model/binRanges/QuantitativeVisualBinRange.ts
new file mode 100644
index 000000000..80886416b
--- /dev/null
+++ b/src/client/northstar/model/binRanges/QuantitativeVisualBinRange.ts
@@ -0,0 +1,90 @@
+import { QuantitativeBinRange } from '../idea/idea'
+import { VisualBinRange } from './VisualBinRange';
+import { format } from "d3-format";
+
+export class QuantitativeVisualBinRange extends VisualBinRange {
+
+ public DataBinRange: QuantitativeBinRange;
+
+ constructor(dataBinRange: QuantitativeBinRange) {
+ super();
+ this.DataBinRange = dataBinRange;
+ }
+
+ public AddStep(value: number): number {
+ return value + this.DataBinRange.step!;
+ }
+
+ public GetValueFromIndex(index: number): number {
+ return this.DataBinRange.minValue! + (index * this.DataBinRange.step!);
+ }
+
+ public GetLabel(value: number): string {
+ return QuantitativeVisualBinRange.NumberFormatter(value);
+ }
+
+ public static NumberFormatter(val: number): string {
+ if (val === 0) {
+ return "0";
+ }
+ if (val < 1) {
+ /*if (val < Math.abs(0.001)) {
+ return val.toExponential(2);
+ }*/
+ return format(".3")(val);
+ }
+ return format("~s")(val);
+ }
+
+ public GetBins(): number[] {
+ let bins = new Array<number>();
+
+ for (let v: number = this.DataBinRange.minValue!; v < this.DataBinRange.maxValue!; v += this.DataBinRange.step!) {
+ bins.push(v);
+ }
+ return bins;
+ }
+
+ public static Initialize(dataMinValue: number, dataMaxValue: number, targetBinNumber: number, isIntegerRange: boolean): QuantitativeVisualBinRange {
+ let extent = QuantitativeVisualBinRange.getExtent(dataMinValue, dataMaxValue, targetBinNumber, isIntegerRange);
+ let dataBinRange = new QuantitativeBinRange();
+ dataBinRange.minValue = extent[0];
+ dataBinRange.maxValue = extent[1];
+ dataBinRange.step = extent[2];
+
+ return new QuantitativeVisualBinRange(dataBinRange);
+ }
+
+ private static getExtent(dataMin: number, dataMax: number, m: number, isIntegerRange: boolean): number[] {
+ if (dataMin === dataMax) {
+ // dataMin -= 0.1;
+ dataMax += 0.1;
+ }
+ let span = dataMax - dataMin;
+
+ let step = Math.pow(10, Math.floor(Math.log10(span / m)));
+ let err = m / span * step;
+
+ if (err <= .15) {
+ step *= 10;
+ }
+ else if (err <= .35) {
+ step *= 5;
+ }
+ else if (err <= .75) {
+ step *= 2;
+ }
+
+ if (isIntegerRange) {
+ step = Math.ceil(step);
+ }
+ let ret: number[] = new Array<number>(3);
+ let minDivStep = Math.floor(dataMin / step);
+ let maxDivStep = Math.floor(dataMax / step);
+ ret[0] = minDivStep * step; // Math.floor(Math.Round(dataMin, 8)/step)*step;
+ ret[1] = maxDivStep * step + step; // Math.floor(Math.Round(dataMax, 8)/step)*step + step;
+ ret[2] = step;
+
+ return ret;
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/model/binRanges/VisualBinRange.ts b/src/client/northstar/model/binRanges/VisualBinRange.ts
new file mode 100644
index 000000000..f53008f9a
--- /dev/null
+++ b/src/client/northstar/model/binRanges/VisualBinRange.ts
@@ -0,0 +1,36 @@
+import { BinLabel } from '../../model/idea/idea'
+
+export abstract class VisualBinRange {
+
+ constructor() {
+
+ }
+
+ public abstract AddStep(value: number): number;
+
+ public abstract GetValueFromIndex(index: number): number;
+
+ public abstract GetBins(): Array<number>;
+
+ public GetLabel(value: number): string {
+ return value.toString();
+ }
+
+ public GetLabels(): Array<BinLabel> {
+ var labels = new Array<BinLabel>();
+ var bins = this.GetBins();
+ bins.forEach(b => {
+ labels.push(new BinLabel({
+ value: b,
+ minValue: b,
+ maxValue: this.AddStep(b),
+ label: this.GetLabel(b)
+ }));
+ });
+ return labels;
+ }
+}
+
+export enum ChartType {
+ HorizontalBar = 0, VerticalBar = 1, HeatMap = 2, SinglePoint = 3
+} \ No newline at end of file
diff --git a/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts b/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts
new file mode 100644
index 000000000..9eae39800
--- /dev/null
+++ b/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts
@@ -0,0 +1,71 @@
+import { BinRange, NominalBinRange, QuantitativeBinRange, Exception, AlphabeticBinRange, DateTimeBinRange, AggregateBinRange, DoubleValueAggregateResult, HistogramResult } from "../idea/idea";
+import { VisualBinRange, ChartType } from "./VisualBinRange";
+import { NominalVisualBinRange } from "./NominalVisualBinRange";
+import { QuantitativeVisualBinRange } from "./QuantitativeVisualBinRange";
+import { AlphabeticVisualBinRange } from "./AlphabeticVisualBinRange";
+import { DateTimeVisualBinRange } from "./DateTimeVisualBinRange";
+import { Settings } from "../../manager/Gateway";
+import { ModelHelpers } from "../ModelHelpers";
+import { AttributeTransformationModel } from "../../core/attribute/AttributeTransformationModel";
+
+export const SETTINGS_X_BINS = 15;
+export const SETTINGS_Y_BINS = 15;
+export const SETTINGS_SAMPLE_SIZE = 100000;
+
+export class VisualBinRangeHelper {
+
+ public static GetNonAggregateVisualBinRange(dataBinRange: BinRange): VisualBinRange {
+ if (dataBinRange instanceof NominalBinRange) {
+ return new NominalVisualBinRange(dataBinRange as NominalBinRange);
+ }
+ else if (dataBinRange instanceof QuantitativeBinRange) {
+ return new QuantitativeVisualBinRange(dataBinRange as QuantitativeBinRange);
+ }
+ else if (dataBinRange instanceof AlphabeticBinRange) {
+ return new AlphabeticVisualBinRange(dataBinRange as AlphabeticBinRange);
+ }
+ else if (dataBinRange instanceof DateTimeBinRange) {
+ return new DateTimeVisualBinRange(dataBinRange as DateTimeBinRange);
+ }
+ throw new Exception()
+ }
+
+ public static GetVisualBinRange(dataBinRange: BinRange, histoResult: HistogramResult, attr: AttributeTransformationModel, chartType: ChartType): VisualBinRange {
+
+ if (!(dataBinRange instanceof AggregateBinRange)) {
+ return VisualBinRangeHelper.GetNonAggregateVisualBinRange(dataBinRange);
+ }
+ else {
+ var aggregateKey = ModelHelpers.CreateAggregateKey(attr, histoResult, ModelHelpers.AllBrushIndex(histoResult));
+ var minValue = Number.MAX_VALUE;
+ var maxValue = Number.MIN_VALUE;
+ for (var b = 0; b < histoResult.brushes!.length; b++) {
+ var brush = histoResult.brushes![b];
+ aggregateKey.brushIndex = brush.brushIndex;
+ for (var key in histoResult.bins) {
+ if (histoResult.bins.hasOwnProperty(key)) {
+ var bin = histoResult.bins[key];
+ var res = <DoubleValueAggregateResult>ModelHelpers.GetAggregateResult(bin, aggregateKey);
+ if (res && res.hasResult && res.result) {
+ minValue = Math.min(minValue, res.result);
+ maxValue = Math.max(maxValue, res.result);
+ }
+ }
+ }
+ };
+
+ let visualBinRange = QuantitativeVisualBinRange.Initialize(minValue, maxValue, 10, false);
+
+ if (chartType == ChartType.HorizontalBar || chartType == ChartType.VerticalBar) {
+ visualBinRange = QuantitativeVisualBinRange.Initialize(Math.min(0, minValue),
+ Math.max(0, (visualBinRange as QuantitativeVisualBinRange).DataBinRange.maxValue!),
+ SETTINGS_X_BINS, false);
+ }
+ else if (chartType == ChartType.SinglePoint) {
+ visualBinRange = QuantitativeVisualBinRange.Initialize(Math.min(0, minValue), Math.max(0, maxValue),
+ SETTINGS_X_BINS, false);
+ }
+ return visualBinRange;
+ }
+ }
+}
diff --git a/src/client/northstar/operations/BaseOperation.ts b/src/client/northstar/operations/BaseOperation.ts
new file mode 100644
index 000000000..4c0303a48
--- /dev/null
+++ b/src/client/northstar/operations/BaseOperation.ts
@@ -0,0 +1,174 @@
+import { FilterModel } from '../core/filter/FilterModel'
+import { ErrorResult, Exception, OperationParameters, OperationReference, Result, ResultParameters } from '../model/idea/idea';
+import { action, computed, observable } from "mobx";
+import { Gateway } from '../manager/Gateway';
+
+export abstract class BaseOperation {
+ private _interactionTimeoutId: number = 0;
+ private static _currentOperations: Map<number, PollPromise> = new Map<number, PollPromise>();
+ //public InteractionTimeout: EventDelegate<InteractionTimeoutEventArgs> = new EventDelegate<InteractionTimeoutEventArgs>();
+
+ @observable public Error: string = "";
+ @observable public OverridingFilters: FilterModel[] = [];
+ @observable public Result: Result | undefined;
+ @observable public ComputationStarted: boolean = false;
+ public OperationReference: OperationReference | undefined = undefined;
+
+ private static _nextId = 0;
+ public RequestSalt: string = "";
+ public Id: number;
+
+ constructor() {
+ this.Id = BaseOperation._nextId++;
+ }
+
+ @computed
+ public get FilterString(): string {
+
+ // if (this.OverridingFilters.length > 0) {
+ // return "(" + this.OverridingFilters.filter(fm => fm != null).map(fm => fm.ToPythonString()).join(" || ") + ")";
+ // }
+ // let rdg = MainManager.Instance.MainViewModel.FilterReverseDependencyGraph;
+ // let sliceModel = this.TypedViewModel.IncomingSliceModel;
+ // if (sliceModel != null && sliceModel.Source != null && instanceOfIBaseFilterProvider(sliceModel.Source) && rdg.has(sliceModel.Source)) {
+ // let filterModels = sliceModel.Source.FilterModels.map(f => f);
+ // return FilterModel.GetFilterModelsRecursive(rdg.get(sliceModel.Source), new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), filterModels, false);
+ // }
+
+ // if (rdg.has(this.TypedViewModel)) {
+ // let filterModels = [];
+ // return FilterModel.GetFilterModelsRecursive(rdg.get(this.TypedViewModel), new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), filterModels, true)
+ // }
+ return "";
+ }
+
+
+ @action
+ public SetResult(result: Result): void {
+ this.Result = result;
+ }
+
+ public async Update(): Promise<void> {
+
+ try {
+ if (BaseOperation._currentOperations.has(this.Id)) {
+ BaseOperation._currentOperations.get(this.Id)!.Cancel();
+ if (this.OperationReference) {
+ Gateway.Instance.PauseOperation(this.OperationReference.toJSON());
+ }
+ }
+
+ let operationParameters = this.CreateOperationParameters();
+ this.Result = undefined;
+ this.Error = "";
+ let salt = Math.random().toString();
+ this.RequestSalt = salt;
+
+ if (!operationParameters) {
+ this.ComputationStarted = false;
+ return;
+ }
+
+ this.ComputationStarted = true;
+ //let start = performance.now();
+ let promise = Gateway.Instance.StartOperation(operationParameters.toJSON());
+ promise.catch(err => {
+ action(() => {
+ this.Error = err;
+ console.error(err);
+ });
+ });
+ let operationReference = await promise;
+
+
+ if (operationReference) {
+ this.OperationReference = operationReference;
+
+ let resultParameters = new ResultParameters();
+ resultParameters.operationReference = operationReference;
+
+ let pollPromise = new PollPromise(salt, operationReference);
+ BaseOperation._currentOperations.set(this.Id, pollPromise);
+
+ pollPromise.Start(async () => {
+ let result = await Gateway.Instance.GetResult(resultParameters.toJSON());
+ if (result instanceof ErrorResult) {
+ throw new Error((result as ErrorResult).message);
+ }
+ if (this.RequestSalt == pollPromise.RequestSalt) {
+ if (result && (!this.Result || this.Result.progress != result.progress)) {
+ /*if (operationViewModel.Result !== null && operationViewModel.Result !== undefined) {
+ let t1 = performance.now();
+ console.log((t1 - start) + " milliseconds.");
+ start = performance.now();
+ }*/
+ this.SetResult(result);
+ }
+
+ if (!result || result.progress! < 1) {
+ return true;
+ }
+ }
+ return false;
+ }, 100).catch((err: Error) => action(() => {
+ this.Error = err.message;
+ console.error(err.message);
+ })()
+ );
+ }
+ }
+ catch (err) {
+ console.error(err as Exception);
+ // ErrorDialog.Instance.HandleError(err, operationViewModel);
+ }
+ }
+
+ public CreateOperationParameters(): OperationParameters | undefined { return undefined; }
+
+ private interactionTimeout() {
+ // clearTimeout(this._interactionTimeoutId);
+ // this.InteractionTimeout.Fire(new InteractionTimeoutEventArgs(this.TypedViewModel, InteractionTimeoutType.Timeout));
+ }
+}
+
+export class PollPromise {
+ public RequestSalt: string;
+ public OperationReference: OperationReference;
+
+ private _notCanceled: boolean = true;
+ private _poll: undefined | (() => Promise<boolean>);
+ private _delay: number = 0;
+
+ public constructor(requestKey: string, operationReference: OperationReference) {
+ this.RequestSalt = requestKey;
+ this.OperationReference = operationReference;
+ }
+
+ public Cancel(): void {
+ this._notCanceled = false;
+ }
+
+ public Start(poll: () => Promise<boolean>, delay: number): Promise<void> {
+ this._poll = poll;
+ this._delay = delay;
+ return this.pollRecursive();
+ }
+
+ private pollRecursive = (): Promise<void> => {
+ return Promise.resolve().then(this._poll).then((flag) => {
+ this._notCanceled && flag && new Promise((res) => (setTimeout(res, this._delay)))
+ .then(this.pollRecursive);
+ });
+ }
+}
+
+
+export class InteractionTimeoutEventArgs {
+ constructor(public Sender: object, public Type: InteractionTimeoutType) {
+ }
+}
+
+export enum InteractionTimeoutType {
+ Reset = 0,
+ Timeout = 1
+}
diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts
new file mode 100644
index 000000000..a4f5cac70
--- /dev/null
+++ b/src/client/northstar/operations/HistogramOperation.ts
@@ -0,0 +1,111 @@
+import { reaction, computed, action } from "mobx";
+import { Attribute, DataType, QuantitativeBinRange, HistogramOperationParameters, AggregateParameters, AggregateFunction, AverageAggregateParameters } from "../model/idea/idea";
+import { ArrayUtil } from "../utils/ArrayUtil";
+import { CalculatedAttributeManager } from "../core/attribute/CalculatedAttributeModel";
+import { ModelHelpers } from "../model/ModelHelpers";
+import { SETTINGS_X_BINS, SETTINGS_Y_BINS, SETTINGS_SAMPLE_SIZE } from "../model/binRanges/VisualBinRangeHelper";
+import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
+import { Main } from "../../views/Main";
+import { BaseOperation } from "./BaseOperation";
+
+
+export class HistogramOperation extends BaseOperation {
+ public X: AttributeTransformationModel;
+ public Y: AttributeTransformationModel;
+ public V: AttributeTransformationModel;
+ constructor(x: AttributeTransformationModel, y: AttributeTransformationModel, v: AttributeTransformationModel) {
+ super();
+ this.X = x;
+ this.Y = y;
+ this.V = v;
+ reaction(() => this.createOperationParamsCache, () => this.Update());
+ }
+
+ @computed.struct
+ public get BrushString() {
+ return [];
+ // let brushes = [];
+ // this.TypedViewModel.BrusherModels.map(brushLinkModel => {
+ // if (instanceOfIBaseFilterProvider(brushLinkModel.From) && brushLinkModel.From.FilterModels.some && brushLinkModel.From instanceof BaseOperationViewModel) {
+ // let brushFilterModels = [];
+ // let gnode = MainManager.Instance.MainViewModel.FilterDependencyGraph.has(brushLinkModel.From) ?
+ // MainManager.Instance.MainViewModel.FilterDependencyGraph.get(brushLinkModel.From) :
+ // new GraphNode<BaseOperationViewModel, FilterLinkViewModel>(brushLinkModel.From);
+ // let brush = FilterModel.GetFilterModelsRecursive(gnode, new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), brushFilterModels, false);
+ // brushes.push(brush);
+ // }
+ // });
+ // return brushes;
+ }
+
+
+ @computed.struct
+ public get SelectionString() {
+ return "";
+ // let filterModels = new Array<FilterModel>();
+ // let rdg = MainManager.Instance.MainViewModel.FilterReverseDependencyGraph;
+ // let graphNode: GraphNode<BaseOperationViewModel, FilterLinkViewModel>;
+ // if (rdg.has(this.TypedViewModel)) {
+ // graphNode = MainManager.Instance.MainViewModel.FilterReverseDependencyGraph.get(this.TypedViewModel);
+ // }
+ // else {
+ // graphNode = new GraphNode<BaseOperationViewModel, FilterLinkViewModel>(this.TypedViewModel);
+ // }
+ // return FilterModel.GetFilterModelsRecursive(graphNode, new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), filterModels, false);
+ }
+
+ GetAggregateParameters(histoX: AttributeTransformationModel, histoY: AttributeTransformationModel, histoValue: AttributeTransformationModel) {
+ let allAttributes = new Array<AttributeTransformationModel>(histoX, histoY, histoValue);
+ allAttributes = ArrayUtil.Distinct(allAttributes.filter(a => a.AggregateFunction !== AggregateFunction.None));
+
+ let numericDataTypes = [DataType.Int, DataType.Double, DataType.Float];
+ let perBinAggregateParameters: AggregateParameters[] = ModelHelpers.GetAggregateParametersWithMargins(allAttributes);
+ let globalAggregateParameters: AggregateParameters[] = [];
+ [histoX, histoY]
+ .filter(a => a.AggregateFunction === AggregateFunction.None && ArrayUtil.Contains(numericDataTypes, a.AttributeModel.DataType))
+ .forEach(a => {
+ let avg = new AverageAggregateParameters();
+ avg.attributeParameters = ModelHelpers.GetAttributeParameters(a.AttributeModel);
+ globalAggregateParameters.push(avg);
+ });
+ return [perBinAggregateParameters, globalAggregateParameters];
+ }
+
+ @computed
+ get createOperationParamsCache() {
+ return this.CreateOperationParameters();
+ }
+
+ public QRange: QuantitativeBinRange | undefined;
+
+ public CreateOperationParameters(): HistogramOperationParameters | undefined {
+ if (this.X && this.Y && this.V) {
+ let [perBinAggregateParameters, globalAggregateParameters] = this.GetAggregateParameters(this.X, this.Y, this.V);
+ return new HistogramOperationParameters({
+ enableBrushComputation: true,
+ adapterName: Main.Instance.ActiveSchema!.displayName,
+ filter: this.FilterString,
+ brushes: this.BrushString,
+ binningParameters: [ModelHelpers.GetBinningParameters(this.X, SETTINGS_X_BINS, this.QRange ? this.QRange.minValue : undefined, this.QRange ? this.QRange.maxValue : undefined),
+ ModelHelpers.GetBinningParameters(this.Y, SETTINGS_Y_BINS)],
+ sampleStreamBlockSize: SETTINGS_SAMPLE_SIZE,
+ perBinAggregateParameters: perBinAggregateParameters,
+ globalAggregateParameters: globalAggregateParameters,
+ sortPerBinAggregateParameter: undefined,
+ attributeCalculatedParameters: CalculatedAttributeManager
+ .AllCalculatedAttributes.map(a => ModelHelpers.GetAttributeParametersFromAttributeModel(a)),
+ degreeOfParallism: 1, // Settings.Instance.DegreeOfParallelism,
+ isCachable: false
+ });
+ }
+ }
+
+
+ @action
+ public async Update(): Promise<void> {
+ // this.TypedViewModel.BrushColors = this.TypedViewModel.BrusherModels.map(e => e.Color);
+ return super.Update();
+ }
+}
+
+
diff --git a/src/client/northstar/utils/ArrayUtil.ts b/src/client/northstar/utils/ArrayUtil.ts
new file mode 100644
index 000000000..f35c98317
--- /dev/null
+++ b/src/client/northstar/utils/ArrayUtil.ts
@@ -0,0 +1,90 @@
+import { Exception } from "../model/idea/idea";
+
+export class ArrayUtil {
+
+ public static Contains(arr1: any[], arr2: any): boolean {
+ if (arr1.length === 0) {
+ return false;
+ }
+ let isComplex = typeof arr1[0] === "object";
+ for (let i = 0; i < arr1.length; i++) {
+ if (isComplex && "Equals" in arr1[i]) {
+ if (arr1[i].Equals(arr2)) {
+ return true;
+ }
+ }
+ else {
+ if (arr1[i] === arr2) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+
+ public static RemoveMany(arr: any[], elements: Object[]) {
+ elements.forEach(e => ArrayUtil.Remove(arr, e));
+ }
+
+ public static AddMany(arr: any[], others: Object[]) {
+ arr.push(...others);
+ }
+
+ public static Clear(arr: any[]) {
+ arr.splice(0, arr.length);
+ }
+
+
+ public static Remove(arr: any[], other: Object) {
+ const index = ArrayUtil.IndexOfWithEqual(arr, other);
+ if (index === -1) {
+ return;
+ }
+ arr.splice(index, 1);
+ }
+
+
+ public static First<T>(arr: T[], predicate: (x: any) => boolean): T {
+ let filtered = arr.filter(predicate);
+ if (filtered.length > 0) {
+ return filtered[0];
+ }
+ throw new Exception()
+ }
+
+ public static FirstOrDefault<T>(arr: T[], predicate: (x: any) => boolean): T | undefined {
+ let filtered = arr.filter(predicate);
+ if (filtered.length > 0) {
+ return filtered[0];
+ }
+ return undefined;
+ }
+
+ public static Distinct(arr: any[]): any[] {
+ let ret = [];
+ for (let i = 0; i < arr.length; i++) {
+ if (!ArrayUtil.Contains(ret, arr[i])) {
+ ret.push(arr[i]);
+ }
+ }
+ return ret;
+ }
+
+ public static IndexOfWithEqual(arr: any[], other: any): number {
+ for (let i = 0; i < arr.length; i++) {
+ let isComplex = typeof arr[0] === "object";
+ if (isComplex && "Equals" in arr[i]) {
+ if (arr[i].Equals(other)) {
+ return i;
+ }
+ }
+ else {
+ if (arr[i] === other) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/utils/Extensions.ts b/src/client/northstar/utils/Extensions.ts
new file mode 100644
index 000000000..71bcadf89
--- /dev/null
+++ b/src/client/northstar/utils/Extensions.ts
@@ -0,0 +1,20 @@
+interface String {
+ ReplaceAll(toReplace: string, replacement: string): string;
+}
+
+String.prototype.ReplaceAll = function (toReplace: string, replacement: string): string {
+ var target = this;
+ return target.split(toReplace).join(replacement);
+}
+
+interface Math {
+ log10(val: number): number;
+}
+
+Math.log10 = function (val: number): number {
+ return Math.log(val) / Math.LN10;
+}
+
+declare interface ObjectConstructor {
+ assign(...objects: Object[]): Object;
+}
diff --git a/src/client/northstar/utils/GeometryUtil.ts b/src/client/northstar/utils/GeometryUtil.ts
new file mode 100644
index 000000000..d5f3ba631
--- /dev/null
+++ b/src/client/northstar/utils/GeometryUtil.ts
@@ -0,0 +1,129 @@
+import { MathUtil, PIXIRectangle, PIXIPoint } from "./MathUtil";
+
+
+export class GeometryUtil {
+
+ public static ComputeBoundingBox(points: { x: number, y: number }[], scale = 1, padding: number = 0): PIXIRectangle {
+ let minX: number = Number.MAX_VALUE;
+ let minY: number = Number.MAX_VALUE;
+ let maxX: number = Number.MIN_VALUE;
+ let maxY: number = Number.MIN_VALUE;
+ for (var i = 0; i < points.length; i++) {
+ if (points[i].x < minX)
+ minX = points[i].x;
+ if (points[i].y < minY)
+ minY = points[i].y;
+ if (points[i].x > maxX)
+ maxX = points[i].x;
+ if (points[i].y > maxY)
+ maxY = points[i].y;
+ }
+ return new PIXIRectangle(minX * scale - padding, minY * scale - padding, (maxX - minX) * scale + padding * 2, (maxY - minY) * scale + padding * 2);
+ }
+
+ public static RectangleOverlap(rect1: PIXIRectangle, rect2: PIXIRectangle) {
+ let x_overlap = Math.max(0, Math.min(rect1.right, rect2.right) - Math.max(rect1.left, rect2.left));
+ let y_overlap = Math.max(0, Math.min(rect1.bottom, rect2.bottom) - Math.max(rect1.top, rect2.top));
+ return x_overlap * y_overlap;
+ }
+
+ public static RotatePoints(center: { x: number, y: number }, points: { x: number, y: number }[], angle: number): PIXIPoint[] {
+ const rotate = (cx: number, cy: number, x: number, y: number, angle: number) => {
+ const radians = angle,
+ cos = Math.cos(radians),
+ sin = Math.sin(radians),
+ nx = (cos * (x - cx)) + (sin * (y - cy)) + cx,
+ ny = (cos * (y - cy)) - (sin * (x - cx)) + cy;
+ return new PIXIPoint(nx, ny);
+ }
+ return points.map(p => rotate(center.x, center.y, p.x, p.y, angle));
+ }
+
+ public static LineByLeastSquares(points: { x: number, y: number }[]): PIXIPoint[] {
+ let sum_x: number = 0;
+ let sum_y: number = 0;
+ let sum_xy: number = 0;
+ let sum_xx: number = 0;
+ let count: number = 0;
+
+ let x: number = 0;
+ let y: number = 0;
+
+
+ if (points.length === 0) {
+ return [];
+ }
+
+ for (let v = 0; v < points.length; v++) {
+ x = points[v].x;
+ y = points[v].y;
+ sum_x += x;
+ sum_y += y;
+ sum_xx += x * x;
+ sum_xy += x * y;
+ count++;
+ }
+
+ let m = (count * sum_xy - sum_x * sum_y) / (count * sum_xx - sum_x * sum_x);
+ let b = (sum_y / count) - (m * sum_x) / count;
+ let result: PIXIPoint[] = new Array<PIXIPoint>();
+
+ for (let v = 0; v < points.length; v++) {
+ x = points[v].x;
+ y = x * m + b;
+ result.push(new PIXIPoint(x, y));
+ }
+ return result;
+ }
+
+ // public static PointInsidePolygon(vs:Point[], x:number, y:number):boolean {
+ // // ray-casting algorithm based on
+ // // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
+
+ // var inside = false;
+ // for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
+ // var xi = vs[i].x, yi = vs[i].y;
+ // var xj = vs[j].x, yj = vs[j].y;
+
+ // var intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
+ // if (intersect)
+ // inside = !inside;
+ // }
+
+ // return inside;
+ // };
+
+ public static IntersectLines(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number): boolean {
+ let a1: number, a2: number, b1: number, b2: number, c1: number, c2: number;
+ let r1: number, r2: number, r3: number, r4: number;
+ let denom: number, offset: number, num: number;
+
+ a1 = y2 - y1;
+ b1 = x1 - x2;
+ c1 = (x2 * y1) - (x1 * y2);
+ r3 = ((a1 * x3) + (b1 * y3) + c1);
+ r4 = ((a1 * x4) + (b1 * y4) + c1);
+
+ if ((r3 !== 0) && (r4 !== 0) && (MathUtil.Sign(r3) === MathUtil.Sign(r4))) {
+ return false;
+ }
+
+ a2 = y4 - y3;
+ b2 = x3 - x4;
+ c2 = (x4 * y3) - (x3 * y4);
+
+ r1 = (a2 * x1) + (b2 * y1) + c2;
+ r2 = (a2 * x2) + (b2 * y2) + c2;
+
+ if ((r1 !== 0) && (r2 !== 0) && (MathUtil.Sign(r1) === MathUtil.Sign(r2))) {
+ return false;
+ }
+
+ denom = (a1 * b2) - (a2 * b1);
+
+ if (denom === 0) {
+ return false;
+ }
+ return true;
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/utils/IDisposable.ts b/src/client/northstar/utils/IDisposable.ts
new file mode 100644
index 000000000..5e9843326
--- /dev/null
+++ b/src/client/northstar/utils/IDisposable.ts
@@ -0,0 +1,3 @@
+export interface IDisposable {
+ Dispose(): void;
+} \ No newline at end of file
diff --git a/src/client/northstar/utils/IEquatable.ts b/src/client/northstar/utils/IEquatable.ts
new file mode 100644
index 000000000..2f81c2478
--- /dev/null
+++ b/src/client/northstar/utils/IEquatable.ts
@@ -0,0 +1,3 @@
+export interface IEquatable {
+ Equals(other: Object): boolean;
+} \ No newline at end of file
diff --git a/src/client/northstar/utils/KeyCodes.ts b/src/client/northstar/utils/KeyCodes.ts
new file mode 100644
index 000000000..044569ffe
--- /dev/null
+++ b/src/client/northstar/utils/KeyCodes.ts
@@ -0,0 +1,137 @@
+/**
+ * Class contains the keycodes for keys on your keyboard.
+ *
+ * Useful for auto completion:
+ *
+ * ```
+ * switch (event.key)
+ * {
+ * case KeyCode.UP:
+ * {
+ * // Up key pressed
+ * break;
+ * }
+ * case KeyCode.DOWN:
+ * {
+ * // Down key pressed
+ * break;
+ * }
+ * case KeyCode.LEFT:
+ * {
+ * // Left key pressed
+ * break;
+ * }
+ * case KeyCode.RIGHT:
+ * {
+ * // Right key pressed
+ * break;
+ * }
+ * default:
+ * {
+ * // ignore
+ * break;
+ * }
+ * }
+ * ```
+ */
+export class KeyCodes
+{
+ public static TAB:number = 9;
+ public static CAPS_LOCK:number = 20;
+ public static SHIFT:number = 16;
+ public static CONTROL:number = 17;
+ public static SPACE:number = 32;
+ public static DOWN:number = 40;
+ public static UP:number = 38;
+ public static LEFT:number = 37;
+ public static RIGHT:number = 39;
+ public static ESCAPE:number = 27;
+ public static F1:number = 112;
+ public static F2:number = 113;
+ public static F3:number = 114;
+ public static F4:number = 115;
+ public static F5:number = 116;
+ public static F6:number = 117;
+ public static F7:number = 118;
+ public static F8:number = 119;
+ public static F9:number = 120;
+ public static F10:number = 121;
+ public static F11:number = 122;
+ public static F12:number = 123;
+ public static INSERT:number = 45;
+ public static HOME:number = 36;
+ public static PAGE_UP:number = 33;
+ public static PAGE_DOWN:number = 34;
+ public static DELETE:number = 46;
+ public static END:number = 35;
+ public static ENTER:number = 13;
+ public static BACKSPACE:number = 8;
+ public static NUMPAD_0:number = 96;
+ public static NUMPAD_1:number = 97;
+ public static NUMPAD_2:number = 98;
+ public static NUMPAD_3:number = 99;
+ public static NUMPAD_4:number = 100;
+ public static NUMPAD_5:number = 101;
+ public static NUMPAD_6:number = 102;
+ public static NUMPAD_7:number = 103;
+ public static NUMPAD_8:number = 104;
+ public static NUMPAD_9:number = 105;
+ public static NUMPAD_DIVIDE:number = 111;
+ public static NUMPAD_ADD:number = 107;
+ public static NUMPAD_ENTER:number = 13;
+ public static NUMPAD_DECIMAL:number = 110;
+ public static NUMPAD_SUBTRACT:number = 109;
+ public static NUMPAD_MULTIPLY:number = 106;
+ public static SEMICOLON:number = 186;
+ public static EQUAL:number = 187;
+ public static COMMA:number = 188;
+ public static MINUS:number = 189;
+ public static PERIOD:number = 190;
+ public static SLASH:number = 191;
+ public static BACKQUOTE:number = 192;
+ public static LEFTBRACKET:number = 219;
+ public static BACKSLASH:number = 220;
+ public static RIGHTBRACKET:number = 221;
+ public static QUOTE:number = 222;
+ public static ALT:number = 18;
+ public static COMMAND:number = 15;
+ public static NUMPAD:number = 21;
+ public static A:number = 65;
+ public static B:number = 66;
+ public static C:number = 67;
+ public static D:number = 68;
+ public static E:number = 69;
+ public static F:number = 70;
+ public static G:number = 71;
+ public static H:number = 72;
+ public static I:number = 73;
+ public static J:number = 74;
+ public static K:number = 75;
+ public static L:number = 76;
+ public static M:number = 77;
+ public static N:number = 78;
+ public static O:number = 79;
+ public static P:number = 80;
+ public static Q:number = 81;
+ public static R:number = 82;
+ public static S:number = 83;
+ public static T:number = 84;
+ public static U:number = 85;
+ public static V:number = 86;
+ public static W:number = 87;
+ public static X:number = 88;
+ public static Y:number = 89;
+ public static Z:number = 90;
+ public static NUM_0:number = 48;
+ public static NUM_1:number = 49;
+ public static NUM_2:number = 50;
+ public static NUM_3:number = 51;
+ public static NUM_4:number = 52;
+ public static NUM_5:number = 53;
+ public static NUM_6:number = 54;
+ public static NUM_7:number = 55;
+ public static NUM_8:number = 56;
+ public static NUM_9:number = 57;
+ public static SUBSTRACT:number = 189;
+ public static ADD:number = 187;
+} \ No newline at end of file
diff --git a/src/client/northstar/utils/MathUtil.ts b/src/client/northstar/utils/MathUtil.ts
new file mode 100644
index 000000000..3ed8628ee
--- /dev/null
+++ b/src/client/northstar/utils/MathUtil.ts
@@ -0,0 +1,236 @@
+
+
+export class PIXIPoint {
+ public x: number;
+ public y: number;
+ constructor(x: number, y: number) {
+ this.x = x;
+ this.y = y;
+ }
+}
+export class PIXIRectangle {
+ public x: number;
+ public y: number;
+ public width: number;
+ public height: number
+ public get left() { return this.x }
+ public get right() { return this.x + this.width; }
+ public get top() { return this.y }
+ public get bottom() { return this.top + this.height }
+ constructor(x: number, y: number, width: number, height: number) {
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ }
+}
+
+export class MathUtil {
+
+ public static EPSILON: number = 0.001;
+
+ public static Sign(value: number): number {
+ return value >= 0 ? 1 : -1;
+ }
+
+ public static AddPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint {
+ if (inline) {
+ p1.x += p2.x;
+ p1.y += p2.y;
+ return p1;
+ }
+ else {
+ return new PIXIPoint(p1.x + p2.x, p1.y + p2.y);
+ }
+ }
+
+ public static Perp(p1: PIXIPoint): PIXIPoint {
+ return new PIXIPoint(-p1.y, p1.x);
+ }
+
+ public static DividePoint(p1: PIXIPoint, by: number, inline: boolean = false): PIXIPoint {
+ if (inline) {
+ p1.x /= by;
+ p1.y /= by;
+ return p1;
+ }
+ else {
+ return new PIXIPoint(p1.x / by, p1.y / by);
+ }
+ }
+
+ public static MultiplyConstant(p1: PIXIPoint, by: number, inline: boolean = false) {
+ if (inline) {
+ p1.x *= by;
+ p1.y *= by;
+ return p1;
+ }
+ else {
+ return new PIXIPoint(p1.x * by, p1.y * by);
+ }
+ }
+
+ public static SubtractPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint {
+ if (inline) {
+ p1.x -= p2.x;
+ p1.y -= p2.y;
+ return p1;
+ }
+ else {
+ return new PIXIPoint(p1.x - p2.x, p1.y - p2.y);
+ }
+ }
+
+ public static Area(rect: PIXIRectangle): number {
+ return rect.width * rect.height;
+ }
+
+ public static DistToLineSegment(v: PIXIPoint, w: PIXIPoint, p: PIXIPoint) {
+ // Return minimum distance between line segment vw and point p
+ var l2 = MathUtil.DistSquared(v, w); // i.e. |w-v|^2 - avoid a sqrt
+ if (l2 == 0.0) return MathUtil.Dist(p, v); // v == w case
+ // Consider the line extending the segment, parameterized as v + t (w - v).
+ // We find projection of point p onto the line.
+ // It falls where t = [(p-v) . (w-v)] / |w-v|^2
+ // We clamp t from [0,1] to handle points outside the segment vw.
+ var dot = MathUtil.Dot(
+ MathUtil.SubtractPoint(p, v),
+ MathUtil.SubtractPoint(w, v)) / l2;
+ var t = Math.max(0, Math.min(1, dot));
+ // Projection falls on the segment
+ var projection = MathUtil.AddPoint(v,
+ MathUtil.MultiplyConstant(
+ MathUtil.SubtractPoint(w, v), t));
+ return MathUtil.Dist(p, projection);
+ }
+
+ public static LineSegmentIntersection(ps1: PIXIPoint, pe1: PIXIPoint, ps2: PIXIPoint, pe2: PIXIPoint): PIXIPoint | undefined {
+ var a1 = pe1.y - ps1.y;
+ var b1 = ps1.x - pe1.x;
+
+ var a2 = pe2.y - ps2.y;
+ var b2 = ps2.x - pe2.x;
+
+ var delta = a1 * b2 - a2 * b1;
+ if (delta == 0) {
+ return undefined;
+ }
+ var c2 = a2 * ps2.x + b2 * ps2.y;
+ var c1 = a1 * ps1.x + b1 * ps1.y;
+ var invdelta = 1 / delta;
+ return new PIXIPoint((b2 * c1 - b1 * c2) * invdelta, (a1 * c2 - a2 * c1) * invdelta);
+ }
+
+ public static PointInPIXIRectangle(p: PIXIPoint, rect: PIXIRectangle): boolean {
+ if (p.x < rect.left - this.EPSILON)
+ return false;
+ if (p.x > rect.right + this.EPSILON)
+ return false;
+ if (p.y < rect.top - this.EPSILON)
+ return false;
+ if (p.y > rect.bottom + this.EPSILON)
+ return false;
+
+ return true;
+ }
+
+ public static LinePIXIRectangleIntersection(lineFrom: PIXIPoint, lineTo: PIXIPoint, rect: PIXIRectangle): Array<PIXIPoint> {
+ var r1 = new PIXIPoint(rect.left, rect.top);
+ var r2 = new PIXIPoint(rect.right, rect.top);
+ var r3 = new PIXIPoint(rect.right, rect.bottom);
+ var r4 = new PIXIPoint(rect.left, rect.bottom);
+ var ret = new Array<PIXIPoint>();
+ var dist = this.Dist(lineFrom, lineTo)
+ var inter = this.LineSegmentIntersection(lineFrom, lineTo, r1, r2);
+ if (inter != null && this.PointInPIXIRectangle(inter, rect) &&
+ this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist)
+ ret.push(inter);
+ inter = this.LineSegmentIntersection(lineFrom, lineTo, r2, r3);
+ if (inter != null && this.PointInPIXIRectangle(inter, rect) &&
+ this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist)
+ ret.push(inter);
+ inter = this.LineSegmentIntersection(lineFrom, lineTo, r3, r4);
+ if (inter != null && this.PointInPIXIRectangle(inter, rect) &&
+ this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist)
+ ret.push(inter);
+ inter = this.LineSegmentIntersection(lineFrom, lineTo, r4, r1);
+ if (inter != null && this.PointInPIXIRectangle(inter, rect) &&
+ this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist)
+ ret.push(inter);
+ return ret;
+ }
+
+ public static Intersection(rect1: PIXIRectangle, rect2: PIXIRectangle): PIXIRectangle {
+ const left = Math.max(rect1.x, rect2.x);
+ const right = Math.min(rect1.x + rect1.width, rect2.x + rect2.width);
+ const top = Math.max(rect1.y, rect2.y);
+ const bottom = Math.min(rect1.y + rect1.height, rect2.y + rect2.height);
+ return new PIXIRectangle(left, top, right - left, bottom - top);
+ }
+
+ public static Dist(p1: PIXIPoint, p2: PIXIPoint): number {
+ return Math.sqrt(MathUtil.DistSquared(p1, p2));
+ }
+
+ public static Dot(p1: PIXIPoint, p2: PIXIPoint): number {
+ return p1.x * p2.x + p1.y * p2.y
+ }
+
+ public static Normalize(p1: PIXIPoint) {
+ var d = this.Length(p1);
+ return new PIXIPoint(p1.x / d, p1.y / d);
+ }
+
+ public static Length(p1: PIXIPoint): number {
+ return Math.sqrt(p1.x * p1.x + p1.y * p1.y);
+ }
+
+ public static DistSquared(p1: PIXIPoint, p2: PIXIPoint): number {
+ const a = p1.x - p2.x;
+ const b = p1.y - p2.y;
+ return (a * a + b * b)
+ }
+
+ public static RectIntersectsRect(r1: PIXIRectangle, r2: PIXIRectangle): boolean {
+ return !(r2.x > r1.x + r1.width ||
+ r2.x + r2.width < r1.x ||
+ r2.y > r1.y + r1.height ||
+ r2.y + r2.height < r1.y);
+ }
+
+ public static ArgMin(temp: number[]): number {
+ let index = 0;
+ let value = temp[0];
+ for (let i = 1; i < temp.length; i++) {
+ if (temp[i] < value) {
+ value = temp[i];
+ index = i;
+ }
+ }
+ return index;
+ }
+
+ public static ArgMax(temp: number[]): number {
+ let index = 0;
+ let value = temp[0];
+ for (let i = 1; i < temp.length; i++) {
+ if (temp[i] > value) {
+ value = temp[i];
+ index = i;
+ }
+ }
+ return index;
+ }
+
+ public static Combinations<T>(chars: T[]) {
+ let result = new Array<T>();
+ let f = (prefix: any, chars: any) => {
+ for (let i = 0; i < chars.length; i++) {
+ result.push(prefix.concat(chars[i]));
+ f(prefix.concat(chars[i]), chars.slice(i + 1));
+ }
+ };
+ f([], chars);
+ return result;
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/utils/PartialClass.ts b/src/client/northstar/utils/PartialClass.ts
new file mode 100644
index 000000000..2f20de96f
--- /dev/null
+++ b/src/client/northstar/utils/PartialClass.ts
@@ -0,0 +1,7 @@
+
+export class PartialClass<T> {
+
+ constructor(data?: Partial<T>) {
+ Object.assign(this, data);
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/utils/Utils.ts b/src/client/northstar/utils/Utils.ts
new file mode 100644
index 000000000..b35dce820
--- /dev/null
+++ b/src/client/northstar/utils/Utils.ts
@@ -0,0 +1,75 @@
+import { IBaseBrushable } from '../core/brusher/IBaseBrushable'
+import { IBaseFilterConsumer } from '../core/filter/IBaseFilterConsumer'
+import { IBaseFilterProvider } from '../core/filter/IBaseFilterProvider'
+import { AggregateFunction } from '../model/idea/idea'
+
+export class Utils {
+
+ public static EqualityHelper(a: Object, b: Object): boolean {
+ if (a === b) return true;
+ if (a === undefined && b !== undefined) return false;
+ if (a === null && b !== null) return false;
+ if (b === undefined && a !== undefined) return false;
+ if (b === null && a !== null) return false;
+ if ((<any>a).constructor.name !== (<any>b).constructor.name) return false;
+ return true;
+ }
+
+ public static LowercaseFirstLetter(str: string) {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+ }
+
+ //
+ // this Type Guard tests if dropTarget is an IDropTarget. If it is, it coerces the compiler
+ // to treat the dropTarget parameter as an IDropTarget *ouside* this function scope (ie, in
+ // the scope of where this function is called from).
+ //
+
+ public static isBaseBrushable<T>(obj: Object): obj is IBaseBrushable<T> {
+ let typed = <IBaseBrushable<T>>obj;
+ return typed != null && typed.BrusherModels !== undefined;
+ }
+
+ public static isBaseFilterProvider(obj: Object): obj is IBaseFilterProvider {
+ let typed = <IBaseFilterProvider>obj;
+ return typed != null && typed.FilterModels !== undefined;
+ }
+
+ public static isBaseFilterConsumer(obj: Object): obj is IBaseFilterConsumer {
+ let typed = <IBaseFilterConsumer>obj;
+ return typed != null && typed.FilterOperand !== undefined;
+ }
+
+ public static EncodeQueryData(data: any): string {
+ const ret = [];
+ for (let d in data) {
+ ret.push(encodeURIComponent(d) + "=" + encodeURIComponent(data[d]));
+ }
+ return ret.join("&");
+ }
+
+ public static ToVegaAggregationString(agg: AggregateFunction): string {
+ if (agg === AggregateFunction.Avg) {
+ return "average";
+ }
+ else if (agg === AggregateFunction.Count) {
+ return "count";
+ }
+ else {
+ return "";
+ }
+ }
+
+ public static GetQueryVariable(variable: string) {
+ let query = window.location.search.substring(1);
+ let vars = query.split("&");
+ for (let i = 0; i < vars.length; i++) {
+ let pair = vars[i].split("=");
+ if (decodeURIComponent(pair[0]) == variable) {
+ return decodeURIComponent(pair[1]);
+ }
+ }
+ return undefined;
+ }
+}
+
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index ac51a7d87..b27f63e52 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -1,4 +1,4 @@
-import { action, configure, observable, runInAction, trace, computed } from 'mobx';
+import { action, configure, observable, runInAction, trace, computed, reaction } from 'mobx';
import "normalize.css";
import * as React from 'react';
import * as ReactDOM from 'react-dom';
@@ -22,7 +22,6 @@ import "./Main.scss";
import { observer } from 'mobx-react';
import { InkingControl } from './InkingControl';
import { RouteStore } from '../../server/RouteStore';
-import { json } from 'body-parser';
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFont } from '@fortawesome/free-solid-svg-icons';
@@ -42,9 +41,10 @@ import { ServerUtils } from '../../server/ServerUtil';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
import { Field, Opt } from '../../fields/Field';
import { ListField } from '../../fields/ListField';
-import { map } from 'bluebird';
import { Gateway, Settings } from '../northstar/manager/Gateway';
-import { Catalog } from '../northstar/model/idea/idea';
+import { Catalog, Schema, Attribute, AttributeGroup } from '../northstar/model/idea/idea';
+import { ArrayUtil } from '../northstar/utils/ArrayUtil';
+import '../northstar/model/ModelExtensions'
@observer
export class Main extends React.Component {
@@ -88,16 +88,45 @@ export class Main extends React.Component {
this.initEventListeners();
Documents.initProtos(() => this.initAuthenticationRouters());
+
+ reaction(() => [this.mainContainer, this.ActiveSchema],
+ () => {
+ if (this.mainContainer && this.ActiveSchema) {
+ if (!this.mainContainer!.GetTAsync(KeyStore.ActiveDB, ListField, field => this.NorthstarCatalog = field!.Data as Document[])) {
+ this.NorthstarCatalog = this.GetAllAttributes().map(a => Documents.HistogramDocument({ width: 200, height: 200, title: a.displayName! }));
+ this.mainContainer!.SetData(KeyStore.ActiveDB, this.NorthstarCatalog, ListField);
+ }
+ }
+ })
}
+ NorthstarCatalog: Document[] = [];
+ @observable ActiveSchema: Schema | undefined;
@action SetNorthstarCatalog(ctlog: Catalog) {
this._northstarCatalog = ctlog;
- if (this._northstarCatalog) {
+ if (this._northstarCatalog && this._northstarCatalog.schemas) {
console.log("CATALOG " + this._northstarCatalog.schemas);
+ this.ActiveSchema = ArrayUtil.FirstOrDefault<Schema>(this._northstarCatalog.schemas!, (s: Schema) => s.displayName === "mimic");
}
}
+ public GetAllAttributes() {
+ if (!this.ActiveSchema || !this.ActiveSchema.rootAttributeGroup) {
+ return [];
+ }
+ const recurs = (attrs: Attribute[], g: AttributeGroup) => {
+ if (g.attributes) {
+ attrs.push.apply(attrs, g.attributes);
+ if (g.attributeGroups) {
+ g.attributeGroups.forEach(ng => recurs(attrs, ng));
+ }
+ }
+ };
+ const allAttributes: Attribute[] = new Array<Attribute>();
+ recurs(allAttributes, this.ActiveSchema.rootAttributeGroup);
+ return allAttributes;
+ }
async initializeNorthstar(): Promise<void> {
- let envPath = "assets/env.json";
+ let envPath = "/assets/env.json";
const response = await fetch(envPath, {
redirect: "follow",
method: "GET",
@@ -251,7 +280,7 @@ export class Main extends React.Component {
let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" }))
let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" }));
- let addSchemaNode = action(() => Documents.SchemaDocument([], { width: 200, height: 200, title: "a schema collection" }));
+ let addSchemaNode = action(() => Documents.SchemaDocument(this.NorthstarCatalog, { width: 200, height: 200, title: "a schema collection" }));
let addVideoNode = action(() => Documents.VideoDocument(videourl, { width: 200, height: 200, title: "video node" }));
let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a schema collection" }));
let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" }));
@@ -269,7 +298,9 @@ export class Main extends React.Component {
[React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode],
]
- let addClick = (creator: () => Document) => action(() => this.mainfreeform!.GetList<Document>(KeyStore.Data, []).push(creator()));
+ let addClick = (creator: () => Document) => action(() => {
+ this.mainfreeform!.GetList<Document>(KeyStore.Data, []).push(creator())
+ });
return < div id="add-nodes-menu" >
<input type="checkbox" id="add-menu-toggle" />
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index ce72ab64b..2f0459f88 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -19,6 +19,7 @@ import { KeyValueBox } from "./KeyValueBox";
import { PDFBox } from "./PDFBox";
import { VideoBox } from "./VideoBox";
import { WebBox } from "./WebBox";
+import { HistogramBox } from "./HistogramBox";
import React = require("react");
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -47,8 +48,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
render() {
return <JsxParser
- components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox }}
- bindings={this.CreateBindings()}
+ components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} bindings={this.CreateBindings()}
jsx={this.layout}
showWarnings={true}
onError={(test: any) => { console.log(test) }}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index b6d50bffb..f6343c631 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -7,7 +7,6 @@ import { TextField } from "../../../fields/TextField";
import { NumberField } from "../../../fields/NumberField";
import { RichTextField } from "../../../fields/RichTextField";
import { ImageField } from "../../../fields/ImageField";
-import { WebField } from "../../../fields/WebField";
import { VideoField } from "../../../fields/VideoField"
import { Key } from "../../../fields/Key";
import { FormattedTextBox } from "./FormattedTextBox";
@@ -64,9 +63,11 @@ export class FieldView extends React.Component<FieldViewProps> {
}
else if (field instanceof AudioField) {
return <AudioBox {...this.props} />
- } else if (field instanceof Document) {
+ }
+ else if (field instanceof Document) {
return <div>{field.Title}</div>
- } else if (field instanceof ListField) {
+ }
+ else if (field instanceof ListField) {
return (<div>
{(field as ListField<Field>).Data.map(f => {
return f instanceof Document ? f.Title : f.GetValue().toString();
diff --git a/src/client/views/nodes/HistogramBox.scss b/src/client/views/nodes/HistogramBox.scss
new file mode 100644
index 000000000..04bf1d732
--- /dev/null
+++ b/src/client/views/nodes/HistogramBox.scss
@@ -0,0 +1,8 @@
+.histogrambox-container {
+ padding: 0vw;
+ position: relative;
+ text-align: center;
+ width: 100%;
+ height: 100%;
+ }
+ \ No newline at end of file
diff --git a/src/client/views/nodes/HistogramBox.tsx b/src/client/views/nodes/HistogramBox.tsx
new file mode 100644
index 000000000..0fcc25e66
--- /dev/null
+++ b/src/client/views/nodes/HistogramBox.tsx
@@ -0,0 +1,67 @@
+import React = require("react")
+import { observer } from "mobx-react";
+import { FieldView, FieldViewProps } from './FieldView';
+import "./VideoBox.scss";
+import { observable, reaction } from "mobx";
+import { HistogramOperation } from "../../northstar/operations/HistogramOperation";
+import { Main } from "../Main";
+import { ColumnAttributeModel } from "../../northstar/core/attribute/AttributeModel";
+import { AttributeTransformationModel } from "../../northstar/core/attribute/AttributeTransformationModel";
+import { AggregateFunction, HistogramResult, DoubleValueAggregateResult } from "../../northstar/model/idea/idea";
+import { ModelHelpers } from "../../northstar/model/ModelHelpers";
+
+@observer
+export class HistogramBox extends React.Component<FieldViewProps> {
+
+ public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(HistogramBox, fieldStr) }
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ }
+
+ @observable _histoResult?: HistogramResult;
+ _histoOp?: HistogramOperation;
+
+ componentDidMount() {
+ Main.Instance.GetAllAttributes().map(a => {
+ if (a.displayName == this.props.doc.Title) {
+ var atmod = new ColumnAttributeModel(a);
+ this._histoOp = new HistogramOperation(new AttributeTransformationModel(atmod, AggregateFunction.None),
+ new AttributeTransformationModel(atmod, AggregateFunction.Count),
+ new AttributeTransformationModel(atmod, AggregateFunction.Count));
+ reaction(() => [this._histoOp && this._histoOp.Result],
+ () => this._histoResult = this._histoOp ? this._histoOp.Result as HistogramResult : undefined
+ );
+ this._histoOp.Update();
+ }
+ })
+ }
+
+ twoString() {
+ let str = "";
+ if (this._histoResult && !this._histoResult.isEmpty) {
+ for (let key in this._histoResult.bins) {
+ if (this._histoResult.bins.hasOwnProperty(key)) {
+ let bin = this._histoResult.bins[key];
+ str += JSON.stringify(bin.binIndex!.toJSON()) + " = ";
+ let valueAggregateKey = ModelHelpers.CreateAggregateKey(this._histoOp!.V, this._histoResult, ModelHelpers.AllBrushIndex(this._histoResult));
+ let value = ModelHelpers.GetAggregateResult(bin, valueAggregateKey) as DoubleValueAggregateResult;
+ if (value && value.hasResult && value.result) {
+ str += value.result;
+ }
+ }
+ }
+ }
+ return str;
+ }
+
+ render() {
+ if (!this._histoResult)
+ return (null);
+ return (
+ <div className="histogrambox-container">
+ `HISTOGRAM RESULT : ${this.twoString()}`
+ </div>
+ )
+ }
+} \ No newline at end of file
diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts
index 68883d6f1..319702cc4 100644
--- a/src/fields/KeyStore.ts
+++ b/src/fields/KeyStore.ts
@@ -28,6 +28,7 @@ export namespace KeyStore {
export const SchemaSplitPercentage = new Key("SchemaSplitPercentage");
export const Caption = new Key("Caption");
export const ActiveFrame = new Key("ActiveFrame");
+ export const ActiveDB = new Key("ActiveDB");
export const DocumentText = new Key("DocumentText");
export const LinkedToDocs = new Key("LinkedToDocs");
export const LinkedFromDocs = new Key("LinkedFromDocs");