Working with Networks
A Network in this context refers to a WSModelObject
that contains tables and objects.
Obtaining a Network
To work with individual objects within a network, you need to access a WSOpenNetwork
instance. The mechanism for doing this is different between the UI and Exchange.
Within the UI:
network = WSApplication.current_network()
Within Exchange, you need to first access the WSNetworkObject
or WSNumbatNetworkObject
class:
database = WSApplication.open()
network_mo = database.model_object_from_type_and_id('Model Network', 2)
network = network.open
Accessing Row Objects
Objects within a network are called row objects, represented by the WSRowObject
class.
You can access objects specifically by type and ID:
node = network.row_object('wn_node', 'Badger')
Or you can obtain an array of objects:
nodes = network.row_objects('wn_node')
Or an array of selected objects:
nodes = network.row_objects_selection('wn_node')
Categories can be used to obtain the objects across multiple tables. The most common use of a category is to obtain all of the nodes or links in a network, regardless of the types of the individual nodes or links.
The categories are:
_nodes
- all nodes_links
- all links_subcatchments
- all subcatchments_other
- other objects
For example, to obtain an array of all nodes:
nodes = network.row_objects('_nodes')
Getting and Setting Values in Row Objects
Named Fields
Named fields are the fixed properties of each type of object, which will be familiar to users of the software. A field can contain different types of data:
- Primitive data types - strings, numbers, booleans, etc which can be accessed and set directly
- Arrays - some fields contain arrays of data, e.g. pipe bends
- Structured data - represented by a
WSStructure
class, which is used to access and set rows of data - Fake - not accessible to Ruby, but exist in the user interface to summarize other data
Flags are separate fields, i.e. a field node_id
also has a field node_id_flag
.
The real (database) name of a field may not match the interface name. The interface name is usually called the 'Description'.
Get / Set Methods
Getting and setting values can use the Array/Hash like [] and []= notation:
value = ro['field'] # Get value from an object field
ro['field'] = value # Set value of an object field
Or using method like notation:
value = ro.field # Get value from an object field
ro.field = value # Set value of an object field
Note that some fields are incompatible with the method like notation due to their name.
Nil Values
Fields can usually be set to nil
which is the equivalent of being empty in the user interface, or NULL
in SQL. Like SQL, some fields may contain an empty string value, which is not the same as nil
.
Unlike SQL, nil
values cannot be safely ignored. For example, this SQL script finds and selects pipes with a length less than or equal to 200:
DESELECT ALL;
SELECT WHERE length <= 200
An equivalent in Ruby:
network.clear_selection
network.row_objects('_links').each do |ro|
ro.selected = true if ro['length'] <= 200
end
But if a pipe had no length value, this would raise a runtime error because nil
cannot be compared to 200
. You would catch this by checking the length is not nil
first:
network.clear_selection
network.row_objects('_links').each do |ro|
ro.selected = true if (!ro['length'].nil? && ro['length'] <= 200)
end
Structure Blobs
A structure blob (or struct) is a field that contains structured rows of other data. In Ruby, it is represented by a WSStructure
object which can be iterated over, with each element being a WSStructureRow
containing named fields.
If we wanted to check what the first value of depth volume curve is, we could save the structure to a variable, then the first row (index 0), and access the field 'volume':
depth_struct = res.depth_volume
depth_struct_row = depth_struct[0]
puts depth_struct_row['volume']
Or we could write this as one line:
puts res.depth_volume[0]['volume']
We can write data to structs, though we have to be sure that there is already space using the size=
method. Changes must be saved by using the WSStructure.write
method, as well as WSRowObject.write
on the object it belongs to.
depth_struct = res.depth_volume
depth_struct[0]['volume'] = 100
depth_struct.write
res.write
Writing Changes
Changes to an object must be explicitly written to the WSRowObject
using the WSRowObject.write
method, which can only be done within a network transaction.
network = WSApplication.current_network
network.clear_selection
network.transaction_begin
network.row_objects('cams_cctv_survey').each do |ro|
ro['user_number_1'] = ro['surveyed_length'] / ro['total_length']
ro['user_number_2'] = ro['total_length'] / ro['pipe_length']
ro.write
end
network.transaction_commit
Setting the value of a field requires that value to be cast to a native InfoWorks type, and so it has to fit a strict criteria for the field: the correct type, length, etc.
Tags
Tags are temporary values added to row objects for the duration of the script. They can be used for storing working values against a specific object, usually to aggregate or store them later, but are not saved and are lost when the script finishes.
The names of tags are not fixed but must begin with an underscore _
and can only contain the letters a-z (without accents), digits, and underscores. They can also contain capitalised A-Z letters, but this is against Ruby naming conventions.
Getting and setting tag values can use the Array/Hash like [] and []= notation:
value = ro['_tag_name'] # Get value of a tag
ro['_tag_name'] = value # Set value of a tag
Or using method like notation:
value = ro._tag_name # Get value of a tag
ro._tag_name = value # Set value of a tag
- Unlike changes to object fields, tags do not need to be explicitly written to the
WSRowObject
- There is no requirement that all values for a given tag name are the same, and any Ruby type is allowed
- While changes to object fields may be cached in the database, Ruby values are not, and so storing too much data could exceed the allowed memory allocation
Choosing Notation
Which style of get / set notation you use is personal preference, however you may wish to use different notation for fields and tags to avoid ambiguity. In this case, using Hash syntax for named fields and method syntax for tags may be preferable:
ro['field'] = value
ro._tag_name = value
Saving Changes
Network Transactions
Any change to objects within a network must be done within a transaction. The three relevant methods are:
WSOpenNetwork.transaction_begin
to begin a transactionWSOpenNetwork.transaction_rollback
to rollback any changes made since the transaction was startedWSOpenNetwork.transaction_commit
to commit the changes made in this transaction
At this point, the changes will have been committed to the local working copy of network, but the changes are not committed to the database.
Committing Changes
To commit changes to the database, you can use the WSNumbatNetworkObject.commit
method from Exchange. You must have the model object for the network, which can be obtained using the WSOpenNetwork.model_object
method.
The key differences in behavior between object fields and values, beyond that of the object field values having a life beyond the duration of the running of the script, are:
- Object field values must be explicitly written back to the local database for the network using the write method - since tags are not stored anywhere other than in working memory, the write method does not need to be called for them.
- Object field values can only be stored within an active 'transaction' (see below).
- Object field values are stored in the 'InfoWorks / InfoAsset' world. Any given field has a particular data type and, for string fields, a length. Any attempt to store values incompatible with the object's data type will fail. Tags, on the other hand, exist in the Ruby world and may therefore contain anything that can be stored in a Ruby variable. There is no requirement for all the values for different objects of the same tag to be of the same data type.
- Object field values may be cached in the database, allowing more objects and more data to be manipulated within a network than with tags, which always exist in memory. Using too many tags and storing too much data in them may cause the program's memory limit to be exceeded.
Flags are treated as being separate fields. Fields can, in general, be set to nil which is the equivalent of causing them to be blank in the user interface or setting them to NULL in SQL. NULL in SQL and nil in Ruby are essentially the same. Arrays e.g. of coordinates are returned as a Ruby array.
This example finds and selects pipes with width less than 200 or length less than 60 or, of course, both.
net=WSApplication.current_network
net.clear_selection
ro=net.row_objects('cams_pipe').each do |ro|
if (!ro.width.nil? && ro.width<200) || (!ro.length.nil? && ro.length<60)
ro.selected=true
end
end
This demonstrates a key difference between Ruby and SQL; in SQL it is safe to say width<200, the expression will ignore values which are NULL. In Ruby however, it is necessary to explicitly check for nil values, nil being the Ruby counterpart to NULL. If you fail to do this check a runtime error will be raised.
An equivalent way of writing the same script would be to use the [] notation as follows:
net=WSApplication.current_network
net.clear_selection
ro=net.row_objects('cams_pipe').each do |ro|
if (!ro['width'].nil? && ro['width']<200) || (!ro['length'].nil? && ro['length']<60)
ro.selected=true
end
end
In the rare cases where the field name begins with a digit or the _
character it is necessary to use the ro['fieldname']
form to access the value.
To set values it is necessary to:
- Set them within a transaction. Transactions are treated as a single unit for purposes of undo / redo. When run from the user interface, each transaction is treated as a single undo / redo step and appears in the menu as 'Scripted transaction'.
- Call the write method on the row object to explicitly put the values into the database. This is the equivalent in the user interface of finishing to edit an object, of which you might have changed a number of values.
This example sets a couple of users fields for CCTV surveys based on simple calculations performed on other fields:
net=WSApplication.current_network
net.clear_selection
net.transaction_begin
ro = net.row_objects('cams_cctv_survey').each do |ro|
ro['user_number_1'] = ro['surveyed_length'] / ro['total_length']
ro['user_number_2'] = ro['total_length'] / ro['pipe_length']
ro.write
end
net.transaction_commit
Since the parameter of the []
method is a Ruby string it can also be an expression. The following demonstrates this by storing the two values used on the right-hand side of the above expressions as string parameters, and building up the user field name as a string expression:
net=WSApplication.current*network
net.clear_selection
net.transaction_begin
expressions=[['surveyed_length','total_length'],['total_length','pipe_length']]
ro=net.row_objects('cams_cctv_survey').each do |ro|
(0...expressions.size).each do |i|
ro['user_number*'+(i+1).to_s] = ro[expressions[i][0]] / ro[expressions[i][1]]
ro.write
end
end
net.transaction_commit
Once the user has run a script such as the above, the changes will have been made to the local network as though the change had been made manually in the user interface, or via SQL or similar, the changes have NOT been committed to the master database. It IS possible to commit the network to the master database by adding a call to the commit method with a suitable comment as a parameter e.g.
net.commit 'set user fields'
Two users of tags, one simple and one more complex, are demonstrated below in the 'navigating between objects' section.
Various data fields in InfoWorks and InfoAsset are represented as 'structure blobs' - the field contains a number of 'rows' of values for each object which in some respects behave as though they are a sub-table - they have a number of named fields with values.
The structure blobs that are most common are the following:
- hyperlinks
- attachments
- material_details
- resource_details
Many tables contain a hyperlinks field. The following tables in asset networks contain one or more of the other three fields named above:
Navigating Between Objects
You can navigate between objects by physical connectivity (e.g. the upstream node, the downstream links) or conceptual connectivity (e.g. the surveys for an asset, the assets for a survey).
Nodes and Links
Nodes and links are instances of classes WSNode
and WSLink
respectively. The nodes have the methods us_links
and ds_links
, and the links have methods us_node
and ds_node
.
This code clears the selection, selects a node, iteratively selects its upstream links, their upstream nodes, then their upstream links etc.
network = WSApplication.current_network
network.clear_selection
ro = network.row_object('cams_manhole', 'MH354671')
ro.selected=true
ro._seen=true
unprocessedLinks = []
ro.us_links.each do |l|
if !l._seen
unprocessedLinks << l
l._seen=true
end
end
while unprocessedLinks.size > 0
working = unprocessedLinks.shift
working.selected = true
workingUSNode = working.us_node
if !workingUSNode.nil? && !workingUSNode._seen
workingUSNode.selected = true
workingUSNode.us_links.each do |l|
if !l._seen
unprocessedLinks << l
l.selected = true
l._seen = true
end
end
end
end
As well as demonstrating use of the us_links method of WSNode and the us_node method of WSLink, this demonstrate some other useful techniques:
- As with the examples listing the
WSModelObject
objects in a database, this demonstrates the use of a breadth first search - we add the upstream links of the node to an array, then work through the array from the front, taking the links from it, selecting them, then if they have an upstream node, getting the upstream links of that node and adding them to the back of the array. In this case we are using the shift method of the Ruby array, which returns the first item in the array, removing it from the array. - Unlike the navigation of the database, where the objects are in a simple tree structure, networks can contain loops, therefore you will typically need to make sure that you only process any given node or link once, otherwise your script will keep revisiting the same objects over and over again. We do this by use of a tag which we have named
_seen
. Whenever we process a node or link we set the value of the_seen
tag to true, and we ensure that we don't process nodes or links if they have got the tag set to true, signifying that they have already been processed.
General
The more general way of navigating between objects is to use the WSRowObject.navigate
(one-to-one, one-to-many) and WSRowObject.navigate1
(one-to-one) methods.
The previous example may be rewritten using these methods as follows:
network = SApplication.current_network
network.clear_selection
ro = network.row_object('cams_manhole','MH354671')
ro.selected = true
ro._seen = true
unprocessedLinks = []
ro.navigate('us_links').each do |l|
if !l._seen
unprocessedLinks << l
l._seen = true
end
end
while unprocessedLinks.size > 0
working = unprocessedLinks.shift
working.selected = true
workingUSNode = working.navigate1('us_node')
if !workingUSNode.nil? && !workingUSNode._seen
workingUSNode.selected=true
workingUSNode.navigate('us_links').each do |l|
if !l._seen
unprocessedLinks << l
l.selected = true
l._seen = true
end
end
end
end
The only changes here are that calls to us_links
are replaced by calls to nagivate('us_links')
and the call to us_link
is replaced by a call to navigate1('us_link')
.
The navigate method however is much more versatile - this example navigates from CCTV surveys to pipes:
net=WSApplication.current_network
interesting_codes=['ABC','DEF','GHI','JKL','MNO']
net.transaction_begin
net.row_objects('cams_pipe').each do |ro|
(0...interesting_codes.size).each do |i|
ro['user_number_'+(i+1).to_s]=nil
end
ro.write
end
codes=Hash.new
net.row_objects('cams_cctv_survey').each do |ro|
ro.details.each do |d|
code=d.code
code_index=interesting_codes.index(code)
if !code_index.nil?
pipe=ro.navigate1('pipe')
if pipe
if pipe._defects.nil?
pipe._defects=Array.new(interesting_codes.size,0)
end
pipe._defects[code_index]+=1
end
end
end
end
net.row_objects('cams_pipe').each do |ro|
if !ro._defects.nil?
(0...interesting_codes.size).each do |i|
ro['user_number_'+(i+1).to_s]=ro._defects[i]
end
ro.write
end
end
net.transaction_commit
This clears user numbers 1 to 5 for all pipes, then iterates through all defects, counting the number of defects of 5 particular codes for each pipe, then stores those in user numbers 1 to 5.
Note the use of arrays stored in tags for temporary storage of counts.