Imports ThinkGeo.Core
Imports ThinkGeo.UI.WinForms
Imports System.Collections.ObjectModel
Imports System.Text
Imports System.Text.RegularExpressions
Imports NetTopologySuite.Index.Quadtree
Imports NetTopologySuite.Geometries
Imports EO.Internal
''' <summary>
''' This class was written in order to implement Cayzu #9943. With the 
''' standard text/icon symbol you could specify formatting for numeric
''' values; whether or not to use thousands separators and how many decimal
''' places to show. But the formatting would not work if you used more than
''' one column from the layer in your text symbol. This class overrides protected
''' sub OnFormatting in order to intercept and format the text before it is
''' drawn to the screen. The the key fuctions that implement the formatting are
''' CreateFormatString and mParseColumnValues.
''' SET 11/15/2018
''' </summary>
<Serializable()>
Public Class CustomTextStyle
  Inherits TextStyle
  'Private Const mcsGetLiteralText As String = "\](.*?)\["
  'Private Const mcsGetBracketedText As String = "\[(.*?)\]"
  Public Structure StructColumnFormatProps
    Public Property Name As String
    Public Property NumDecimalPlaces As Integer
    Public Property UseThousandsSeparator As Boolean
    Public Sub Init()
      Name = String.Empty
      NumDecimalPlaces = -1
      UseThousandsSeparator = False
    End Sub
  End Structure

  Private Const mcsGetLiteralText As String = "\}(.*?)\{"
  Private Const mcsGetBracketedText As String = "\{(.*?)\}"

  Public FormatDictionary As New Dictionary(Of String, StructColumnFormatProps)
  Private msFormatString As String = String.Empty
  Private mbStartsWithLiteral As Boolean = True
  Private mbEndsWithLiteral As Boolean = True
  Private moLiteralTextVals As New List(Of String)
  Private mbIsUsingLabelExpression As Boolean = False
#Region "Stuff for label re-positioning"
  '***** Cayzu #6460
  '***** SET 09/27/2019
  Private moLabelCoords As New Dictionary(Of String, LabelPosition)
  Private moColumnValues As New Dictionary(Of String, String)
  Private msBaseKey As String = Guid.NewGuid.ToString
  <Serializable()>
  Private Class LabelPosition
    Public WorldPosition As Vertex
    Public XOffsetPixel As Single = 0
    Public YOffsetPixel As Single = 0
  End Class
  Public Enum EnumTextSymbolType
    Polygon = 1
    Line = 2
    Point = 3
  End Enum
  Public Property TextSymbolType As EnumTextSymbolType
  <NonSerialized()>
  Private moMap As CustomMapView
  Public Property Map As CustomMapView
    Get
      Return moMap
    End Get
    Set(value As CustomMapView)
      moMap = value
    End Set
  End Property
#End Region
  Public Sub New()
    MyBase.New()
    TextBrush = New GeoSolidBrush(GeoColors.Black)
    Font = New GeoFont("Century Gothic", 8)
  End Sub
  Public Sub New(sTextColumnName As String, oGeoFont As GeoFont, oBrush As GeoSolidBrush, oMap As CustomMapView, eTextSymbolType As EnumTextSymbolType)
    MyBase.New(sTextColumnName, oGeoFont, oBrush)
    moMap = oMap
    TextSymbolType = eTextSymbolType
  End Sub
  ''' <summary>
  ''' Override to intercept and format text.
  ''' </summary>
  ''' <param name="e"></param>
  Protected Overrides Sub OnFormatting(e As FormattingPositionStyleEventArgs)
    '***** Each time a label is drawn the text is formatted using
    '***** msFormatString in function mParseColumnValues.
    e.Text = String.Format(mParseColumnValues(e.Text))
    MyBase.OnFormatting(e)
  End Sub
  '***** Got this from the ThinkGeo forum, thought it might help with centering
  '***** text. It didn't.
  'Protected Overrides Function FormatCore(text As String, labeledShape As BaseShape) As String

  '  Dim tmpLabel As String = "  " & text
  '  Dim subTexts As String() = tmpLabel.Split(New String() {vbCrLf, vbLf}, StringSplitOptions.None)
  '  Dim maxLength As Integer = subTexts(0).Length

  '  For Each item In subTexts
  '    If maxLength < item.Length Then maxLength = item.Length
  '  Next

  '  Dim sb As StringBuilder = New StringBuilder()

  '  For i As Integer = 0 To subTexts.Length - 1
  '    sb.Append(subTexts(i))
  '    If i <> (subTexts.Length - 1) Then sb.AppendLine()
  '  Next

  '  text = sb.ToString()

  '  Return MyBase.FormatCore(text, labeledShape)
  'End Function
  ''' <summary>
  ''' Creates a format string and stores it in msFormatString. This is done
  ''' only once, when the style is created and then is used each time the
  ''' style is drawn.
  ''' </summary>
  Public Function CreateFormatString() As Boolean
    Dim iStart As Integer = 0
    Dim iLength As Integer = 0
    Dim sLiteralText As String
    Dim bSuccess As Boolean = True

    If TextColumnName = String.Empty Then
      bSuccess = False
      Return bSuccess
    End If

    Dim oRegex As New Regex(mcsGetLiteralText, RegexOptions.Singleline)
    Dim oMatches As MatchCollection = oRegex.Matches(TextColumnName)
    Dim sColumnValues As New List(Of String)
    Dim oBuilder As New StringBuilder

    mbStartsWithLiteral = True
    mbEndsWithLiteral = True
    '***** This member variable stores the literal values that make up
    '***** a multi-column text style. They are used each time a text
    '***** style is drawn to figure out which values are pulled from the 
    '***** columns of the label so that they can be formatted.
    moLiteralTextVals.Clear()

    '***** Find the literal text between the column names, even if it's only a space
    '***** The column names are stored between brackets, so if the string
    '***** starts with a bracket we know it's not a literal value.
    If TextColumnName.StartsWith("{") Then
      mbStartsWithLiteral = False
    End If

    If TextColumnName.EndsWith("}") Then
      mbEndsWithLiteral = False
    End If

    If mbStartsWithLiteral Then
      '***** Get the first literal value.
      sLiteralText = TextColumnName.Substring(0, TextColumnName.IndexOf("{"))
      moLiteralTextVals.Add(sLiteralText)
      iStart = sLiteralText.Length
    End If

    '***** Use Regex to find the rest of the literal values with the
    '***** possible exception of the last one, if the string ends with
    '***** a literal.
    For Each oMatch In oMatches
      sLiteralText = oMatch.ToString.Replace("}", "").Replace("{", "")
      moLiteralTextVals.Add(sLiteralText)
    Next oMatch

    '***** Now get the last value if there is one
    If mbEndsWithLiteral Then
      iStart = TextColumnName.LastIndexOf("}") + 1
      sLiteralText = TextColumnName.Substring(iStart)
      moLiteralTextVals.Add(sLiteralText)
    End If

    '***** Now the format string can be built
    Dim iLiteralTextCounter As Integer = 0

    If mbStartsWithLiteral Then
      '***** Literal values simply get appended to the string
      oBuilder.Append(moLiteralTextVals(0))
      iLiteralTextCounter += 1
    End If

    '***** Now we find the column names and create formatting
    '***** for each of them that goes between the literal values.
    oRegex = New Regex(mcsGetBracketedText)
    oMatches = oRegex.Matches(TextColumnName)
    Dim iFormatNum As Integer = 0
    For Each oMatch In oMatches
      '***** The FormatDictionary contains information on how to format each
      '***** of the numeric columns. It does not contain infor for formatting
      '***** non-numerics.
      If FormatDictionary.ContainsKey(oMatch.ToString.Replace("}", "").Replace("{", "")) Then
        '***** Create the correct formatting for the number of decimal places specified,
        '***** and whether or not thousands separators are desired.
        oBuilder.Append(mGetNumericFormat(FormatDictionary(oMatch.ToString.Replace("}", "").Replace("{", "")), iFormatNum))
      Else
        '***** Create neutral formatting
        oBuilder.Append("{" + iFormatNum.ToString + "}")
      End If
      '***** Append the next literal value if there is one
      If moLiteralTextVals.Count > iLiteralTextCounter Then
        oBuilder.Append(moLiteralTextVals(iLiteralTextCounter))
      End If
      iLiteralTextCounter += 1
      iFormatNum += 1
    Next oMatch
    '***** The native NumericFormat will not be used set it to an empty string
    NumericFormat = String.Empty
    '***** Save a value indicating an Expression is being used rather
    '***** that a single column value.
    mbIsUsingLabelExpression = True
    '***** Save the format string.
    msFormatString = oBuilder.ToString
    Return bSuccess

  End Function
  Private Function mGetNumericFormat(eFormatProps As StructColumnFormatProps, iFormatNum As Integer) As String
    Dim oFormatBuilder As New System.Text.StringBuilder
    Dim iNumDecimalPlaces As Integer


    If eFormatProps.UseThousandsSeparator Then
      oFormatBuilder.Append("{" + iFormatNum.ToString + ":#,##0")
    Else
      oFormatBuilder.Append("{" + iFormatNum.ToString + ":##0")
    End If

    iNumDecimalPlaces = eFormatProps.NumDecimalPlaces
    If iNumDecimalPlaces > 0 Then
      oFormatBuilder.Append(".")
    End If
    For i As Integer = 0 To iNumDecimalPlaces - 1
      oFormatBuilder.Append("0")
    Next i

    oFormatBuilder.Append("}")
    Return oFormatBuilder.ToString

  End Function
  ''' <summary>
  ''' When using a LabelExpression, parses the string about to be drawn to
  ''' the screen in order to apply numeric formatting. A LabelExpression is
  ''' when more than one column from the layer is used, possibly mixed with
  ''' literal values.
  ''' </summary>
  ''' <param name="sText"></param>
  ''' <returns></returns>
  Private Function mParseColumnValues(sText As String) As String
    Dim iStart As Integer = 0
    Dim iLength As Integer = 0
    Dim sLiteralValue As String
    Dim iLiteralValueIdx As Integer = 0

    If Not mbIsUsingLabelExpression OrElse msFormatString = String.Empty Then
      '***** No formatting required.
      Return sText
    End If

    Try
      If mbStartsWithLiteral Then
        '***** Get the first literal value from the List 
        sLiteralValue = moLiteralTextVals(iLiteralValueIdx)
        '***** Increment the index for reading the next literal value
        iLiteralValueIdx += 1
        '***** Save where to start searching for the first dynamic value
        iStart = sLiteralValue.Length
      End If

      '***** Index for adding to oArgs array
      Dim iColCount As Integer = 0
      '***** Dim it to a size we're pretty sure we will not exceed
      Dim oArgs(20) As Object

      '***** Iterate through the remaining literal values, there should be a dynamic value 
      '***** in between each of them.
      For i = iLiteralValueIdx To moLiteralTextVals.Count - 1
        '***** Get the literal value
        sLiteralValue = moLiteralTextVals(i)
        '***** Figure out the length of the dynamic value, we will start
        '***** either at the beginning of the string, or at the length of the
        '***** first value, if there was one. And end at the index where the
        '***** next literal value is found.
        iLength = sText.IndexOf(sLiteralValue, iStart) - iStart
        '***** This happens if some of the features have missing values in any of the fields being used
        '***** in a multi-column label.
        '***** SET 07/09/2020
        If iLength <= 0 Then
          Return sText
        End If
        '***** Now add the dynamic array to the oArgs array
        oArgs(iColCount) = sText.Substring(iStart, iLength)
        '***** Convert the value to a double if it's numeric, otherwise the
        '***** formatting will not work.
        If IsNumeric(oArgs(iColCount)) Then
          oArgs(iColCount) = CDbl(oArgs(iColCount))
        End If
        '***** Increment the array index
        iColCount += 1
        '***** Calculate the starting index to be used next time
        '***** through the loop
        iStart += sLiteralValue.Length + sText.Substring(iStart, iLength).Length
      Next i

      If Not mbEndsWithLiteral Then
        '***** Get the last column value
        oArgs(iColCount) = sText.Substring(iStart)
        If IsNumeric(oArgs(iColCount)) Then
          oArgs(iColCount) = CDbl(oArgs(iColCount))
        End If
        iColCount += 1
      End If

      Dim sValueString = String.Format(msFormatString, oArgs)

      Return sValueString
    Catch ex As Exception
      Debug.WriteLine("***** Error in mParseColumnValues " + ex.Message)
      '***** If there are null values in the columns used for 
      '***** the text symbol, parsing does not work and an exception
      '***** will be thrown, so just return the original text.
      Return sText
    End Try
  End Function
  Public Function Copy() As CustomTextStyle
    Dim oCustomTextStyle As New CustomTextStyle(TextColumnName, Font, TextBrush, Map, TextSymbolType)

    With oCustomTextStyle
      '.Advanced.TextCustomBrush = Advanced.TextCustomBrush
      .AllowLineCarriage = AllowLineCarriage
      .TextPlacement = TextPlacement
      '.CustomTextStyles = oIconStyle.CustomTextStyles
      .DateFormat = DateFormat
      .DrawingLevel = DrawingLevel
      .DuplicateRule = DuplicateRule
      .FittingLineInScreen = FittingLineInScreen
      .FittingPolygon = FittingPolygon
      .FittingPolygonFactor = FittingPolygonFactor
      .FittingPolygonInScreen = FittingPolygonInScreen
      .ForceHorizontalLabelForLine = ForceHorizontalLabelForLine
      .ForceLineCarriage = ForceLineCarriage
      .GridSize = GridSize
      '***** Cayzu #8544
      '***** Both of the 'HaloPen' properties below had a dot, so the HaloPen was not being copied.
      '***** This was resulting in the HaloPen properties NOT being properly populated into the 
      '***** Layer Properties form.
      '***** SET 07/26/2019
      .HaloPen = HaloPen
      .IsActive = IsActive
      .LabelAllLineParts = LabelAllLineParts
      .LabelAllPolygonParts = LabelAllPolygonParts
      .LeaderLineMinimumLengthInPixels = LeaderLineMinimumLengthInPixels
      .LeaderLineRule = LeaderLineRule
      .LeaderLineStyle = LeaderLineStyle
      .Mask = Mask
      .MaskMargin = MaskMargin
      .MaskType = MaskType
      .MaxNudgingInPixel = MaxNudgingInPixel
      .Name = Name
      .NudgingIntervalInPixel = NudgingIntervalInPixel
      .NumericFormat = NumericFormat
      .OverlappingRule = OverlappingRule
      .TextPlacement = TextPlacement
      .PolygonLabelingLocationMode = PolygonLabelingLocationMode
      .RotationAngle = RotationAngle
      .SplineType = SplineType
      '.StyleClass = StyleClass
      .SuppressPartialLabels = SuppressPartialLabels
      .TextColumnName = TextColumnName
      .TextFormat = TextFormat
      .TextLineSegmentRatio = TextLineSegmentRatio
      .XOffsetInPixel = XOffsetInPixel
      .YOffsetInPixel = YOffsetInPixel

      For Each s In RequiredColumnNames
        .RequiredColumnNames.Add(s)
      Next

    End With

    Return oCustomTextStyle
  End Function
  Public Function HasBeenMoved(sKey As String) As Boolean
    Return moLabelCoords.ContainsKey(sKey)
  End Function
  Public Sub SetPrivateProperties(sFormatString As String,
                                  bStartsWithLiteral As Boolean,
                                  bEndsWithLiteral As Boolean,
                                  bIsUsingLabelExpression As Boolean,
                                  oLiteralTextVals As List(Of String))
    msFormatString = sFormatString
    mbStartsWithLiteral = bStartsWithLiteral
    mbEndsWithLiteral = bEndsWithLiteral
    moLiteralTextVals = oLiteralTextVals
    mbIsUsingLabelExpression = bIsUsingLabelExpression

  End Sub
  ''' <summary>
  ''' Overwritten to implement a workaround provided by ThinkGeo. The purspose is to be able to ignore an exception
  ''' that gets thrown when labels are being drawn when the 'FittingPolygonInScreen' property is set = True.
  ''' Since the exception should no longer be thrown once TG implements a fix to the root cause this can remain in
  ''' place without causing any issues.
  ''' SET
  ''' 06/19/2019
  ''' </summary>
  ''' <param name="feature"></param>
  ''' <param name="canvas"></param>
  ''' <returns></returns>
  Protected Overrides Function GetLabelingTextCore(feature As Feature, canvas As GeoCanvas) As String
    '***** Cayzu #12842
    '***** SET 06/19/2019
    Try
      '***** Intercept an exception that gets thrown here.
      Return MyBase.GetLabelingTextCore(feature, canvas)
    Catch ex As Exception
      Trace.WriteLine(ex.Message)
      '***** Return an empty string
      Return String.Empty
    End Try
  End Function

  ''' <summary>
  ''' Cayzu #6460
  ''' Here is where the labels are drawn. If the Label Tool is active when this method is called, the
  ''' points layer of the LabelInteractiveOverlay is populated with the location of each label, which
  ''' allows the user to click and drag the label to a new position.
  ''' </summary>
  ''' <param name="oFeatures"></param>
  ''' <param name="canvas"></param>
  ''' <param name="labelsInThisLayer"></param>
  ''' <param name="labelsInAllLayers"></param>
  Protected Overrides Sub DrawCore(oFeatures As IEnumerable(Of Feature), canvas As GeoCanvas, labelsInThisLayer As Collection(Of SimpleCandidate), labelsInAllLayers As Collection(Of SimpleCandidate))
    Dim oLabelingCandidates As Collection(Of LabelingCandidate)
    Dim oPointsLayer As InMemoryFeatureLayer = Nothing
    Dim oPoint As PointShape = Nothing
    Dim oScreenPoint As ScreenPointF
    Dim oaScreenPoints(0) As ScreenPointF
    Dim oInfo As LabelInformation
    Dim oOffsetScreenPoint As ScreenPointF

    If Me.SplineType = SplineType.ForceSplining OrElse Me.SplineType = SplineType.StandardSplining Then
      '***** We do not currently support the repositioning of labels that are set to
      '***** spline with the line they are labeling.
      MyBase.DrawCore(oFeatures, canvas, labelsInThisLayer, labelsInAllLayers)
      Return
    End If

    oFeatures = GetLabeledFeatures(oFeatures, canvas, labelsInThisLayer, labelsInAllLayers)
    'Debug.WriteLine("There are " + oFeatures.Count.ToString + " labeled features.")
    Dim iCtr As Integer
    For Each oFeature As Feature In oFeatures
      iCtr += 1
      'Debug.WriteLine("Drawing label " + iCtr.ToString + " of " + oFeatures.Count.ToString)
      'oLabelingCandidates = GetLabelingCandidateCore(oFeature, canvas)
      oLabelingCandidates = CType(oFeature.Tag, Collection(Of LabelingCandidate))
      'Debug.WriteLine("There are " + oLabelingCandidates.Count.ToString + " labeling candidates")
      'Dim i As Integer = 1
      For Each oCandidate As CustomLabelingCandidate In oLabelingCandidates
        Dim sKey As String = msBaseKey + "_" + oFeature.Id + "_" + oCandidate.LabelingId.ToString
        'Dim sKey As String = msBaseKey + "_" + i.ToString
        oInfo = oCandidate.LabelInformation(0)
        If moLabelCoords.ContainsKey(sKey) Then
          oOffsetScreenPoint = MapUtil.ToScreenCoordinate(canvas.CurrentWorldExtent, moLabelCoords(sKey).WorldPosition.X, moLabelCoords(sKey).WorldPosition.Y, canvas.Width, canvas.Height)
          oScreenPoint = New ScreenPointF(CSng(oInfo.PositionInScreenCoordinates.X), CSng(oInfo.PositionInScreenCoordinates.Y))
          'If moLabelCoords(sKey).XOffsetPixel = 0 Then
          '***** The offset has not yet been calculated, so do it.
          moLabelCoords(sKey).XOffsetPixel = CSng(oInfo.PositionInScreenCoordinates.X) - oOffsetScreenPoint.X
          moLabelCoords(sKey).YOffsetPixel = CSng(oInfo.PositionInScreenCoordinates.Y) - oOffsetScreenPoint.Y
          'End If
          Select Case TextSymbolType
            '***** If the label is for a point or line then it's world coordinates will change based on the zoom level in order to keep
            '***** it the same distance in pixels from the feature being labelled. So instead of just using the world coordinates for
            '***** positioning the label, and offset is computed. Note that it only has to be calculated once for each time the label is
            '***** repositioned.
            Case EnumTextSymbolType.Point, EnumTextSymbolType.Line
              oaScreenPoints(0) = oScreenPoint
              '***** Create the point and draw the label using the offset calculated above. The PointShape (oPoint) will be
              '***** added to the EditShapesLayer of the CustomLabelEditingOverlay on the map.
              oPoint = MapUtil.ToWorldCoordinate(canvas.CurrentWorldExtent, oScreenPoint.X - moLabelCoords(sKey).XOffsetPixel, oScreenPoint.Y - moLabelCoords(sKey).YOffsetPixel, canvas.Width, canvas.Height)
              '***** Cayzu #12951
              '***** Added the call to DrawMask, which draws the rectangle around the label, if one has been specified.
              '***** SET 07/21/2020
              DrawMask(oCandidate, canvas, labelsInThisLayer, labelsInAllLayers)
              canvas.DrawText(oInfo.Text, Me.Font, Me.TextBrush, Me.HaloPen, oaScreenPoints, DrawingLevel.LabelLevel, -moLabelCoords(sKey).XOffsetPixel, -moLabelCoords(sKey).YOffsetPixel, CSng(oInfo.RotationAngle), DrawingTextAlignment.Default)
            Case EnumTextSymbolType.Polygon
              '***** Create the point and draw the label without the offset calculated above. The PointShape (oPoint) will be
              '***** added to the EditShapesLayer of the CustomLabelEditingOverlay on the map.
              oaScreenPoints(0) = oOffsetScreenPoint
              oPoint = New PointShape(moLabelCoords(sKey).WorldPosition.X, moLabelCoords(sKey).WorldPosition.Y)
              'if Not CheckOverlapping(oCandidate, canvas, labelsInThisLayer, labelsInAllLayers) Then
              '***** Cayzu #12951
              '***** Added the call to DrawMask, which draws the rectangle around the label, if one has been specified.
              '***** SET 07/21/2020
              DrawMask(oCandidate, canvas, labelsInThisLayer, labelsInAllLayers)
              'canvas.DrawText(oInfo.Text, Me.Font, Me.TextBrush, Me.HaloPen, oaScreenPoints, DrawingLevel.LabelLevel, 0, 0, CSng(oInfo.RotationAngle), DrawingTextAlignment.Center)

              canvas.DrawText(oInfo.Text, Me.Font, Me.TextBrush, Me.HaloPen, oaScreenPoints, DrawingLevel.LabelLevel, 0, 0, DrawingTextAlignment.Center, CSng(oInfo.RotationAngle))
              'End If
          End Select
        Else
          'Debug.WriteLine("In the Else, so no label repositioning has beend done.")
          '***** The label has not been relocated, so just draw it in its default location.
          '***** Note that if a pixel offset has been set by the user that the coordinates from the
          '***** LabelInfo object (oInfo) will already be adjusted for that.
          '***** SET 08/09/2019
          oScreenPoint = New ScreenPointF(CSng(oInfo.PositionInScreenCoordinates.X), CSng(oInfo.PositionInScreenCoordinates.Y))
          oaScreenPoints(0) = oScreenPoint
          oPoint = MapUtil.ToWorldCoordinate(canvas.CurrentWorldExtent, CSng(oInfo.PositionInScreenCoordinates.X), CSng(oInfo.PositionInScreenCoordinates.Y), canvas.Width, canvas.Height)
          'If Not CheckOverlapping(oCandidate, canvas, labelsInThisLayer, labelsInAllLayers) Then
          '***** Cayzu #12951
          '***** Added the call to DrawMask, which draws the rectangle around the label, if one has been specified.
          '***** SET 07/21/2020
          DrawMask(oCandidate, canvas, labelsInThisLayer, labelsInAllLayers)
          'canvas.DrawText(oInfo.Text, Me.Font, Me.TextBrush, Me.HaloPen, oaScreenPoints, DrawingLevel.LabelLevel, 0, 0, CSng(oInfo.RotationAngle), DrawingTextAlignment.Right)
          canvas.DrawText(oInfo.Text, Me.Font, Me.TextBrush, Me.HaloPen, oaScreenPoints, DrawingLevel.LabelLevel, 0, 0, DrawingTextAlignment.Right, CSng(oInfo.RotationAngle))
          'End If
        End If
        oPoint.Tag = oInfo.RotationAngle
        oPoint.Id = sKey
      Next oCandidate
    Next oFeature


  End Sub

  ''' <summary>
  ''' This method is called when someone moves a label to a new position. The new position is stored
  ''' in Dictonary moLabelCoords. From that point forward the new coords are used to draw the label.
  ''' </summary>
  ''' <param name="oPoint">The <see cref="PointShape"/> containing the new coordinates.</param>
  ''' <param name="sFeatureId"><see cref="String"/> containing the identifier for the new coordinates.</param>
  Public Sub AddNewLabelPosition(oPoint As PointShape, sFeatureId As String)
    Dim oNewLabelPos As New LabelPosition
    If moLabelCoords.ContainsKey(sFeatureId) Then
      '***** This label has been moved previously, simply update the coordinates.
      oNewLabelPos.WorldPosition = New Vertex(oPoint.X, oPoint.Y)
      moLabelCoords(sFeatureId) = oNewLabelPos
    Else
      '***** This label is being moved for the first time, add it to the collection.
      oNewLabelPos.WorldPosition = New Vertex(oPoint.X, oPoint.Y)
      moLabelCoords.Add(sFeatureId, oNewLabelPos)
    End If
  End Sub
  ''' <summary>
  ''' Copy this CustomTextStyle to an ordinary TextStyle.
  ''' </summary>
  ''' <returns><see cref="TextStyle"/></returns>
  Public Function ToTextStyle() As TextStyle
    Dim oTextStyle As New TextStyle(TextColumnName, Font, TextBrush)

    With oTextStyle
      '.Advanced.TextCustomBrush = Advanced.TextCustomBrush
      .AllowLineCarriage = AllowLineCarriage
      .TextPlacement = TextPlacement
      .DateFormat = DateFormat
      .DrawingLevel = DrawingLevel
      .DuplicateRule = DuplicateRule
      .FittingLineInScreen = FittingLineInScreen
      .FittingPolygon = FittingPolygon
      .FittingPolygonFactor = FittingPolygonFactor
      .FittingPolygonInScreen = FittingPolygonInScreen
      .ForceHorizontalLabelForLine = ForceHorizontalLabelForLine
      .ForceLineCarriage = ForceLineCarriage
      .GridSize = GridSize
      .HaloPen = HaloPen
      .IsActive = IsActive
      .LabelAllLineParts = LabelAllLineParts
      .LabelAllPolygonParts = LabelAllPolygonParts
      .LeaderLineMinimumLengthInPixels = LeaderLineMinimumLengthInPixels
      .LeaderLineRule = LeaderLineRule
      .LeaderLineStyle = LeaderLineStyle
      .Mask = Mask
      .MaskMargin = MaskMargin
      .MaskType = MaskType
      .MaxNudgingInPixel = MaxNudgingInPixel
      .Name = Name
      .NudgingIntervalInPixel = NudgingIntervalInPixel
      .NumericFormat = NumericFormat
      .OverlappingRule = OverlappingRule
      .TextPlacement = TextPlacement
      .PolygonLabelingLocationMode = PolygonLabelingLocationMode
      .RotationAngle = RotationAngle
      .SplineType = SplineType
      '.StyleClass = StyleClass
      .SuppressPartialLabels = SuppressPartialLabels
      .TextColumnName = TextColumnName
      .TextFormat = TextFormat
      .TextLineSegmentRatio = TextLineSegmentRatio
      .XOffsetInPixel = XOffsetInPixel
      .YOffsetInPixel = YOffsetInPixel

      For Each s In RequiredColumnNames
        .RequiredColumnNames.Add(s)
      Next

    End With

    Return oTextStyle
  End Function
  Public Sub ReprojectLabelCoords(oProj4 As ProjectionConverter)
    If oProj4 Is Nothing Then
      Return
    End If

    For Each oKvp As KeyValuePair(Of String, LabelPosition) In moLabelCoords
      oKvp.Value.WorldPosition = oProj4.ConvertToExternalProjection(oKvp.Value.WorldPosition.X, oKvp.Value.WorldPosition.Y)
    Next oKvp
  End Sub
  Public Sub RemoveLabelFromDictionary(sKey As String)
    If moLabelCoords.ContainsKey(sKey) Then
      moLabelCoords.Remove(sKey)
    End If
  End Sub

  'Public Function GetLabeledFeatures(ByVal oAllFeatures As Collection(Of Feature), ByVal oCanvas As GeoCanvas, ByVal labelsInThisLayer As Collection(Of SimpleCandidate), ByVal labelsInAllLayers As Collection(Of SimpleCandidate)) As Collection(Of Feature)
  '  Dim oUnfilteredFeatures As Collection(Of Feature) = MyBase.FilterFeatures(oAllFeatures, oCanvas)
  '  Dim oReturnFeatures As New Collection(Of Feature)()
  '  Dim oCanvasScreenExtent As RectangleShape = Nothing

  '  If SuppressPartialLabels Then
  '    '***** Get the screen extent
  '    oCanvasScreenExtent = ConvertToScreenShape(New Feature(oCanvas.CurrentWorldExtent), oCanvas).GetBoundingBox()
  '  End If

  '  For Each oFeature As Feature In oUnfilteredFeatures
  '    '***** Get the labeling candidates for this feature
  '    Dim oLabelingCandidates As Collection(Of LabelingCandidate) = GetLabelingCandidateCore(oFeature, oCanvas)

  '    For Each oCandidate As LabelingCandidate In oLabelingCandidates

  '      If CheckDuplicate(oCandidate, oCanvas, labelsInThisLayer, labelsInAllLayers) Then
  '        '***** It would be a duplicate so don't add it
  '        Continue For
  '      End If

  '      If CheckOverlapping(oCandidate, oCanvas, labelsInThisLayer, labelsInAllLayers) Then
  '        '***** It would overlap another label so don't add it.
  '        Continue For
  '      End If

  '      Dim simpleCandidate As SimpleCandidate = New SimpleCandidate(oCandidate.OriginalText, oCandidate.ScreenArea)

  '      If labelsInAllLayers IsNot Nothing Then
  '        '***** Didn't get thrown out of the loop, so add to the collection. The collection accumulates SimpleCandidates
  '        '***** that are used in future checks.
  '        labelsInAllLayers.Add(simpleCandidate)
  '      End If

  '      If labelsInThisLayer IsNot Nothing Then
  '        '***** Didn't get thrown out of the loop, so add to the collection. The collection accumulates SimpleCandidates
  '        '***** that are used in future checks.
  '        labelsInThisLayer.Add(simpleCandidate)
  '      End If

  '      If SuppressPartialLabels Then
  '        '***** We are suppressing partial labels so only add the feature if the 
  '        '***** labelling candidate fits inside the screen area.
  '        If Not oCanvasScreenExtent.Contains(oCandidate.ScreenArea) Then
  '          oReturnFeatures.Add(oFeature)
  '        End If
  '      Else
  '        '***** Not suppressing partial labels, so just add the feature.
  '        oReturnFeatures.Add(oFeature)
  '      End If
  '    Next
  '  Next

  '  Return oReturnFeatures
  'End Function
  ''' <summary>
  ''' Unused ThinkGeo function
  ''' </summary>
  ''' <param name="allFeatures"></param>
  ''' <param name="drawingCanvas"></param>
  ''' <param name="labelsInThisLayer"></param>
  ''' <param name="labelsInAllLayers"></param>
  ''' <returns></returns>
  Public Function GetUnLabeledFeaturesForOverlayping(allFeatures As Collection(Of Feature),
                                                     drawingCanvas As GeoCanvas,
                                                     labelsInThisLayer As Collection(Of SimpleCandidate),
                                                     labelsInAllLayers As Collection(Of SimpleCandidate)) As Collection(Of Feature)

    Dim unfilteredFeatures As Collection(Of Feature) = MyBase.FilterFeatures(allFeatures, drawingCanvas)
    Dim returnFeatures As Collection(Of Feature) = New Collection(Of Feature)()

    For Each feature As Feature In unfilteredFeatures
      'Dim labelingCandidates As Collection(Of LabelingCandidate) = GetLabelingCandidates(feature, drawingCanvas)
      ' Post Upgrade change - added String.Emtpy as a parameter.  
      ' Need to determeine what the string value sent in should be.
      Dim labelingCandidates As Collection(Of LabelingCandidate) = GetLabelingCandidates(feature, String.Empty, drawingCanvas, Me.Font, Me.XOffsetInPixel, Me.YOffsetInPixel, Me.RotationAngle)
      For Each labelingCandidate As LabelingCandidate In labelingCandidates

        If CheckDuplicate(labelingCandidate, drawingCanvas, labelsInThisLayer, labelsInAllLayers) Then
          Continue For
        End If

        Dim O As New Quadtree(Of SimpleCandidate)
        If CheckOverlapping(labelingCandidate, drawingCanvas, Me.Font, Me.XOffsetInPixel, Me.YOffsetInPixel, Me.RotationAngle, O) Then
          returnFeatures.Add(feature)
        End If

        Dim env As Envelope = New Envelope(mEnvelopeFromExtent(labelingCandidate.ScreenArea.GetBoundingBox))
        Dim simpleCandidate As SimpleCandidate = New SimpleCandidate(labelingCandidate.OriginalText, env)

        If labelsInAllLayers IsNot Nothing Then
          labelsInAllLayers.Add(simpleCandidate)
        End If

        If labelsInThisLayer IsNot Nothing Then
          labelsInThisLayer.Add(simpleCandidate)
        End If
      Next
    Next

    Return returnFeatures
  End Function
  ''' <summary>
  ''' Unused ThinkGeo function
  ''' </summary>
  ''' <param name="allFeatures"></param>
  ''' <param name="drawingCanvas"></param>
  ''' <param name="labelsInThisLayer"></param>
  ''' <param name="labelsInAllLayers"></param>
  ''' <returns></returns>
  Public Function GetUnLabeledFeaturesForDupulicating(ByVal allFeatures As Collection(Of Feature), ByVal drawingCanvas As GeoCanvas, ByVal labelsInThisLayer As Collection(Of SimpleCandidate), ByVal labelsInAllLayers As Collection(Of SimpleCandidate)) As Collection(Of Feature)
    Dim unfilteredFeatures As Collection(Of Feature) = MyBase.FilterFeatures(allFeatures, drawingCanvas)
    Dim returnFeatures As Collection(Of Feature) = New Collection(Of Feature)()

    For Each feature As Feature In unfilteredFeatures
      Dim labelingCandidates As Collection(Of LabelingCandidate) = GetLabelingCandidates(feature, String.Empty, drawingCanvas, Me.Font, Me.XOffsetInPixel, Me.YOffsetInPixel, Me.RotationAngle)

      For Each labelingCandidate As LabelingCandidate In labelingCandidates

        If CheckDuplicate(labelingCandidate, drawingCanvas, labelsInThisLayer, labelsInAllLayers) Then
          returnFeatures.Add(feature)
        End If

        Dim O As New Quadtree(Of SimpleCandidate)
        If CheckOverlapping(labelingCandidate, drawingCanvas, Me.Font, Me.XOffsetInPixel, Me.YOffsetInPixel, Me.RotationAngle, O) Then
          Continue For
        End If

        Dim env As Envelope = New Envelope(mEnvelopeFromExtent(labelingCandidate.ScreenArea.GetBoundingBox))
        Dim simpleCandidate As SimpleCandidate = New SimpleCandidate(labelingCandidate.OriginalText, env)

        If labelsInAllLayers IsNot Nothing Then
          labelsInAllLayers.Add(simpleCandidate)
        End If

        If labelsInThisLayer IsNot Nothing Then
          labelsInThisLayer.Add(simpleCandidate)
        End If
      Next
    Next

    Return returnFeatures
  End Function

  ''' <summary>
  ''' Gets a collection of features for which labels should be drawn given the 
  ''' duplicate/partial/overlapping labelling rules on the class.
  ''' Needed because this class draws the labels 'manually'. Taken from ThinkGeo forum.
  ''' SET 11/07/2019
  ''' </summary>
  ''' <param name="oAllFeatures"></param>
  ''' <param name="oCanvas"></param>
  ''' <param name="labelsInThisLayer"></param>
  ''' <param name="labelsInAllLayers"></param>
  ''' <returns></returns>
  Public Function GetLabeledFeatures(ByVal oAllFeatures As Collection(Of Feature), ByVal oCanvas As GeoCanvas, ByVal labelsInThisLayer As Collection(Of SimpleCandidate), ByVal labelsInAllLayers As Collection(Of SimpleCandidate)) As Collection(Of Feature)
    'Dim oUnfilteredFeatures As Collection(Of Feature) = MyBase.FilterFeatures(oAllFeatures, oCanvas)
    Dim oUnfilteredFeatures As System.Collections.Generic.IEnumerable(Of Feature) = MyBase.FilterFeatures(oAllFeatures, oCanvas)
    'Dim oUnfilteredFeatures = oAllFeatures
    Dim oReturnFeatures As New Collection(Of Feature)
    Dim oCanvasScreenExtent As RectangleShape = Nothing

    If SuppressPartialLabels Then
      '***** Get the screen extent
      'SSA-476, Jamie Irwin 12/18/2025, Depricated ConvertToScreenShape replaced with 
      'oCanvasScreenExtent = ConvertToScreenShape(New Feature(oCanvas.CurrentWorldExtent), oCanvas).GetBoundingBox()
      oCanvasScreenExtent = MapUtil.ToScreenCoordinate(New Feature(oCanvas.CurrentWorldExtent), oCanvas.CurrentWorldExtent, oCanvas.Width, oCanvas.Height).GetBoundingBox()
    End If

    For Each oFeature As Feature In oUnfilteredFeatures
      If Not oFeature.Intersects(oCanvas.CurrentWorldExtent.GetFeature) Then
        Continue For
      End If
      oFeature.Tag = New Collection(Of LabelingCandidate)
      '***** Get the labeling candidates for this feature
      'Dim oRawLabelingCandidates As Collection(Of LabelingCandidate) = GetLabelingCandidateCore(oFeature, Me.TextColumnName, oCanvas, Me.Font, Me.XOffsetInPixel, Me.YOffsetInPixel, Me.RotationAngle)
      Dim oRawLabelingCandidates As Collection(Of LabelingCandidate) = GetLabelingCandidates(oFeature, FormatTemplate(Me.TextContent, oFeature.ColumnValues), oCanvas, Me.Font, XOffsetInPixel, YOffsetInPixel, RotationAngle)

      'Debug.WriteLine("Got " + oRawLabelingCandidates.Count.ToString + " Raw Labeling Candidates")
      Dim oLabelingCandidates As New Collection(Of CustomLabelingCandidate)
      Dim iLabelId As Integer = 1

      '***** Create a collecttion of CustomLabelingCandidates which save an Id
      '***** for each label
      For Each oLabelingCandidate As LabelingCandidate In oRawLabelingCandidates
        Dim sKey As String = msBaseKey + "_" + oFeature.Id + "_" + iLabelId.ToString
        If moLabelCoords.ContainsKey(sKey) Then
          Dim oPointFeature As New Feature(New PointShape(moLabelCoords(sKey).WorldPosition.X, moLabelCoords(sKey).WorldPosition.X), oFeature.ColumnValues)
          Dim o = GetLabelingCandidates(oPointFeature, String.Empty, oCanvas, Me.Font, Me.XOffsetInPixel, Me.YOffsetInPixel, Me.RotationAngle)
          o(0).LabelInformation(0) = oLabelingCandidate.LabelInformation(0)
          oLabelingCandidates.Add(New CustomLabelingCandidate(o(0), iLabelId))
        Else
          oLabelingCandidates.Add(New CustomLabelingCandidate(oLabelingCandidate, iLabelId))
        End If
        iLabelId += 1
      Next oLabelingCandidate

      'Debug.WriteLine("There are " + oLabelingCandidates.Count.ToString + " in oLabelingCandidates")

      For Each oLabelingCandidate As CustomLabelingCandidate In oLabelingCandidates
        Dim sKey As String = msBaseKey + "_" + oFeature.Id + "_" + oLabelingCandidate.LabelingId.ToString
        If CheckDuplicate(oLabelingCandidate, oCanvas, labelsInThisLayer, labelsInAllLayers) Then
          '***** It would be a duplicate so don't add it
          Continue For
        End If

        '*****************************************************************************************************
        '***** RIGHT HERE!
        Try
          Dim oQuadTree As New Quadtree(Of SimpleCandidate)
          If CheckOverlapping(oLabelingCandidate, oCanvas, Me.Font, Me.XOffsetInPixel, Me.YOffsetInPixel, Me.RotationAngle, oQuadTree) Then
            '***** It would overlap another label so don't add it.
            Continue For
          End If
          '*****************************************************************************************************
        Catch ex As Exception
          Debug.WriteLine("Error checking for overlaps: " + ex.Message)
        End Try


        Dim env As Envelope = New Envelope(mEnvelopeFromExtent(oLabelingCandidate.ScreenArea.GetBoundingBox))
        Dim simpleCandidate As SimpleCandidate = New SimpleCandidate(oLabelingCandidate.OriginalText, env)

        If labelsInAllLayers IsNot Nothing Then
          '***** Didn't get thrown out of the loop, so add to the collection. The collection accumulates SimpleCandidates
          '***** that are used in future checks.
          labelsInAllLayers.Add(simpleCandidate)
        End If

        If labelsInThisLayer IsNot Nothing Then
          '***** Didn't get thrown out of the loop, so add to the collection. The collection accumulates SimpleCandidates
          '***** that are used in future checks.
          labelsInThisLayer.Add(simpleCandidate)
        End If

        If SuppressPartialLabels Then
          '***** We are suppressing partial labels so only add the feature if the 
          '***** labelling candidate fits inside the screen area.
          If Not oCanvasScreenExtent.Contains(oLabelingCandidate.ScreenArea) Then
            'oReturnFeatures.Add(oFeature)
            CType(oFeature.Tag, Collection(Of LabelingCandidate)).Add(oLabelingCandidate)
          End If
        Else
          '***** Not suppressing partial labels, so just add the feature.
          'oReturnFeatures.Add(oFeature)
          'Debug.WriteLine("Adding a Labeling Candidate to the Tag collection.")
          CType(oFeature.Tag, Collection(Of LabelingCandidate)).Add(oLabelingCandidate)
        End If
      Next oLabelingCandidate
      'Debug.WriteLine("Adding a feature to ReturnFeatures")
      oReturnFeatures.Add(oFeature)

    Next oFeature
    Debug.WriteLine("Returning " + oReturnFeatures.Count.ToString + " features from GetLabeledFeatures")
    Return oReturnFeatures
  End Function
  'Protected Overrides Function CheckOverlappingCore(labelingCandidate As LabelingCandidate, labelIndex As Quadtree(Of SimpleCandidate)) As Boolean
  '  Try
  '    Return MyBase.CheckOverlappingCore(labelingCandidate, labelIndex)
  '  Catch ex As Exception
  '    Debug.WriteLine("Error in CheckOverlappingCore: " + ex.Message)
  '  End Try

  'End Function
  Public Function FormatTemplate(sTemplate As String, oColumnValues As Dictionary(Of String, String)) As String
    Dim sResult As String = sTemplate

    For Each sKey As String In oColumnValues.Keys
      '***** In the If, which was new to me note: if values(key) is null an empty string
      '***** is returned. Otherwise you get the value itself.
      sResult = sResult.Replace("{" & sKey & "}", If(oColumnValues(sKey), ""))
    Next sKey

    Return sResult
  End Function
  Private Function mEnvelopeFromExtent(oExtent As RectangleShape) As Envelope
    Return New Envelope(oExtent.MinX, oExtent.MaxX, oExtent.MinY, oExtent.MaxY)
  End Function
  Protected Overrides Function CheckDuplicateCore(labelingCandidate As LabelingCandidate, canvas As GeoCanvas, labelsInThisLayer As Collection(Of SimpleCandidate), labelsInAllLayers As Collection(Of SimpleCandidate)) As Boolean
    Return MyBase.CheckDuplicateCore(labelingCandidate, canvas, labelsInThisLayer, labelsInAllLayers)
  End Function
End Class
