diff options
author | Tyler Schicke <tyler_schicke@brown.edu> | 2019-03-21 21:36:52 -0400 |
---|---|---|
committer | Tyler Schicke <tyler_schicke@brown.edu> | 2019-03-21 21:36:52 -0400 |
commit | ce4e7a03e82c71bd5c979b19308f4dba03be08a2 (patch) | |
tree | 3f1eb320790bd66cc128c8899c5ee9ed170d3382 /src | |
parent | 27299e9666ec0aae6a4d1335fd30b2714ad67970 (diff) | |
parent | 1cf618563838f4ce7d8a98c8a0c8d94670bc4e18 (diff) |
Merge branch 'master' of github-tsch-brown:browngraphicslab/Dash-Web into promises_and_user_document
Diffstat (limited to 'src')
43 files changed, 2304 insertions, 37 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 4ed1fc3ed..cf6d7d503 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); } @@ -200,6 +212,9 @@ export namespace Documents { export function SchemaDocument(documents: Array<Document>, options: DocumentOptions, id?: string) { return assignToDelegate(SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Schema }, [documents, ListField], id), options) } + export function TreeDocument(documents: Array<Document>, options: DocumentOptions, id?: string) { + return assignToDelegate(SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Tree }, [documents, ListField], id), options) + } export function DockDocument(config: string, options: DocumentOptions, id?: string) { return assignToDelegate(SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Docking }, [config, TextField], id), 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 06a9a92d3..d2ba6998c 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'; @@ -23,7 +23,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'; @@ -37,15 +36,18 @@ import { faRedoAlt } from '@fortawesome/free-solid-svg-icons'; import { faPenNib } from '@fortawesome/free-solid-svg-icons'; import { faFilm } from '@fortawesome/free-solid-svg-icons'; import { faMusic } from '@fortawesome/free-solid-svg-icons'; +import { faTree } from '@fortawesome/free-solid-svg-icons'; import Measure from 'react-measure'; import { DashUserModel } from '../../server/authentication/models/user_model'; import { ServerUtils } from '../../server/ServerUtil'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { Field, Opt, FieldWaiting } 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' +import '../northstar/utils/Extensions' @observer export class Main extends React.Component { @@ -53,7 +55,8 @@ export class Main extends React.Component { @observable private mainfreeform?: Document; @observable public pwidth: number = 0; @observable public pheight: number = 0; - @observable private _northstarCatalog: Catalog | undefined = undefined; + @observable ActiveSchema: Schema | undefined; + private _northstarColumns: Document[] = []; @computed private get mainContainer(): Document | undefined { let doc = this.userDocument.GetT(KeyStore.ActiveWorkspace, Document); @@ -87,6 +90,10 @@ export class Main extends React.Component { }; // this.initializeNorthstar(); + let y = ""; + y.ReplaceAll("a", "B"); + + CurrentUserUtils.loadCurrentUser(); library.add(faFont); library.add(faImage); @@ -99,29 +106,12 @@ export class Main extends React.Component { library.add(faPenNib); library.add(faFilm); library.add(faMusic); - + library.add(faTree); this.initEventListeners(); Documents.initProtos(() => this.initAuthenticationRouters()); - } - @action SetNorthstarCatalog(ctlog: Catalog) { - this._northstarCatalog = ctlog; - if (this._northstarCatalog) { - console.log("CATALOG " + this._northstarCatalog.schemas); - } - } - async initializeNorthstar(): Promise<void> { - let envPath = "assets/env.json"; - const response = await fetch(envPath, { - redirect: "follow", - method: "GET", - credentials: "include" - }); - const env = await response.json(); - Settings.Instance.Update(env); - let cat = Gateway.Instance.ClearCatalog(); - cat.then(async () => this.SetNorthstarCatalog(await Gateway.Instance.GetCatalog())); + this.initializeNorthstar(); } onHistory = () => { @@ -259,6 +249,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 addTreeNode = action(() => Documents.TreeDocument(this._northstarColumns, { width: 200, height: 200, title: "a tree 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" })); @@ -273,6 +264,7 @@ export class Main extends React.Component { [React.createRef<HTMLDivElement>(), "music", "Add Audio", addAudioNode], [React.createRef<HTMLDivElement>(), "globe-asia", "Add Web Clipping", addWebNode], [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode], + [React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode], [React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode], ] @@ -344,6 +336,43 @@ export class Main extends React.Component { </div> ); } + + // --------------- Northstar hooks ------------- / + + @action SetNorthstarCatalog(ctlog: Catalog) { + if (ctlog && ctlog.schemas) { + this.ActiveSchema = ArrayUtil.FirstOrDefault<Schema>(ctlog.schemas!, (s: Schema) => s.displayName === "mimic"); + this._northstarColumns = this.GetAllNorthstarColumnAttributes().map(a => Documents.HistogramDocument({ width: 200, height: 200, title: a.displayName! })); + } + } + async initializeNorthstar(): Promise<void> { + let envPath = "/assets/env.json"; + const response = await fetch(envPath, { + redirect: "follow", + method: "GET", + credentials: "include" + }); + const env = await response.json(); + Settings.Instance.Update(env); + let cat = Gateway.Instance.ClearCatalog(); + cat.then(async () => this.SetNorthstarCatalog(await Gateway.Instance.GetCatalog())); + } + public GetAllNorthstarColumnAttributes() { + 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; + } } CurrentUserUtils.loadCurrentUser().then(() => { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index fd0810242..950df7261 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -17,6 +17,7 @@ import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; import React = require("react"); import { SubCollectionViewProps } from "./CollectionViewBase"; import { ServerUtils } from "../../../server/ServerUtil"; +import { DragManager } from "../../util/DragManager"; @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { @@ -190,6 +191,21 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @action onPointerDown = (e: React.PointerEvent): void => { var className = (e.target as any).className; + if ((className == "lm_title" || className == "lm_tab lm_active") && e.ctrlKey) { + e.stopPropagation(); + e.preventDefault(); + let docid = (e.target as any).DashDocId; + let tab = (e.target as any).parentElement as HTMLElement; + Server.GetField(docid, action((f: Opt<Field>) => + DragManager.StartDocumentDrag(tab, new DragManager.DocumentDragData(f as Document), + { + handlers: { + dragComplete: action(() => { }), + }, + hideSource: true + })) + ); + } if (className == "lm_drag_handle" || className == "lm_close" || className == "lm_maximise" || className == "lm_minimise" || className == "lm_close_tab") { this._flush = true; } @@ -208,6 +224,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this.stateChanged(); } tabCreated = (tab: any) => { + tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; tab.closeElement.off('click') //unbind the current click handler .click(function () { tab.contentItem.remove(); diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index b986f394c..34b019244 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -2,7 +2,7 @@ import React = require("react") import { library } from '@fortawesome/fontawesome-svg-core'; import { faCog } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable } from "mobx"; +import { action, computed, observable, trace, untracked } from "mobx"; import { observer } from "mobx-react"; import Measure from "react-measure"; import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; @@ -141,6 +141,7 @@ export class CollectionSchemaView extends CollectionViewBase { }; } + @computed get columns() { return this.props.Document.GetList<Key>(KeyStore.ColumnsKey, []); } @@ -167,10 +168,19 @@ export class CollectionSchemaView extends CollectionViewBase { this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage == 0 ? 33 : 0); } } - findAllDocumentKeys = (): { [id: string]: boolean } => { + + @computed + get findAllDocumentKeys(): { [id: string]: boolean } { const docs = this.props.Document.GetList<Document>(this.props.fieldKey, []); let keys: { [id: string]: boolean } = {} - docs.map(doc => doc.GetAllPrototypes().map(proto => proto._proxies.forEach((val: any, key: string) => keys[key] = false))); + if (this._optionsActivated > -1) { + // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. + // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be + // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked. + // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu + // is displayed (unlikely) it won't show up until something else changes. + untracked(() => docs.map(doc => doc.GetAllPrototypes().map(proto => proto._proxies.forEach((val: any, key: string) => keys[key] = false)))); + } this.columns.forEach(key => keys[key.Id] = true) return keys; } @@ -228,13 +238,18 @@ export class CollectionSchemaView extends CollectionViewBase { } } + @observable _optionsActivated: number = 0; + @action + OptionsMenuDown = (e: React.PointerEvent) => { + this._optionsActivated++; + } render() { library.add(faCog); const columns = this.columns; const children = this.props.Document.GetList<Document>(this.props.fieldKey, []); const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined; //all the keys/columns that will be displayed in the schema - const allKeys = this.findAllDocumentKeys(); + const allKeys = this.findAllDocumentKeys; let content = this._selectedIndex == -1 || !selected ? (null) : ( <Measure onResize={this.setScaling}> {({ measureRef }) => @@ -274,7 +289,7 @@ export class CollectionSchemaView extends CollectionViewBase { </div> </div> }> - <button id="schemaOptionsMenuBtn"><FontAwesomeIcon style={{ color: "white" }} icon="cog" size="sm" /></button> + <button id="schemaOptionsMenuBtn" onPointerDown={this.OptionsMenuDown}><FontAwesomeIcon style={{ color: "white" }} icon="cog" size="sm" /></button> </Flyout>); return ( diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index ec1bf5d0e..6cc14ebcb 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -76,7 +76,7 @@ class TreeView extends React.Component<TreeViewProps> { }} />); return ( - <div key={this.props.document.Id} className="docContainer" ref={reference} onPointerDown={onItemDown}> + <div className="docContainer" ref={reference} onPointerDown={onItemDown}> {editableView(this.props.document.Title)} <div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div> </div >) 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..223fdf0d8 --- /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.GetAllNorthstarColumnAttributes().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/Document.ts b/src/fields/Document.ts index 66d3d3aef..fc4906d17 100644 --- a/src/fields/Document.ts +++ b/src/fields/Document.ts @@ -36,7 +36,7 @@ export class Document extends Field { @computed public get Title() { - return this.GetText(KeyStore.Title, "<untitled>"); + return this.GetText(KeyStore.Title, "-untitled-"); } @computed diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts index 891caaa81..611e2951b 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -29,6 +29,7 @@ export namespace KeyStore { export const Caption = new Key("Caption"); export const ActiveFrame = new Key("ActiveFrame"); export const ActiveWorkspace = new Key("ActiveWorkspace"); + export const ActiveDB = new Key("ActiveDB"); export const DocumentText = new Key("DocumentText"); export const LinkedToDocs = new Key("LinkedToDocs"); export const LinkedFromDocs = new Key("LinkedFromDocs"); |