Planning out the New Functionality
When the PlaceGroup() method inserts a group, it places the center of the group at the target location. For the purposes of this lesson, you’re going to specify this location as being a fixed displacement from the center of the room containing the original group. You’ll end up with a new group at the right location in the target room by setting its displacement to the difference in position between the source and target rooms.

A big part of this process will be to find the room containing a particular group. For the sake of simplicity, you will assume that the selected group is completely enclosed by a room: it should not span two or more rooms. You will then look for the room that contains the group’s center point. You will calculate the center point of that room and the copied group’s target location at a fixed displacement from it. Finally, you will place the copy of the selected group at this location.
Here are the tasks you implemented in the previous lessons:
- Prompt the user to select the group to be copied
- Prompt the user to select the target location for this copy
- Place the copy of the group at the target location
To implement the proposed enhancements, there are new tasks (in bold) to be added to the list above. Task b in the above list is no longer needed, as the group will now be placed using different logic.
- Prompt the user to select the group to be copied
- Calculate the center of the selected group
- Find the room that contains the center of the group
- Calculate the center of the room
- Display the x, y and z coordinate of the center of the room in a dialog box
- Calculate the target group location based on the room’s center
- Place the copy of the group at the target location (some modification needed)
As you can see from the above list, you’re going to need to calculate the center point for both the Group and the Room objects. To do so, you will calculate the center point of their respective bounding boxes. As an example, in the following picture the dashed blue line is the bounding box of a selected group, while the blue dot in the middle is its center point.
Both the Group and the Room classes provide a BoundingBox property. They do so because they are derived from the Element class, and the Element class has this property. The Group and Room classes are said to inherit this property from their parent class. Because of this shared ancestry, you can implement a function which works for both the Group and the Room objects (and for any Element object, for that matter).
An Element’s BoundingBox property returns a BoundingBoxXYZ object containing the minimum and maximum points of its geometry. You can calculate the center point from these two points by taking the point halfway between them.
To get the room that contains the selected group, you first need to get all rooms in the model and then go through each one, checking whether it contains the selected group’s center point. You will calculate the center point of the room containing the group in the same way as you did for the group itself.
A Closer Look at the Code
Let's now take a closer look at the code. You'll start by looking at your new methods, before looking at the changes to the command's Execute() method.
You defined the GetElementCenter() method as follows:
public XYZ GetElementCenter(Element elem)
{
BoundingBoxXYZ bounding = elem.get_BoundingBox(null);
XYZ center = (bounding.Max + bounding.Min) * 0.5;
return center;
}
In the implementation of the GetElementCenter() method, you started by accessing the BoundingBox property of the Element passed in, storing its value in a variable named bounding.
BoundingBoxXYZ bounding = elem.get_BoundingBox(null);
The BoundingBox property is slightly unusual in that it takes a parameter: the view for which the bounding box is to be calculated. If this parameter is null, the property returns the bounding box of the model geometry. If a property of a class takes one or more parameters, the get_ prefix is needed before the property name to read the property value. This prefix isn't needed if the property doesn't take any parameters: you can just use the property name.
The returned BoundingBoxXYZ contains the coordinates of the minimum and maximum extents of the Element's geometry. The center point is calculated by taking the average (or mid-point) of these two points. For the sake of clarity, you stored this in another variable named center.
XYZ center = (bounding.Max + bounding.Min) * 0.5;
Finally, you returned this center point from the function.
You defined GetRoomOfGroup() method as follow:
Room GetRoomOfGroup(Document doc, XYZ point)
{
FilteredElementCollector collector =
new FilteredElementCollector(doc);
collector.OfCategory(BuiltInCategory.OST_Rooms);
Room room = null;
foreach (Element elem in collector)
{
room = elem as Room;
if (room != null)
{
// Decide if this point is in the picked room
if (room.IsPointInRoom(point))
{
break;
}
}
}
return room;
}Let's now take a closer look at the implementation of the GetRoomOfGroup() method. In this method, you started by retrieving all the rooms in the document, going through them to find the room that contains the group. The FilteredElementCollector class helped you with this task: it collects elements of a certain type from the document provided. That's why you needed to pass a document parameter to the GetRoomOfGroup() method, so it can be used there.
FilteredElementCollector collector = new FilteredElementCollector(doc);
The collector object is now used to filter the elements in the document. In the next step you added a filter requesting that only rooms be collected.
collector.OfCategory(BuiltInCategory.OST_Rooms);
You added your category filter to the collector using the OfCategory() method. Once the filter was applied, the collector only provided access to rooms. The FilteredElementCollector class provides several methods to add filters (and multiple methods can be applied at the same time for more complex requirements). More information on adding filters is provided later in this lesson in the Additional Topics section.
You then iterated through each room in the collector using a foreach expression. The code between the braces is repeatedly executed on each of the elements found by the collector. While you know these elements will be rooms, at this stage you accessed them as generic elements, as that's how the FilteredElementCollector provides access to them.
foreach (Element elem in collector)
{
//code between braces pair executed repetitively.
}The elem variable represents the current element in the collector. So when the code in the body of the foreach statement gets executed for the first time, the elem variable contains the first room. When the code in the body of the foreach statement is executed again, this time the elem variable contains the second room. And so on until you have executed the code against each of the rooms collected from the document.
As mentioned before, the collector provides you with access to each element it contains an instance of the generic Element class. As you know your elements will actually be instances of the Room class, you needed to cast them to that type before being able to access the functionality you needed from them (i.e. the IsPointInRoom() method).
room = elem as Room;
As you may recall, the as keyword first checks the actual type of the object before performing the type conversion: if the object is not of type Room, the variable will be set to null. Even though you fully expected the collector only to return rooms, it's still good practice to double-check that the room variable contains a valid room, just in case.
if (room != null)
The above if statement performs a conditional operation. If the condition provided between the brackets evaluates to true, the subsequent code block gets executed. An optional else clause can be used to execute different code when the condition evaluates to false (although this particular statement does not have one). The if statement is a very important programming concept, so we have provided more information on its use in the Additional Topics section.
As implied by its name, the IsPointInRoom() method judged whether the specified point is inside the boundaries of the room. If it was inside, the method returned true, otherwise false. You called IsPointInRoom() on each room: as soon as it returned true for a particular room, you knew you had found the one that contained your point and so you did not need to check the others. You then used a break statement to escape the iteration, even though there may well have been rooms that had not yet been checked. The break statement stops execution of code in the enclosing loop (in this case the foreach) and starts executing the code following it.
if (room.IsPointInRoom(point))
{
break;
}On completion of the loop, the room variable either contains the room in which the point was found – if IsPointInRoom() succeeded for it – or the last room in the list of rooms, otherwise. In either case, the contents of this variable gets returned as the result of the GetRoomOfGroup() method.
return room;
You defined GetRoomCenter() method as follows:
public XYZ GetRoomCenter(Room room)
{
// Get the room center point.
XYZ boundCenter = GetElementCenter(room);
LocationPoint locPt = (LocationPoint)room.Location;
XYZ roomCenter =
new XYZ(boundCenter.X, boundCenter.Y, locPt.Point.Z);
return roomCenter;
}Let's now take a closer look at the implementation of the GetRoomCenter() method. This function is internally going to make use – once again – of your GetElementCenter() method, modifying the returned point to make it at the same level as the floor of the room.
The first line of actual code in the implementation of the GetRoomCenter() stores the center point of your Room's geometry in a variable, named boundCenter:
// Get the room center point.
XYZ boundCenter = GetElementCenter(room);In order to make sure this point is adjusted to be at the elevation of the Room, you needed to access the Room's location point. The location point of a Room is always on its floor, so you determined its elevation by checking the location's Z coordinate.
The location of a particular room is at the intersection of the lines of the cross displayed by Revit when the room is selected.

You accessed this point via the Room's Location property. As a Room is located at a particular point, the value returned from the Location property is actually a LocationPoint object. But the Location property – as it is shared with other Elements – returns a more generic Location object. To get access to the point information stored in the property, you therefore needed to cast its value to be a LocationPoint. You stored the result of type conversion – i.e. the room's location – in a variable of type LocationPoint.
LocationPoint locPt = (LocationPoint)room.Location;
This cast does not use the as keyword, as you saw before. The as keyword checks the type of an object before casting it to the target type. The above code does not perform this check and is appropriate for cases where you know the underlying type of the object. As you know that instances of the Room class always return a LocationPoint from their Location property, it's quite safe to use it here. If the property somehow could not be treated as a LocationPoint (hypothetically speaking), this operation would cause an InvalidCastException to be thrown.
To get the modified point to return, you took the X and Y coordinates from your Room's center point and the Z coordinate from its LocationPoint and used these three values to create a new XYZ point.
XYZ roomCenter =
new XYZ(boundCenter.X, boundCenter.Y, locPt.Point.Z);Finally, you returned the adjusted point from the GetRoomCenter() method.
That's it for your new methods. Let's now take a look at the code you inserted into your command's Execute() method.
Now that you have your methods in place to do the actual calculations, it's a fairly simple matter of calling them, one after the other. You can see a progression in the data that is effectively passed from one to the other.
// Get the group's center point
XYZ origin = GetElementCenter(group);
// Get the room that the picked group is located in
Room room = GetRoomOfGroup(doc, origin);
// Get the room's center point
XYZ sourceCenter = GetRoomCenter(room);You started by getting the center of your group using GetElementCenter(), placing it in the origin variable of type XYZ. This was passed into the GetRoomOfGroup() method – along with the active document – to help with the room collection process, which returned the containing Room object to be stored in the room variable. This was then passed to the GetRoomCenter() method to determine the center of this room, which was stored in the sourceCenter variable.
The sourceCenter variable of type XYZ – containing the center of the Room where the Group selected by the user resides – then needed to be displayed to the user in a dialog box.
Each coordinate – X, Y and Z – of the XYZ class is of type double. This is the type you use to represent a double-precision, floating-point number. It's not important to worry about what the terms in italics actually mean: suffice it to say that a variable of type double uses 64-bits of data to hold an extremely accurate decimal number (to an accuracy of 15-16 digits).
To show these values in a dialog box, they need to be converted to a string. sourceCenter.X returns the point's X coordinate as a double, which can then be converted to a string type using the ToString() method. The + operator concatenates (joins together) two strings. The "\r\n" string represents a line break (it's a combination of a carriage return (\r) followed by a linefeed (\n), two characters that hark back to the days of the typewriter). This allowed you to split your string across multiple lines in the dialog box.
string coords =
"X = " + sourceCenter.X.ToString() + "\r\n" +
"Y = " + sourceCenter.Y.ToString() + "\r\n" +
"Z = " + sourceCenter.Z.ToString();The TaskDialog class allows you to display a standardized dialog box inside the Revit user interface. It's possible to add code to customize the dialog's controls prior to its display, but in this example you just used the Show() method to display your message. The first parameter is the dialog's title and the second is the main message.
TaskDialog.Show("Source room Center", coords);This wraps up the coding part of Lesson 6, before you move on to the next lesson, we will discuss a couple topics more in depth: Filtering with FilteredElementCollector and the if Statement.