InfoNet and InfoWorks is renound for innaccessibility of data. Business critical asset data is often captured in models, and then never shared with the outside world. Typically the only way to share data with the outside world is via manual exports to a GIS format, and interpreting the data through other system.
This needs to change and a REST API for InfoNet and InfoWorks is an ideal candidate for connectivity of this data.
Many businesses are moving to Cloud-based services for a multitude of reasons. Some of the main reasons I have seen over the past few years are:
- Collaborative working
- Connectivity of systems
- Work from anywhere without need for installing software
- On server processing (potentially faster)
- Security (no physical storage = lower probability of data leaks) e.g. lost laptops won't cause data leaks.
- Lower costs
Although I generally disagree with cloud computing, I can understand why this looks appealing to business leads. Regardless of my personal opinion, cloud computing is likely the future for business systems, so we should get used to it and prepare accordingly.
My specific REST Implementation was mainly designed to allow quick injection of IExchange API.
You can make a very simple web server as follows:
require 'webrick'
server = WEBrick::HTTPServer.new :Port => 8000
trap 'INT' do
server.shutdown
end
server.mount_proc '/' do |request, response|
response.body = request.path
end
server.start
A few examples of URLs and their responses are given below:
http://localserver:8000/ : /
http://localserver:8000/hello/ : /hello
http://localserver:8000/world/ : /world
http://localserver:8000/a/b/c : /a/b/c
As you can see we can obtain the whole path supplied to the local server by looking in request.path, for this reason we could envisage a system where we supply our base classes and call from there, e.g.
http://localserver:8000/WSApplication/current_network/row_objects/hw_nodes #Get nodes
http://localserver:8000/WSApplication/current_network/row_objects/hw_conduit #Get pipes
Equivalent to:
WSApplication.current_network.row_objects("hw_nodes")
WSApplication.current_network.row_objects("hw_conduit")
In order to do this we need to keep a "stack" and pop names off the stack, interprate them and change the base object accordingly:
path = "/WSApplication/current_network/row_objects/hw_nodes"
parts = path.split("/") #=> ["","WSApplication","current_network","row_objects","hw_nodes"]
parts.shift() #=> ["WSApplication","current_network","row_objects","hw_nodes"]
obj = nil
roots = {
"WSApplication" => WSApplication
}
#Loop over all url parts
obj=nil
while part = parts.shift()
if !obj
#lookup first element in roots map
obj = roots[part]
else
#Ensure object supports the method given
if obj.respond_to?(part.to_sym)
#Understand if this method has parameters (think row_objects() which requires table name)
paramCount = obj.method(part.to_sym).parameters.length
if paramCount > 0
#Ensure params are within parts
if paramCount >= parts.length
#Populate arguments array
args = []
paramCount.times do
args.push(parts.shift())
end
#Call object method with arguments and set return value to obj
obj = obj.public_send(part.to_sym,*args)
else
raise "Not enough parameters passed"
end
end
else
#Error that the method doesn't exist
raise "Method doesn't exist"
end
end
wend
#At this point `obj` is ready to be returned to the body.
At this point obj is ready to be returned, but how should we return the data? Realistically we'd want row_objects to be returned as geoJSON so we can render it on a GIS control e.g. Leaflet.
We can do this via implementing the to_json
method ontop of the JSON library.
response.status = 200
response['Content-Type'] = 'text/json'
response.body = obj.to_json
Extend WSRowObject
and WSRowObjectCollection
with to_json
method to provide serialization capability.
class WSOpenNetwork
def to_json
return self.tables.select do |table|
self.row_object_collection(table.name).length > 0
end.map do |table|
next {"table" => table.name, "description" => table.description, "rowCount" => self.row_object_collection(table.name).length}
end
end
end
class WSRowObjectCollection
def to_json
return self.enum_for(:each).to_json
end
end
class WSRowObject
def to_json
#rowData template
rowData = {
"type" => "Feature",
"properties" => {},
"geometry": {}
}
#Populate all fields
self.table_info.fields.each do |field|
rowData["properties"][field.description] = self[field.name]
end
#Determine geometry type from supported fields
fields = self.table_info.fields.map {|f| f.name}
if fields.include?("x") && fields.include?("y")
rowData["geometry"]["type"] = "Point"
rowData["geometry"]["coordinates"] = [self.x,self.y]
elsif fields.include?("point_array")
rowData["geometry"]["type"] = "LineString"
rowData["geometry"]["coordinates"] = self.point_array
elsif fields.include?("boundary_array")
rowData["geometry"]["type"] = "Polygon"
rowData["geometry"]["coordinates"] = self.boundary_array
end
return rowData.to_json
end
end
And at this point as long as we wrap the above method to use WSApplication
and WSDatabase
we have a full REST API for InfoNet.
class WSOpenNetwork
def to_json
return self.tables.select do |table|
self.row_object_collection(table.name).length > 0
end.map do |table|
next {"table" => table.name, "description" => table.description, "rowCount" => self.row_object_collection(table.name).length}
end
end
end
class WSRowObjectCollection
def to_json
return self.enum_for(:each).to_json
end
end
class WSRowObject
def to_json
#rowData template
rowData = {
"type" => "Feature",
"properties" => {},
"geometry": {}
}
#Populate all fields
self.table_info.fields.each do |field|
rowData["properties"][field.description] = self[field.name]
end
#Determine geometry type from supported fields
fields = self.table_info.fields.map {|f| f.name}
if fields.include?("x") && fields.include?("y")
rowData["geometry"]["type"] = "Point"
rowData["geometry"]["coordinates"] = [self.x,self.y]
elsif fields.include?("point_array")
rowData["geometry"]["type"] = "LineString"
rowData["geometry"]["coordinates"] = self.point_array
elsif fields.include?("boundary_array")
rowData["geometry"]["type"] = "Polygon"
rowData["geometry"]["coordinates"] = self.boundary_array
end
return rowData.to_json
end
end
require_relative 'RESTServer'
server = RESTServer.new({:Port => 8000})
server.register("app", WSApplication)
server.register("db", WSDatabase)
server.start
At this point however, with full IExchange the language becomes very verbose:
http://localhost:8000/app/open/st1w444%3A4000%2FMainDatabase/object_by_type_and_id/Collection%20Network/40/open/hw_nodes
http://localhost:8000/app/open/st1w444%3A4000%2FMainDatabase/object_by_type_and_id/Collection%20Network/40/open/hw_conduit
A better approach might look like this:
http://localhost:8000/api/MainDatabase/networks/40/hw_nodes
http://localhost:8000/api/MainDatabase/networks/40/hw_conduit
Of course this is typically incredibly simple, we simply define the necessary class wrappers and shove them into the api methods.
## Final example