Попробую объяснить, чем я закончил.
Я думаю, чтобы следовать, вам нужно немного узнать о требованиях. Этот интерактивный / настраиваемый график / график используется в качестве шаблона для оценки сроков производства.
Всего 3 штуки:
- Шаги
- мероприятия
- Продолжительность действий, которая различается в зависимости от типа объекта, к которому применяется расписание.
Поскольку это шаблон, используемый для оценки, изначально нет дат - только произвольная длительность, привязанная к действиям, сопоставленным с шагами. Однако в конечном итоге шаги сопоставляются с датами из импортированного отчета (или вводятся вручную).
Есть 3 страницы, на которых пользователь постепенно составляет расписание:
- Добавить / изменить шаги: шаги - это строки, которые создаются со значением порядка сортировки (предполагаемым).
- Добавление / редактирование действий: матрица с шагами в виде столбцов и действиями в виде строк. Каждый перекресток - это флажок. Для каждого действия необходимо выбрать начальный и конечный этапы.
- Добавить / изменить продолжительность: выбирается тип элемента, и продолжительность добавляется для каждого действия.
Классы
- Шаг [Имя, StepOrder, ..]
- Действие [Имя, StartStepID, StartStepOrder, EndStepID, EndStepOrder, ..]
- ActivityDuration: Activity [Duration, ItemType, ..]
В контроллере / репозитории MVC:
// start by ordering the durations by the order of the steps
var sortedActivities = durations
.OrderBy(x => x.StartStepOrder)
.ThenBy(x => x.EndStepOrder);
// create func to get the path name from the old + new activity
var getActivityPath = new Func<string, string, string>(
(prevPath, activityID) =>
{
return string.IsNullOrEmpty(prevPath)
? string.Format("{0}", activityID)
: string.Format("{0}.{1}", prevPath, activityID);
});
// create the recursive func we'll call to do all the work
Action<List<ActivityDuration>, string, long?, IEnumerable<ActivityDuration>> buildPaths = null;
buildPaths = (activities, path, startStepID, starts) =>
{
// activities will be null until we are joining gapped paths,
// so grab the activities with the provided startStepID
if (starts == null)
starts = activities.Where(x => x.StartStepID == startStepID);
// each activity represents a new branch in the path
foreach (var activity in starts)
{
var newPath = getActivityPath(path, activity.Id.ToString());
// add the new path and it's ordered collection
// of activities to the collection
if (string.IsNullOrEmpty(path))
{
paths.Add(newPath, new ActivityDuration[] { activity });
}
else
{
paths.Add(newPath, paths[path].Concat(new ActivityDuration[] { activity }));
}
// do this recursively providing the EndStepID as the new Start
buildPaths(activities, newPath, activity.EndStepID, null);
}
// if there were any new branches, remove the previous
// path from the collection
if (starts.Any() && !string.IsNullOrEmpty(path))
{
paths.Remove(path);
}
};
// since the activities are in step order, the first activity's
// StartStepID will be where all paths start.
var firstStepID = sortedActivities.FirstOrDefault().StartStepID;
// call the recursive function starting with the first step
buildPaths(sortedActivities.ToList(), null, firstStepID, null);
// handle gaps in the paths after all the first connected ones have been pathed.
// :: ie - step 1,2 & 4,5 are mapped, but not step 3
// these would be appended to the longest path with a step order < start step of the gapped activity's start step (!!!)
// :: ie - the path should be 1-2,2-4,4-5)
// because the list of paths can grow, we need to keep track of the count
// and loop until there are no more paths added
var beforeCount = paths.Count;
var afterCount = beforeCount + 1;
while (beforeCount < afterCount)
{
foreach (var path in paths.ToArray())
{
var lastActivity = path.Value.Last();
// check for activities that start after the last one in each path ..
// .. that don't start on another activity's end step (because that would be a part of a path already)
var gapped = sortedActivities
.Where(x => x.StartStepOrder > lastActivity.EndStepOrder)
.Where(thisAct =>
!sortedActivities
.Select(otherAct => otherAct.EndStepID)
.Contains(thisAct.StartStepID)
);
beforeCount = paths.Count;
// for each new gapped path, build paths as if it was specified by the previous activity
buildPaths(sortedActivities.ToList(), path.Key, null, gapped);
afterCount = paths.Count;
}
}
// build up an object that can be returned to
// JS with summations and ordering already done.
rValue = new ActivityPaths()
{
Paths = paths
.Select(x => new ActivityPath()
{
Path = x.Key,
ActivityDurations = x.Value,
TotalDuration = x.Value.Sum(y => y.Duration)
})
.OrderByDescending(x => x.TotalDuration)
};
По общему признанию, у этой конструкции есть некоторые недостатки, но сценарии использования это позволяют. В частности: - Действие не может напрямую иметь более одного зависимого шага - или, другими словами, 2 шага не могут иметь одинаковый порядок шагов. - Если 2 пути имеют одинаковую общую продолжительность, только один будет отображаться как критический.
Так как даты, которые сопоставлены с шагами, в конечном итоге используются для вычисления назад / вперед до конца пути с заданного момента времени, это нормально. После того, как все даты указаны, при необходимости можно рассчитать более точный критический путь.
Возвращается весь набор путей, так что некоторая проверка может быть реализована в javascript. Первый путь будет критическим "единичным", и этот путь будет выделен в пользовательском интерфейсе с отображением общей продолжительности критического пути:
person
JoeBrockhaus
schedule
22.01.2014