docs: def getAsKmlAnimation(self, session, channelInputFile, path=None, documentName=None, styles={}):
          """
          Generate a KML visualization of the the link node dataset file.
  
          Link node dataset files are time stamped link node value datasets. This will yield a value for each stream node
          at each time step that output is written. The resulting KML visualization will be an animation.
  
          The stream nodes are represented by cylinders where the z dimension/elevation represents the values. A color
          ramp is applied to make different values stand out even more. The method attempts to identify an appropriate
          scale factor for the z dimension, but it can be set manually using the styles dictionary.
  
          Args:
              session (:mod:`sqlalchemy.orm.session.Session`): SQLAlchemy session object bound to PostGIS enabled database
              channelInputFile (:class:`gsshapy.orm.ChannelInputFile`): Channel input file object to be associated with
                  this link node dataset file.
              path (str, optional): Path to file where KML will be written. Defaults to None.
              documentName (str, optional): Name of the KML document. This will be the name that appears in the legend.
                  Defaults to the name of the link node dataset file.
              styles (dict, optional): Custom styles to apply to KML geometry. Defaults to empty dictionary.
  
                  Valid keys (styles) include:
                     * zScale (float): multiplier to apply to the values (z dimension)
                     * radius (float): radius in meters of the node cylinder
                     * colorRampEnum (:mod:`mapkit.ColorRampGenerator.ColorRampEnum` or dict): Use ColorRampEnum to select a default color ramp or a dictionary with keys 'colors' and 'interpolatedPoints' to specify a custom color ramp. The 'colors' key must be a list of RGB integer tuples (e.g.: (255, 0, 0)) and the 'interpolatedPoints' must be an integer representing the number of points to interpolate between each color given in the colors list.
  
          Returns:
              str: KML string
          """
          # Constants
          DECMIAL_DEGREE_METER = 0.00001
          OPTIMAL_Z_MAX = 300  # meters
  
          # Default styles
          radiusMeters = 2 * DECMIAL_DEGREE_METER  # 2 meters
          zScale = 1
          colorRamp = ColorRampGenerator.generateDefaultColorRamp(ColorRampEnum.COLOR_RAMP_HUE)
  
          # Validate
          if not documentName:
              documentName = self.name
  
          if 'zScale' in styles:
              try:
                  float(styles['zScale'])
                  zScale = styles['zScale']
  
              except ValueError:
                  log.warning('zScale must be a valid number representing z dimension multiplier.')
  
          if 'radius' in styles:
              try:
                  float(styles['radius'])
                  radiusMeters = styles['radius'] * DECMIAL_DEGREE_METER
  
              except ValueError:
                  log.warning('radius must be a number representing the radius of the value cylinders in meters.')
  
          if 'colorRampEnum' in styles:
              colorRampEnum = styles['colorRampEnum']
  
              if isinstance(colorRampEnum, dict):
                  colorRamp = ColorRampGenerator.generateCustomColorRamp(colorRampEnum['colors'], colorRampEnum['interpolatedPoints'])
              elif isinstance(colorRampEnum, int):
                  colorRamp = ColorRampGenerator.generateDefaultColorRamp(colorRampEnum)
  
          # Link to channel input file
          self.linkToChannelInputFile(session, channelInputFile)
  
          # Create instance of GeometryConverter
          converter = GeometryConverter(session)
  
          # Get LinkNodeTimeSteps
          linkNodeTimeSteps = self.timeSteps
  
          # Get date time parameters
          timeStepDelta = timedelta(minutes=self.timeStepInterval)
          startDateTime = datetime(1970, 1, 1)
          startTimeParts = self.startTime.split()
  
          # Calculate min and max values for the color ramp
          minValue = 0.0
          maxValue = session.query(func.max(NodeDataset.value)).\
                             filter(NodeDataset.linkNodeDatasetFile == self).\
                             filter(NodeDataset.status == 1).\
                             scalar()
  
          avgValue = session.query(func.avg(NodeDataset.value)).\
                             filter(NodeDataset.linkNodeDatasetFile == self).\
                             filter(NodeDataset.status == 1).\
                             scalar()
  
          # Calculate automatic zScale if not assigned
          if 'zScale' not in styles:
              zScale = OPTIMAL_Z_MAX / ((maxValue + avgValue) / 2)
  
          # Map color ramp to values
          mappedColorRamp = ColorRampGenerator.mapColorRampToValues(colorRamp, minValue, maxValue)
  
          if len(startTimeParts) > 5:
              # Default start date time to epoch
              startDateTime = datetime(year=int(startTimeParts[2]) or 1970,
                                       month=int(startTimeParts[1]) or 1,
                                       day=int(startTimeParts[0]) or 1,
                                       hour=int(startTimeParts[3]) or 0,
                                       minute=int(startTimeParts[4]) or 0)
  
          # Start the Kml Document
          kml = ET.Element('kml', xmlns='http://www.opengis.net/kml/2.2')
          document = ET.SubElement(kml, 'Document')
          docName = ET.SubElement(document, 'name')
          docName.text = documentName
  
          # Apply special style to hide legend items
          style = ET.SubElement(document, 'Style', id='check-hide-children')
          listStyle = ET.SubElement(style, 'ListStyle')
          listItemType = ET.SubElement(listStyle, 'listItemType')
          listItemType.text = 'checkHideChildren'
          styleUrl = ET.SubElement(document, 'styleUrl')
          styleUrl.text = '#check-hide-children'
  
          for linkNodeTimeStep in linkNodeTimeSteps:
              # Create current datetime objects
              timeSpanBegin = startDateTime + (linkNodeTimeStep.timeStep * timeStepDelta)
              timeSpanEnd = timeSpanBegin + timeStepDelta
  
              # Get Link Datasets
              linkDatasets = linkNodeTimeStep.linkDatasets
  
              for linkDataset in linkDatasets:
                  # Don't process special link datasets (with node counts of -1 or 0)
                  if linkDataset.numNodeDatasets <= 0:
                      break
  
                  # Get Node Datasets
                  nodeDatasets = linkDataset.nodeDatasets
  
                  for nodeDataset in nodeDatasets:
                      # Get node
                      node = nodeDataset.node
                      link = node.streamLink
                      extrude = nodeDataset.value
  
                      # Don't extrude below 0
                      if nodeDataset.value < 0.0:
                          extrude = 0.0
  
                      # Convert to circle
                      circleString = converter.getPointAsKmlCircle(tableName=node.tableName,
                                                                   radius=radiusMeters,
                                                                   extrude=extrude,
                                                                   zScaleFactor=zScale,
                                                                   geometryId=node.id)
  
                      # Convert alpha from 0.0-1.0 decimal to 00-FF string
                      integerAlpha = mappedColorRamp.getAlphaAsInteger()
  
                      # Get RGB color from color ramp and convert to KML hex ABGR string with alpha
                      integerRGB = mappedColorRamp.getColorForValue(nodeDataset.value)
  
                      # Make color ABGR string
                      colorString = '%02X%02X%02X%02X' % (integerAlpha,
                                                          integerRGB[mappedColorRamp.B],
                                                          integerRGB[mappedColorRamp.G],
                                                          integerRGB[mappedColorRamp.R])
  
                      # Create placemark
                      placemark = ET.SubElement(document, 'Placemark')
  
                      # Create style tag and setup styles
                      style = ET.SubElement(placemark, 'Style')
  
                      # Set polygon line style
                      lineStyle = ET.SubElement(style, 'LineStyle')
  
                      # Disable lines by setting line width to 0
                      lineWidth = ET.SubElement(lineStyle, 'width')
                      lineWidth.text = str(0)
  
                      # Set polygon fill color
                      polyStyle = ET.SubElement(style, 'PolyStyle')
                      polyColor = ET.SubElement(polyStyle, 'color')
                      polyColor.text = colorString
  
                      if len(linkNodeTimeSteps) > 1:
                          # Create TimeSpan tag
                          timeSpan = ET.SubElement(placemark, 'TimeSpan')
  
                          # Create begin and end tags
                          begin = ET.SubElement(timeSpan, 'begin')
                          begin.text = timeSpanBegin.strftime('%Y-%m-%dT%H:%M:%S')
                          end = ET.SubElement(timeSpan, 'end')
                          end.text = timeSpanEnd.strftime('%Y-%m-%dT%H:%M:%S')
  
                      # Append geometry
                      polygonCircle = ET.fromstring(circleString)
                      placemark.append(polygonCircle)
  
                      # Embed node data
                      nodeExtendedData = ET.SubElement(placemark, 'ExtendedData')
  
                      nodeNumberData = ET.SubElement(nodeExtendedData, 'Data', name='node_number')
                      nodeNumberValue = ET.SubElement(nodeNumberData, 'value')
                      nodeNumberValue.text = str(node.nodeNumber)
  
                      nodeLinkNumberData = ET.SubElement(nodeExtendedData, 'Data', name='link_number')
                      nodeLinkNumberValue = ET.SubElement(nodeLinkNumberData, 'value')
                      nodeLinkNumberValue.text = str(link.linkNumber)
  
                      nodeElevationData = ET.SubElement(nodeExtendedData, 'Data', name='value')
                      nodeElevationValue = ET.SubElement(nodeElevationData, 'value')
                      nodeElevationValue.text = str(nodeDataset.value)
  
  
          kmlString = ET.tostring(kml)
  
          if path:
              with open(path, 'w') as f:
                  f.write(kmlString)
  
          return kmlString