devexpress-gantt
Version:
DevExpress Gantt Control
562 lines (531 loc) • 24 kB
text/typescript
import { Task } from "../Entities/Task";
import { ResourceCollection } from "../Collections/ResourceCollection";
import { TaskCollection } from "../Collections/TaskCollection";
import { DependencyCollection } from "../Collections/DependencyCollection";
import { ResourceAssignmentCollection } from "../Collections/ResourceAssignmentCollection";
import { ViewVisualModelItem } from "./ViewVisualModelItem";
import { ViewVisualModelDependencyInfo } from "./ViewVisualModelDependencyInfo";
import { WorkingTimeCalculator } from "../WorkingTime/WorkingTimeCalculator";
import { DateRange } from "../WorkingTime/DateRange";
import { isDefined } from "@devexpress/utils/lib/utils/common";
import { DataObject, GanttDataObjectNames } from "../Entities/DataObject";
import { Dependency } from "../Entities/Dependency";
import { Resource } from "../Entities/Resource";
import { ResourceAssignment } from "../Entities/ResourceAssignment";
import { GanttView } from "../../View/GanttView";
import { RenderHelper } from "../../View/Render/RenderHelper";
export class ViewVisualModel {
owner: GanttView;
tasks: TaskCollection;
dependencies: DependencyCollection;
resources: ResourceCollection;
assignments: ResourceAssignmentCollection;
root: ViewVisualModelItem;
rootTaskId: string;
private _itemList: Array<ViewVisualModelItem>;
private _viewItemList: Array<ViewVisualModelItem>;
private _workTimeCalculator: WorkingTimeCalculator;
private _fLockCount: number = 0;
lockChangesProcessing: boolean = false;
constructor(owner: any, tasks: any, dependencies: any, resources: any, assignments: any, dateRange: DateRange, workTimeRules: any) {
this.owner = owner;
this.tasks = new TaskCollection();
this.tasks.importFromObject(tasks);
this.dependencies = new DependencyCollection();
this.dependencies.importFromObject(dependencies);
this.resources = new ResourceCollection();
this.resources.importFromObject(resources);
this.assignments = new ResourceAssignmentCollection();
this.assignments.importFromObject(assignments);
this._itemList = new Array<ViewVisualModelItem>();
this._viewItemList = new Array<ViewVisualModelItem>();
this._workTimeCalculator = new WorkingTimeCalculator(dateRange, workTimeRules);
this.updateModel(true);
}
private get renderHelper(): RenderHelper {
return this.owner.renderHelper;
}
updateModel(isFirstLoad? : boolean): void {
this._itemList.splice(0, this._itemList.length);
const tasks = this.tasks.items;
for(let i = 0; i < tasks.length; i++) {
const task = tasks[i];
if(task)
this._itemList.push(new ViewVisualModelItem(task, this.getAssignedResources(task)));
}
this.createHierarchy(isFirstLoad);
this.populateItemsForView();
if(this.owner && this.owner.currentSelectedTaskID)
this.changeTaskSelected(this.owner.currentSelectedTaskID, true);
}
createHierarchy(isFirstLoad? : boolean): void {
this.root = new ViewVisualModelItem(null, null);
const list = this._itemList;
const inverted = list.reduce((previous, item) => {
const key = item.task?.internalId;
if(isDefined(key))
previous[key] = item;
return previous;
}, { });
const recalculateParentRequired = this.requireFirstLoadParentAutoCalc && isFirstLoad;
for(let i = 0; i < list.length; i++) {
const item = list[i];
const parentId = item.task.parentId;
const parentItem = inverted[parentId] || this.root;
item.parent = parentItem;
parentItem.addChild(item);
if(recalculateParentRequired)
this.owner.validationController.recalculateParents(item, (data: any) => {
if(!isDefined(data.id)) return;
const task = this.tasks.getItemById(data.id);
if(isDefined(data.start)) task.start = data.start;
if(isDefined(data.end)) task.end = data.end;
if(isDefined(data.progress)) task.progress = data.progress;
});
}
if(recalculateParentRequired)
this.owner.dispatcher.notifyParentDataRecalculated(this.getCurrentTaskData());
}
getCurrentTaskData(): any[] { return this.tasks.items.map(t => this.getTaskObjectForDataSource(t)); }
getTaskObjectForDataSource(task: Task): any {
const parentTask = task.parentId && this.tasks.getItemById(task.parentId);
const rootId = this.getRootTaskId();
const isRootLevelTask = rootId && task.parentId === rootId;
const parentId = isRootLevelTask ? this.getRootRawValue() : parentTask?.id;
const taskObject = {
id: task.id,
start: task.isValidStart() ? task.start : null,
end: task.isValidEnd() ? task.end : null,
duration: task.duration,
description: task.description,
parentId: parentId,
progress: task.progress,
color: task.color,
taskType: task.taskType,
title: task.title,
customFields: task.customFields,
expanded: task.expanded
};
return taskObject;
}
getDependencyObjectForDataSource(key: any): any {
const dependency = key instanceof Dependency ? key as Dependency : this.getItemByPublicId("dependency", key) as Dependency;
if(dependency) {
const predecessorId = this.convertInternalToPublicKey("task", dependency.predecessorId);
const successorId = this.convertInternalToPublicKey("task", dependency.successorId);
return {
id: dependency.id,
predecessorId: isDefined(predecessorId) ? predecessorId : dependency.predecessorId,
successorId: isDefined(successorId) ? successorId : dependency.successorId,
type: dependency.type
};
}
return null;
}
getResourceObjectForDataSource(key: any): any {
const resource = key instanceof Resource ? key as Resource : this.getItemByPublicId("resource", key) as Resource;
if(resource)
return {
id: resource.id,
text: resource.text,
color: resource.color
};
return null;
}
getResourceAssignmentObjectForDataSource(key: any): any {
const assignment = key instanceof ResourceAssignment ? key as ResourceAssignment : this.getItemByPublicId("assignment", key) as ResourceAssignment;
if(assignment) {
const taskId = this.convertInternalToPublicKey("task", assignment.taskId);
const resourceId = this.convertInternalToPublicKey("resource", assignment.resourceId);
return {
id: assignment.id,
taskId: isDefined(taskId) ? taskId : assignment.taskId,
resourceId: isDefined(resourceId) ? resourceId : assignment.resourceId
};
}
return null;
}
getRootRawValue(): any {
return this.root.children[0]?.task?.rawParentId ?? null;
}
populateItemsForView(): void {
this._viewItemList.splice(0, this._viewItemList.length);
this.populateVisibleItems(this.root);
this.updateVisibleItemDependencies();
}
populateVisibleItems(item: ViewVisualModelItem): void {
const isRoot = item === this.root;
if(!item || (!item.task && !isRoot)) return;
if(!isRoot) {
this._viewItemList.push(item);
item.visibleIndex = this._viewItemList.length - 1;
}
if(item.getExpanded() || item === this.root)
item.children.forEach(n => this.populateVisibleItems(n));
}
updateVisibleItemDependencies(): void {
const list = this._viewItemList;
for(let i = 0; i < list.length; i++) {
const item = list[i];
const visibleDependencies = this.getTasVisibleDependencies(item.task);
item.setDependencies(visibleDependencies);
}
}
getAssignedResources(task: Task): ResourceCollection {
const res = new ResourceCollection();
const assignments = this.assignments.items.filter(value => value.taskId == task.internalId);
assignments.forEach(assignment => { res.add(this.resources.getItemById(assignment.resourceId)); });
return res;
}
getTasVisibleDependencies(task: Task): Array<ViewVisualModelDependencyInfo> {
const res = new Array<ViewVisualModelDependencyInfo>();
const id = task.internalId;
const dependencies = this.dependencies.items.filter(value => value.successorId == id);
for(let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
const item = this.findItem(dependency.predecessorId);
if(item && item.getVisible())
res.push(new ViewVisualModelDependencyInfo(dependency.internalId, item, dependency.type));
}
return res;
}
changeTaskExpanded(id: string, expanded: boolean): void {
const task = this.tasks.getItemById(String(id));
if(task) {
task.expanded = expanded;
this.changed();
}
}
changeTaskVisibility(id: string, visible: boolean): void {
const item = this.findItem(id);
if(item) {
item.visible = visible;
this.changed();
}
}
changeTaskSelected(id: string, selected: boolean): void {
const item = this._itemList.filter(value => value.task && value.task.internalId === id)[0];
if(item) {
item.selected = selected;
const viewItem = this.findItem(id);
const taskIndex = viewItem && viewItem.visibleIndex;
if(taskIndex > -1)
this.renderHelper.recreateTaskElement(taskIndex);
}
}
beginUpdate(): void {
this._fLockCount++;
}
endUpdate(): void {
this._fLockCount--;
if(this._fLockCount == 0)
this.changed();
}
public compareTaskOrder(taskModel: any): boolean {
const newTasks = new TaskCollection();
newTasks.importFromObject(taskModel);
const newItems = newTasks.items;
const oldItems = this.tasks.items;
if(newItems.length !== oldItems.length)
return false;
for(let i = 0; i < newItems.length; i++) {
const newTask = newItems[i];
const oldTask = oldItems[i];
if(newTask.id !== oldTask.id)
return false;
}
return true;
}
refreshTaskDataIfRequires(tasks: any): boolean {
const changed = !this.lockChangesProcessing && !this.compareTaskOrder(tasks);
if(changed) {
const hash = this.saveTaskInternalIds();
this.tasks.importFromObject(tasks);
this.restoreTaskInternalIds(hash);
this.updateModel();
}
return changed;
}
public saveTaskInternalIds(): Record<any, string> {
const hash = { };
this.tasks.items.map(t => hash[t.id] = t.internalId);
return hash;
}
public restoreTaskInternalIds(hash : Record<any, string>): void {
for(const id in hash) {
if(!Object.prototype.hasOwnProperty.call(hash, id))
continue;
const task = this.tasks.getItemByPublicId(id);
if(task)
task.internalId = hash[id];
}
}
public canCreateDependency(predecessorId: string, successorId: string): boolean {
if(!predecessorId || !successorId || predecessorId === successorId)
return false;
let hasLink = false;
if(this.enableDependencyValidation) {
const hash = this.getDependentTasksHash();
if(this.parentAutoCalc)
hasLink = this.checkTasksInterdependence(predecessorId, successorId, hash) || this.checkParent(predecessorId, successorId) || this.checkParent(successorId, predecessorId);
else
hasLink = this.checkDependencyChain([predecessorId], [successorId], hash, []);
}
return !hasLink;
}
public checkParent(childId: string, parentId: string): boolean {
return this.getTaskTreeLine(childId).indexOf(parentId) > -1;
}
public getTaskTreeLine(taskId: string): Array<string> {
const result = [taskId];
let item = this.findItem(taskId);
if(item) {
item = item.parent;
while(item?.task) {
result.push(item?.task.internalId);
item = item.parent;
}
}
else {
const task = this.tasks.getItemById(taskId);
let parent = this.tasks.getItemById(task?.parentId);
while(parent) {
result.push(parent.id);
parent = this.tasks.getItemById(parent.parentId);
}
}
return result;
}
getDependentTasksHash(): Record<string, Array<string>> {
const result = { };
this.dependencies.items.forEach(d => {
const id1 = d.predecessorId;
const id2 = d.successorId;
result[id1] ??= [ ];
if(result[id1].indexOf(id2) < 0)
result[id1].push(id2);
result[id2] ??= [ ];
if(result[id2].indexOf(id1) < 0)
result[id2].push(id1);
});
return result;
}
checkTasksInterdependence(id1: string, id2: string, depHash: Record<string, Array<string>>): boolean {
const treeLine1 = this.getTaskTreeLine(id1).reverse();
const treeLine2 = this.getTaskTreeLine(id2).reverse();
let group1Root;
let group2Root;
for(let i = 0; i < treeLine1.length - 1; i++) {
const id = treeLine1[i];
const index = treeLine2.indexOf(id);
if(index > -1) {
group1Root = treeLine1[i + 1];
group2Root = treeLine2[index + 1];
}
}
if(!group1Root && !group2Root) {
group1Root = treeLine1[0];
group2Root = treeLine2[0];
}
return this.checkDependencyChain(this.getBranchIds(group1Root), this.getBranchIds(group2Root), depHash, []);
}
checkDependencyChain(group1: Array<string>, group2: Array<string>, depHash: Record<string, Array<string>>, checked: Array<string>): boolean {
if(group1.some(id => group2.indexOf(id) > -1))
return true;
checked.push(...group1);
for(let i = 0; i < group1.length; i++) {
const id1 = group1[i];
const dependent = depHash[id1];
if(dependent && this.checkDependencyChain(dependent.filter(id => checked.indexOf(id) === -1), group2, depHash, checked))
return true;
}
return false;
}
getBranchIds(id: string): Array<string> {
let result = [id];
const item = this.findItem(id);
const children = item?.children;
if(children)
children.forEach(vi => {
const key = vi.task?.internalId;
if(key)
result = result.concat(this.getBranchIds(key));
});
return result;
}
getTasksExpandedState(): Record<any, boolean> {
const items = this.tasks.items;
const state = { };
items.forEach(t => state[t.id] = t.expanded);
return state;
}
applyTasksExpandedState(state: Record<any, boolean>): void {
if(!state) return;
this.beginUpdate();
for(const key in state)
if(Object.prototype.hasOwnProperty.call(state, key))
this.changeTaskExpanded(key, state[key]);
this.endUpdate();
}
changed(): void {
if(this._fLockCount !== 0)
return;
this.populateItemsForView();
if(this.owner && this.owner.onVisualModelChanged)
this.owner.onVisualModelChanged();
}
findItem(taskId: string): ViewVisualModelItem {
return this._viewItemList.filter(value => value.task && value.task.internalId === taskId)[0];
}
get items(): Array<ViewVisualModelItem> { return this._viewItemList; }
get itemCount(): number { return this.items.length; }
getTaskVisibility(id: string):boolean {
const item = this.findItem(id);
return !!item && item.getVisible();
}
getTaskSelected(id: string):boolean {
const item = this.findItem(id);
return !!item && item.selected;
}
get noWorkingIntervals(): Array<DateRange> { return this._workTimeCalculator.noWorkingIntervals; }
updateRange(range: DateRange): void { this._workTimeCalculator.updateRange(range); }
taskHasChildrenByIndex(index: number): boolean { return this._viewItemList[index].children.length > 0; }
taskHasChildren(id: string) : boolean {
const item = this.findItem(id);
return item && item.children.length > 0;
}
get enableDependencyValidation(): boolean {
const settings = this.owner && this.owner.settings;
return settings?.validation?.validateDependencies;
}
get parentAutoCalc(): boolean {
const settings = this.owner && this.owner.settings;
return settings && settings.validation && settings.validation.autoUpdateParentTasks;
}
get enablePredecessorGap(): boolean {
const settings = this.owner && this.owner.settings;
return settings && settings.validation && settings.validation.enablePredecessorGap;
}
get requireFirstLoadParentAutoCalc(): boolean { return this.parentAutoCalc && this.owner.requireFirstLoadParentAutoCalc(); }
isTaskToCalculateByChildren(id: string): boolean { return this.parentAutoCalc && this.taskHasChildren(id); }
hasTasks(): boolean { return this.tasks.length > 0; }
getDataUpdateErrorCallback(): () => void {
return this.owner.getDataUpdateErrorCallback && this.owner.getDataUpdateErrorCallback();
}
convertPublicToInternalKey(dataType: string, publicKey: any): string {
const item = this.getItemByPublicId(dataType, publicKey);
return item && item.internalId;
}
convertInternalToPublicKey(dataType: string, internalId: string): any {
const item = this.getItemByInternalId(dataType, internalId);
return item && item.id;
}
getItemByPublicId(dataType: string, publicKey: any): DataObject {
const strKey = publicKey.toString();
switch(dataType) {
case "task":
return this.tasks.getItemByPublicId(strKey);
case "dependency":
return this.dependencies.getItemByPublicId(strKey);
case "resource":
return this.resources.getItemByPublicId(strKey);
case "assignment":
return this.assignments.getItemByPublicId(strKey);
}
return null;
}
getItemByInternalId(dataType: string, internalId: string): DataObject {
switch(dataType) {
case "task":
return this.tasks.getItemById(internalId);
case "dependency":
return this.dependencies.getItemById(internalId);
case "resource":
return this.resources.getItemById(internalId);
case "assignment":
return this.assignments.getItemById(internalId);
}
return null;
}
findAssignment(resourceKey: any, taskKey: any): ResourceAssignment {
const resourceInternalKey = this.convertPublicToInternalKey("resource", resourceKey);
const taskInternalKey = this.convertPublicToInternalKey("task", taskKey);
return this.assignments.items.filter(val => val.resourceId === resourceInternalKey && val.taskId === taskInternalKey)[0];
}
findAllTaskAssignments(taskInternalKey: any): ResourceAssignment[] {
return this.assignments.items.filter(val => val.taskId === taskInternalKey);
}
getAllVisibleTaskIndices(start?: number, end?: number): Array<number> {
const result = [];
start ??= 0;
end ??= this._viewItemList.length - 1;
for(let i = start; i <= end; i++) {
const item = this._viewItemList[i];
if(item?.getVisible() && item?.task?.isValid())
result.push(i);
}
return result;
}
getVisibleTasks(): Array<Task> { return this.tasks.items.filter(t => t && this.getTaskVisibility(t.internalId) && t.isValid()); }
getVisibleDependencies(): Array<Dependency> {
const visibleTasksKeys = this.getVisibleTasks().map(t => t.internalId);
return this.dependencies.items.filter(d => d && visibleTasksKeys.indexOf(d.successorId) > -1 && visibleTasksKeys.indexOf(d.predecessorId) > -1);
}
getVisibleResourceAssignments(): Array<ResourceAssignment> {
const visibleTasksKeys = this.getVisibleTasks().map(t => t.internalId);
return this.assignments.items.filter(a => a && visibleTasksKeys.indexOf(a.taskId) > -1);
}
getVisibleResources(): Array<Resource> {
const visibleResources = [];
const visibleAssignments = this.getVisibleResourceAssignments();
for(let i = 0; i < visibleAssignments.length; i++) {
const resource = this.getItemByInternalId("resource", visibleAssignments[i].resourceId);
if(resource && visibleResources.indexOf(resource) === -1)
visibleResources.push(resource);
}
return visibleResources;
}
getRootTaskId(): string {
this.rootTaskId ??= this.calculateRootTaskId();
return this.rootTaskId;
}
calculateRootTaskId(): string {
let item = this.items[0];
if(!item)
return null;
while(item.parent && item.task)
item = item.parent;
return item.children[0].task.parentId;
}
public getTaskMinStart(): Date {
let min = this.owner.dataRange.start;
this.tasks.items.forEach(t => {
if(t.isValid() && t.start.getTime() < min.getTime())
min = t.start;
});
return min;
}
public getTaskMaxEnd(): Date {
let max = this.owner.dataRange.end;
this.tasks.items.forEach(t => {
if(t.isValid() && t.end.getTime() > max.getTime())
max = t.end;
});
return max;
}
public processServerInsertedKey(oldKey: string, newKey: string, type: string): void {
if(type === GanttDataObjectNames.task)
this.tasks.invalidate();
if(type === GanttDataObjectNames.dependency) {
this.dependencies.invalidate();
this.updateVisibleItemDependencies();
}
if(type === GanttDataObjectNames.resource)
this.resources.invalidate();
if(type === GanttDataObjectNames.resourceAssignment)
this.assignments.invalidate();
this.owner?.updateHistoryObsoleteInsertedKey(oldKey, newKey, type);
}
public onBeginDataObjectCreate(): void { this.owner.lockUpdateWithReload?.(); }
public onEndDataObjectCreate(): void { this.owner.unlockUpdateWithReload?.(); }
}