Я архитектор, долгое время проектировал здания и сооружения, но вот с лета прошлого года начал программировать на C# используя Revit API. У меня уже есть несколько модулей-надстроек для Revit и теперь я хочу поделиться некоторым опытом разработки для Revit. Предполагается, что читатели умеют писать макросы для Revit на C#.
Revit API не содержит методов для параллельных вычислений. Даже при попытке разместить объекты Revit API в параллельных потоках, возникнет ошибка времени выполнения программы. Поэтому я сейчас хочу показать как можно выполнять все таки параллельные вычисления, работая при этом с Revit API.
Представим следующую практическую задачу, которую выполним в параллельном и последовательном режиме. Выделим несколько сотен стен, найдем центр каждой стены в плане. После проверим расстояние между центрами стенам, и найдем две наиболее близко расположенные стены относительно их центров.
Если мы будем работать непосредственно с объектами Revit API (в данном случае с Wall) нам придется обрабатывать каждую стену последовательно, соответственно такие вычисления будут происходить в одном потоке. Попробуем выделить необходимые свойства стен, добавить их в собственные классы и провести необходимые вычисления уже с собственными классами, в нескольких потоках.
Сначала создадим макрос WallTesting. И теперь создадим класс, который будет получать необходимые для работы свойства объектов стен.
public class MyWall //Класс в который добавим необходимые параметры из элементов Wall
{
private LocationCurve wallLine; //Добавляем линию основание стены, она понадобится для вычислений
private XYZ p1 = new XYZ(); //Добавляем первую точку линии основания
private XYZ p2 = new XYZ(); //Добавляем вторую точку основания линии стены
public XYZ pCenter; //Добавляем среднюю точку стены, которую будем вычислять, но не задаем начальное значение
public MyWall (LocationCurve WallLine) //Конструктор пользовательского класса стены
{
this.wallLine = WallLine; //Делаем в конструкторе минимум работы - просто передаем нужные нам параметры для вычислений
}
public XYZ GetPointCenter () //Метод, вычисляющий среднюю точку стены
//Тут можно бы сделать кэширование срединной точки, и не вычислять ее - если точка известна, но для упрощения кода не будем делать этого.
{
p1 = wallLine.Curve.GetEndPoint(0);
p2 = wallLine.Curve.GetEndPoint(1);
return pCenter = new XYZ((p2.X + p1.X)/2, (p2.Y + p1.Y)/2, (p2.Z + p1.Z)/2);//Тут немножко вспоминаем векторную геометрию за 9 класс школы
}
public double GetLenght (XYZ x) //Метод, вычисляющий расстояние до предложенной ему средней точки другой стены
{
XYZ vector = new XYZ((pCenter.X - x.X), (pCenter.Y - x.Y), (pCenter.Z - x.Z)); //Находим вектор между средней точкой первой стены и второй стены
return Math.Sqrt ( Math.Pow( vector.X,2) + Math.Pow( vector.Y,2) + Math.Pow( vector.Z,2)); //Находим длину вектора между средними точками двух стен
}
}
В общем состав класса и его работа расписана в комментариях, можно теперь написать главный рабочий метод WallTesting.
public void WallTesting ()
{
UIDocument uidoc = this.ActiveUIDocument; //Получаем активный документ
Document doc = uidoc.Document; //Создаем документ
List<MyWall> wallList = new List<ThisApplication.MyWall>(); //Добавляем вспомогательные члены - список в который перенесем необходимые свойства из элементов Wall
List <double> minPoints = new List<double>(); //В этом списке будут храниться минимальные расстояния от каждой стены
Selection selection = uidoc.Selection; // получаем выделенные объекты
ICollection<ElementId> selectedIds = uidoc.Selection.GetElementIds(); //и помещаем их в коллекцию
DateTime end; //Далее проверим как будет работать наша вычисления в многопоточном режиме
DateTime start = DateTime.Now; //Засекаем время
foreach (ElementId e in selectedIds) // Помещаем необходимые свойства элемента Wall в свои объекты MyWall.
//Эту операцию нельзя выполнять в многопоточном режиме, так как мы пока работаем с объектами Revit API
{
Element el = doc.GetElement(e); //получаем элемент по его Id
Wall w = el as Wall; //Смотрим, стена ли это
if (w != null) //Если стена -
{
wallList.Add( new MyWall (w.Location as LocationCurve)); // Создаем объект MyWall и добавляем в его необходимые свойства из элемента Wall
}
}
System.Threading.Tasks.Parallel.For(0, wallList.Count, x => //Далее будем последовательно у каждого объекта MyWall сравнивать
//расстояние от средней точки до средней точки всех остальных объектов (стен). Запускаем задачу в параллельном режиме
{
List <double> allLenght = new List<double>(); //Это вспомогательный список
wallList[x].GetPointCenter(); //Находим срединную точку текущего объекта
foreach (MyWall nn in wallList) //проверяем расстояние до каждой срединной точки остальных объектов(стен)
{
double n = wallList[x].GetLenght( nn.GetPointCenter() );
if (n != 0) //Исключаем добавление в список текущего объекта
allLenght.Add(n); //И записываем все расстояния в этот вспомогательный список
}
allLenght.Sort(); //Сортируем вспомогательный список
minPoints.Add(allLenght[0]); //Добавляем наименьшее расстояние в соответствующий список
});//Заканчиваем задачу
minPoints.Sort(); //Сортируем все минимальные расстояния
double minPoint = minPoints[0]; //Берем самое маленькое расстояние между стенами
end = DateTime.Now; // Записываем текущее время
TimeSpan ts = (end - start);
TaskDialog.Show("Revit", "Минимальное расстояние между стенами - " + (minPoint*304.8).ToString() +
"nЗадача в параллельном режиме обрабатывалась - " + ts.TotalMilliseconds.ToString() + " миллисекунд");
}
Уже можно запустить макрос и посмотреть как он шустро находит минимальное расстояние между средними точками стен, но все же немного потерпим и добавим метод WorkWithWall, который будет работать с объектами Revit API со стенами (Wall) и будет обрабатывать их последовательно. Код не комментирую — в нем методы и параметры аналогичны приведенным выше.
void WorkWithWall(Document doc, ICollection<ElementId> selectedIds)
{
List<Wall> wallList = new List<Wall>();
List <double> minPoints = new List<double>();
DateTime end;
DateTime start = DateTime.Now;
foreach (ElementId e in selectedIds)
{
Element el = doc.GetElement(e);
Wall w = el as Wall;
if (w != null)
{
wallList.Add(w);
}
}
foreach (Wall w in wallList)
{
List <double> allLenght = new List<double>();
LocationCurve wallLine = w.Location as LocationCurve;
XYZ pCenter =
new XYZ((wallLine.Curve.GetEndPoint(1).X + wallLine.Curve.GetEndPoint(0).X)/2,
(wallLine.Curve.GetEndPoint(1).Y + wallLine.Curve.GetEndPoint(0).Y)/2,
(wallLine.Curve.GetEndPoint(1).Z + wallLine.Curve.GetEndPoint(0).Z)/2);
foreach (Wall w2 in wallList)
{
LocationCurve wallLine2 = w2.Location as LocationCurve;
XYZ pCenter2 =
new XYZ((wallLine2.Curve.GetEndPoint(1).X + wallLine2.Curve.GetEndPoint(0).X)/2,
(wallLine2.Curve.GetEndPoint(1).Y + wallLine2.Curve.GetEndPoint(0).Y)/2,
(wallLine2.Curve.GetEndPoint(1).Z + wallLine2.Curve.GetEndPoint(0).Z)/2);
XYZ vector = new XYZ((pCenter.X - pCenter2.X), (pCenter.Y - pCenter2.Y), (pCenter.Z - pCenter2.Z));
double lenght = Math.Sqrt ( Math.Pow( vector.X,2) + Math.Pow( vector.Y,2) + Math.Pow( vector.Z,2));
if (lenght !=0)
allLenght.Add(lenght);
}
allLenght.Sort();
minPoints.Add(allLenght[0]);
}
minPoints.Sort();
double minPoint = minPoints[0];
end = DateTime.Now;
TimeSpan ts = (end - start);
TaskDialog.Show("Revit", "Минимальное расстояние между стенами - " + (minPoint*304.8).ToString() +
"nЗадача в последовательном режиме обрабатывалась - " + ts.TotalMilliseconds.ToString() + " миллисекунд");
}
Добавим в конец рабочего метода WallTesting метод вот так:
WorkWithWall(doc, selectedIds);
Теперь работа завершена, остается создать примерно 1000 стен, запустить макрос у увидеть, как он работает. Разница в параллельной и последовательной работе будет в 3 раза в пользу первой. Я не делал обработчик исключения, на тот случай, если стены не выделены перед запуском макроса. Так, что не забудьте сначала выделить стены.
Резюме
1. Если вам надо обработать большое количество элементов, задумайтесь о многопоточных вычислениях. При малом количестве элементов накладные расходы будут больше извлекаемого ускорения работы в параллельном режиме.
2. Если вам нужны многопоточные вычисления, сначала создайте собственные классы, в которые передайте необходимые свойства элементов Revit API и методы работы с этими свойствами. Создайте список из собственных классов и обрабатывайте его в многопоточном режиме.
Не забудьте — вы можете кэшировать некоторые свойства и члены класса, и не вычислять их, если уже раннее это когда-то было сделано.
3. Не пытайтесь в методы, работающие с разными потоками, передать объекты Revit API, например Wall. У вас возникнет ошибка времени выполнения.
PS: Приложил файлы с примерами. Это файл "Test1000Wall.rvt" в котором находится 1000 стен с расстоянием друг от друга 1000мм (в осях). Справа сверху расстояние между стенами в осях 700мм. Файл "TestParallelWall.cs" это готовый макрос для тестов.
Автор: Akunets