Here is the details of the vessel shape generation script.
First of all, the imports
# Imports
import clr
clr.AddReference('System')
import System, math
from System import Array, Byte, Convert, BitConverter
from System.IO import StreamWriter, MemoryStream, SeekOrigin,BinaryWriter
from Spotfire.Dxp.Data.Import import StdfFileDataSource
from Spotfire.Dxp.Data import DataTableSaveSettings, DataValueCursor
Then define the parameters and data value cursors in preparation for reading. These can be hardcoded in the script or pass in from the script parameters. The Front, Back, Left and Right columns are the distance in meters Longitude and Latitude location to the edges of vessel. They specifie the size of the vessel and are optional. You may use Length and Width instead depending on the your data. You may also use constant length and width values across all vessels in your dataset.
# Define table name and column name parameters
VesselDataTableName = "VESSELS"
IdColName = "VESSEL_ID"
LatColName = "LAT"
LonColName = "LON"
HeadingColName = "HEADING"
FrontColName = "DIM_A"
BackColName = "DIM_B"
LeftColName = "DIM_C"
RightColName = "DIM_D"
# Prepare data value cursors for reading
vesselDataTable = Document.Data.Tables[VesselDataTableName]
idCursor = DataValueCursor.CreateFormatted(vesselDataTable.Columns[IdColName])
latCursor = DataValueCursor.CreateNumeric(vesselDataTable.Columns[LatColName])
lonCursor = DataValueCursor.CreateNumeric(vesselDataTable.Columns[LonColName])
frontCursor = DataValueCursor.CreateNumeric(vesselDataTable.Columns[FrontColName])
backCursor = DataValueCursor.CreateNumeric(vesselDataTable.Columns[BackColName])
leftCursor = DataValueCursor.CreateNumeric(vesselDataTable.Columns[LeftColName])
rightCursor = DataValueCursor.CreateNumeric(vesselDataTable.Columns[RightColName])
headingCursor = DataValueCursor.CreateNumeric(vesselDataTable.Columns[HeadingColName])
Before data reading, initialise the sourceString variable to store all as a STDF format. This already includes the necessary metadata configuration for the geometry column to be used on the Map Chart. This is no need to perform additional configuration as described here.
# Prepares STDF file header. This already includes the necessary metadata configuration for
# the geometry column to be used on the Map Chart.
sourceString = "\! filetype=Spotfire.DataFormat.Text; version=2.0;\r\n"
sourceString +="\! property=mapchart.columntypeid; category=Column; type=String;\r\n"
sourceString +="\! property=ContentType; category=Column; type=String;\r\n"
sourceString +="\! property=Name; category=Column; type=String;\r\n"
sourceString +="\! property=DataType; category=Column; type=String;\r\n"
sourceString +="\?;Geometry;XCenter;XMax;XMin;YCenter;YMax;YMin;\r\n"
sourceString +="\?;application/x-wkb;application/x-wkb;application/x-wkb;application/x-wkb;application/x-wkb;application/x-wkb;application/x-wkb;\r\n"
sourceString +=IdColName+";geometry;XCenter;XMax;XMin;YCenter;YMax;YMin;\r\n"
sourceString +="String;Binary;Double;Double;Double;Double;Double;Double;\r\n"
Then reads vessel data by row, generate the shape as WKB and add it along with other information to the sourceString variable. We will go into the details of the drawVesselWKB() function later on but the result is a Base64 encoded string of the vessel shape in WKB polygon. As mentioned previously, the front, back, left and right values are optional and you can use constant values, for example, front = 120, back = 40, left = 15 and right = 15 if you wish.
for row in vesselDataTable.GetRows(idCursor,latCursor,lonCursor,frontCursor,backCursor,leftCursor,rightCursor,headingCursor):
rowIndex = row.Index
id = idCursor.CurrentValue
lat = latCursor.CurrentValue
lon = lonCursor.CurrentValue
front = frontCursor.CurrentValue
back = backCursor.CurrentValue
left = leftCursor.CurrentValue
right = rightCursor.CurrentValue
heading = headingCursor.CurrentValue
wkb = drawVesselWKB(lon, lat, heading, front, back, left, right)
vesselString = id+";\#"+wkb+";"+str(lon)+";"+str(lon)+";"+str(lon)+";"+str(lat)+";"+str(lat)+";"+str(lat)+";\r\n"
sourceString += vesselString
Finally writes the sourceString as a separate data table (with the "_Geo" suffix which can be changed) using the StdfFileDataSource. Replaces the data table if one already exists.
# make a stream from the string
stream = MemoryStream()
writer = StreamWriter(stream)
writer.Write(sourceString)
writer.Flush()
stream.Seek(0, SeekOrigin.Begin)
dataSource = StdfFileDataSource(stream)
# add the data into a Data Table in Spotfire
geoDataTableName = VesselDataTableName+"_Geo"
if Document.Data.Tables.Contains(geoDataTableName):
Document.Data.Tables[geoDataTableName].ReplaceData(dataSource)
else:
newTable = Document.Data.Tables.Add(geoDataTableName, dataSource)
tableSettings = DataTableSaveSettings (newTable, False, False)
Document.Data.SaveSettings.DataTableSettings.Add(tableSettings)
DrawVesselWKB Function Details
The vessel shape is made up of a rectangle to represent the body and a triangle to represent the head. Five points p1 to p5 are to be generated as illustrated in the diagram below. These points are traversed from the origin point which is specified by the lon and lat parameters at an angle specifed by the heading parameter with the vessel size specified by the front, back, left and right parameters. The headLength is set as 1/4 of total length.
The function to traverse from one point to another point using an angle in degrees and distance in meters is translate().
# Translate source coordinates by angle in degrees and distance in meters
# This returns the new coordinates in longitude and latitude degrees
def translate(source, angle, distance):
sourceLon = source[0]
sourceLat = source[1]
distanceInLon = distance * math.sin(math.radians(angle))
distanceInLat = distance * math.cos(math.radians(angle))
# constant ratio calculated by (pi/180) * earth_radius
metersPerDegree = 111320.0
# Calculate new Lon Lat coordinates based on distances in meters
# https://stackoverflow.com/questions/7477003/calculating-new-longitude-latitude-from-old-n-meters
newLon = sourceLon + (distanceInLon / metersPerDegree) / math.cos(sourceLat * math.pi/180)
newLat = sourceLat + (distanceInLat / metersPerDegree)
return newLon, newLat
The implementation of the DrawVesselWKB() function is listed as below. Note that to draw a polygon, the end point needs to be the same as the starting point, therefore 6 points are required to draw a polygon of 5 points. Change this function accordingly to your desired vessel shape. You may create more complex shapes with polygons and multi-polygons as specified in WKB. Here are some references
https://markyourfootsteps.wordpress.com/tag/wkb/
https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry
Make sure the WKB header (e.g. no of parts and no of points) is changed accordingly to align with the coordinates being included in the WKB.
# Renders a vessel based on Lat Lon coordiantes, heading and size
# This returns the WKB representation (encoded in Base64) of the vessel
# shape which is made up of a rectangle (3/4 of length) and
# a triangle (1/4 of length)
def drawVesselWKB(lon, lat, heading, front, back, left, right):
# Starts a binary stream
stream = MemoryStream()
writer = BinaryWriter(stream)
# Writes the WKB header for a filled polygon with one part
# and 6 points.
# +--------+---------+--------+--------+
# Description : | Byte | Type | No of | No of |
# | Order | Polygon | Parts | Points |
# +--------+---------+--------+--------+
# Size : | 1-Byte | 4-byte | 4-byte | 4-byte |
# +--------+---------+--------+--------+
# Value : | 1 | 3000 | 1000 | 6000 |
# +--------+---------+--------+--------+
writer.Write(Array[Byte]([1,3,0,0,0,1,0,0,0,6,0,0,0]))
headLength = (front+back) / 4.0 * 1.0
# Generate each of five points of the vessel shape
origin = lon, lat
p1 = translate(translate(origin, heading+90.0, right), heading, front - headLength)
p2 = translate(translate(p1, heading, headLength), heading-90.0, (left+right)/2)
p3 = translate(p1, heading-90.0, left+right)
p4 = translate(p3, heading+180.0, front-headLength+back)
p5 = translate(p4, heading+90.0, left+right)
# Write to the binary stream of the generated five points and then back to the
# starting point to close the polygon.
writer.Write(BitConverter.GetBytes(p1[0]))
writer.Write(BitConverter.GetBytes(p1[1]))
writer.Write(BitConverter.GetBytes(p2[0]))
writer.Write(BitConverter.GetBytes(p2[1]))
writer.Write(BitConverter.GetBytes(p3[0]))
writer.Write(BitConverter.GetBytes(p3[1]))
writer.Write(BitConverter.GetBytes(p4[0]))
writer.Write(BitConverter.GetBytes(p4[1]))
writer.Write(BitConverter.GetBytes(p5[0]))
writer.Write(BitConverter.GetBytes(p5[1]))
writer.Write(BitConverter.GetBytes(p1[0]))
writer.Write(BitConverter.GetBytes(p1[1]))
writer.Flush()
# Return as Base64 string
return Convert.ToBase64String(stream.ToArray())
Map Chart Configurations
Last but not least, this part suggests the configuration required on the map chart.
You may include both the Marker layer (with standard marker shapes) and Feature layer (with rendered vessel shapes) and leverage the "Zoom Visbility" feature on the Map Chart as shown below. This allows markers to be shown at a high zoom level and the vessel shapes to be shown at a lower zoom level.