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

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