1.0.7 x/y distance fix and SmartPath improvements

generic [05-20-22 - 14:24]
1.0.7 x/y distance fix and SmartPath improvements
Filename
HarvestRoute.lua
diff --git a/HarvestRoute.lua b/HarvestRoute.lua
index c27646b..861524e 100644
--- a/HarvestRoute.lua
+++ b/HarvestRoute.lua
@@ -1,7 +1,7 @@
 HarvestRoute = {}
 HarvestRoute.name = "HarvestRoute"
 HarvestRoute.displayName = 'HarvestRoute for Harvestmap'
-HarvestRoute.version = "1.0.6"
+HarvestRoute.version = "1.0.7"
 HarvestRoute.author = "generic"
 HarvestRoute.settingsVersion = 3
 HarvestRoute.savedVars = {}
@@ -44,6 +44,7 @@ local pathNodes = {}
 local playerX = 0
 local playerY = 0
 local playerZ = 0
+local ourPathChange = false



@@ -101,16 +102,16 @@ local function initialize( eventCode, addOnName )
     Harvest.callbackManager:RegisterForEvent(Harvest.events.TOUR_CHANGED, HarvestRoute.checkPath )
     HarvestRoute.ApplyStyle()
     if HarvestRoute.savedVars.enableTrackerWindow and HarvestRoute.savedVars.showTrackerWindow then
-      HarvestRoute:SetTrackerHidden( false )
-      HarvestRoute:InitTracker()
-      HarvestRoute:UpdateTracker()
+      HarvestRoute.SetTrackerHidden( false )
+      HarvestRoute.InitTracker()
+      HarvestRoute.UpdateTracker()
     end

 end

 local function TrackerWindowDisabled() return not HarvestRoute.savedVars.enableTrackerWindow end

-function HarvestRoute:InitializeSettingsMenu()
+function HarvestRoute.InitializeSettingsMenu()
   local panel = {
     type = "panel",
     name = HarvestRoute.name,
@@ -146,9 +147,9 @@ function HarvestRoute:InitializeSettingsMenu()
         setFunc = function (value)
             HarvestRoute.savedVars.showTrackerWindow = value
             if value then
-              HarvestRoute:SetTrackerHidden( false )
-              HarvestRoute:InitTracker()
-              HarvestRoute:UpdateTracker()
+              HarvestRoute.SetTrackerHidden( false )
+              HarvestRoute.InitTracker()
+              HarvestRoute.UpdateTracker()
             end
           end,
         default = HarvestRoute.defaultSettings.showTrackerWindow,
@@ -179,58 +180,63 @@ function HarvestRoute:InitializeSettingsMenu()
     LibAddonMenu2:RegisterOptionControls(HarvestRoute.name.."OptionsMenu", options)
 end

-function HarvestRoute:OnButton()
+function HarvestRoute.OnButton()
     if isActive then
       isActive = false
-      HarvestRoute:StopTracker()
+      HarvestRoute.StopTracker()
     else
       isActive = true
-      HarvestRoute:StartTracker()
+      HarvestRoute.StartTracker()
     end
-    HarvestRoute:UpdateTracker()
+    HarvestRoute.UpdateTracker()
 end

-function HarvestRoute:StartTracker()
+function HarvestRoute.StartTracker()
       EVENT_MANAGER:RegisterForUpdate("HarvestRouteUpdatePosition", 50, HarvestRoute.OnUpdate)


       local mapMetaData = Harvest.mapTools:GetViewedMapMetaData()
       MapCache = Harvest.Data:GetMapCache( mapMetaData )
       currentZoneId = GetUnitZoneIndex("player")
-
-      HarvestRoute:SetTrackerHidden(false)
-      HarvestRoute:InitTracker()
+
+      Farm = Harvest.farm
+      Editor = Harvest.editor
+      Helper = Farm.helper
+
+      HarvestRoute.SetTrackerHidden(false)
+      HarvestRoute.InitTracker()
 end

-function HarvestRoute:StopTracker()
+function HarvestRoute.StopTracker()
       EVENT_MANAGER:UnregisterForUpdate("HarvestRouteUpdatePosition")
-      HarvestRoute:InitTracker()
+      HarvestRoute.InitTracker()
 end

-function HarvestRoute:OnPlayerLoaded()
+function HarvestRoute.OnPlayerLoaded()
       local newZoneId = GetUnitZoneIndex("player")
       if currentZoneId and currentZoneId == newZoneId then
         return
       end
-      HarvestRoute:debug('zone switched')
+      HarvestRoute.debug('zone switched')
       lastNodeId = nil
       pathNodeId = nil
-      lastPathIndex = nill
+      lastPathIndex = nil
       pathNodes = {}
       currentZoneId = newZoneId
       if isActive then
-        HarvestRoute:OnButton()
+        HarvestRoute.OnButton()
       end
       if HarvestRoute.savedVars.enableTrackerWindow then
         if HarvestRoute.savedVars.showTrackerWindow or isTrackerVisible then
-          HarvestRoute:SetTrackerHidden( false )
-          HarvestRoute:UpdateTracker()
+          HarvestRoute.SetTrackerHidden( false )
+          HarvestRoute.UpdateTracker()
         end
       end
 end

 -- reset pathnodes when path has changed from outside, and either last node or last path node are invalid
-function HarvestRoute:checkPath(event, path)
+function HarvestRoute.checkPath(event, path)
+  if ourPathChange then return end -- if we are responsible we do not care.
   local validpath = false
   local validnode = false
   if lastNodeId and MapCache then
@@ -247,26 +253,26 @@ function HarvestRoute:checkPath(event, path)
     end
   end
   if not validnode then
-    HarvestRoute:debug('last node invalid')
+    HarvestRoute.debug('last node invalid')
     lastNodeId = nil
   end
   if not validpath then
-    HarvestRoute:debug('last path index invalid')
+    HarvestRoute.debug('last path index invalid')
     lastNodeId = nil
     pathNodeId = nil
     lastPathIndex = nil
     pathNodes = {}
   end
-  HarvestRoute:UpdateTracker()
+  HarvestRoute.UpdateTracker()
 end

-function HarvestRoute:OnUpdate (time)
+function HarvestRoute.OnUpdate (time)
     if isActive then
       playerX, playerY, playerZ = Harvest.GetPlayer3DPosition()
-      local nodeId, nodeDistance, nodePinType = HarvestRoute:GetClosestNode()
+      local nodeId, nodeDistance, nodePinType = HarvestRoute.GetClosestNode()
       if nodeId then
         if nodePinType ~= Harvest.UNKNOWN and nodeId ~= lastNodeId and (nodeDistance < HarvestRoute.savedVars.trackerRange) then
-          HarvestRoute:debug('node in range: '..nodeId)
+          HarvestRoute.debug('node in range: '..nodeId)
           local x, y = MapCache:GetLocal(nodeId)
           if Farm.path then
             local index = Farm.path:GetIndex(nodeId)
@@ -285,20 +291,22 @@ function HarvestRoute:OnUpdate (time)
               end

               if switchToPathNode then
-                HarvestRoute:debug('path node '..nodeId..' ')
+                HarvestRoute.debug('path node '..nodeId..' ')
                 pathNodeId = nodeId
                 lastPathIndex = index
               end
             else
               if pathNodeId then
-                HarvestRoute:debug('insert '..nodeId..' after path node '..pathNodeId..' ')
+                HarvestRoute.debug('insert '..nodeId..' after path node '..pathNodeId..' ')
                 if HarvestRoute.savedVars.usePathHeuristic then
-                  pathNodeId = HarvestRoute:checkPathHeuristic(pathNodeId, lastNodeAddedToPath, nodeId)
+                  pathNodeId = HarvestRoute.getSmartInsertNode(pathNodeId, lastNodeAddedToPath, nodeId)
                 end
+                ourPathChange = true
                 Farm.path:InsertAfter(pathNodeId, nodeId)
                 pathNodeId = nodeId
                 lastNodeAddedToPath = nodeId
                 lastPathIndex = Farm.path:GetIndex(nodeId)
+                ourPathChange = false
                 Harvest.farm.display:Refresh(Farm.path, Harvest.farm.display.selectedPins, Harvest.farm.display.selectedMapCache)
                 Harvest.farm.editor.statusLabel:SetText(Harvest.farm.editor.textConstructor())

@@ -311,64 +319,150 @@ function HarvestRoute:OnUpdate (time)
             end
           else --create a new path
             pathNodes[#pathNodes + 1] = nodeId
-            HarvestRoute:debug('added '..nodeId..' as path node '..#pathNodes)
+            HarvestRoute.debug('added '..nodeId..' as path node '..#pathNodes)
             if #pathNodes > 2 then
+              ourPathChange = true
               path = Harvest.path:New(MapCache)
               path:GenerateFromNodeIds(pathNodes, 1, #pathNodes)
               Farm:SetPath(path)
+              ourPathChange = false
               lastPathIndex = Farm.path:GetIndex(nodeId)
+              lastNodeAddedToPath = nodeId
               pathNodeId = nodeId
               pathNodes = {}
             end
           end
           lastNodeId = nodeId
         end --nodeDistance
-        HarvestRoute:UpdateTracker()
+        HarvestRoute.UpdateTracker()
       end --nodeId
     end --isActive
 end

-function HarvestRoute:getNodeDistance(n1, n2)
-  local distance = 0
+function HarvestRoute.getNodeDistance(n1, n2)
+  local distance = 10000
   if MapCache.worldX and MapCache.worldX[n1] and MapCache.worldX[n2] then
     local dx = MapCache.worldX[n1] - MapCache.worldX[n2]
-    local dy = MapCache.worldX[n1] - MapCache.worldX[n2]
+    local dy = MapCache.worldY[n1] - MapCache.worldY[n2]
     distance = ( dx * dx + dy * dy ) ^ 0.5
   end
   return distance
 end

-function HarvestRoute:GetNextPathNode(nodeId)
+function HarvestRoute.GetPathNodeWithOffset(nodeId, offset)
   local index = Farm.path:GetIndex(nodeId)
+  local oldindex = index
   if not index then return nil end
-  index = ((index + 1) % Farm.path.numNodes)
+  if not offset then return nodeId end
+  local numNodes = Farm.path.numNodes
+  --offset = offset % numNodes
+  index = index + offset + numNodes
+  index = ((index - 1) % numNodes) +1
+  if index < 1 then
+    --index = index + numNodes
+  elseif index > numNodes then
+    --index = index - numNodes
+  end
+  HarvestRoute.debug("Index ".. oldindex .. " + " .. offset .. " = " .. index .. " (" .. numNodes .. ")" )
   return Farm.path.nodeIndices[index]
 end
+function HarvestRoute.GetNextPathNode(nodeId)
+  return HarvestRoute.GetPathNodeWithOffset(nodeId, 1)
+end
+function HarvestRoute.GetPreviousPathNode(nodeId)
+  return HarvestRoute.GetPathNodeWithOffset(nodeId, -1)
+end
+

--- be "smart" and detect which of n1 or n2 adds less total length, to prevent zigzag accidents when backtracking
-function HarvestRoute:checkPathHeuristic(n1, n2, addNode)
-  if not n2 then return n1 end
-  if n1 == n2 then return n1 end
-  HarvestRoute:debug("checkPathHeuristic " .. n1 .." / ".. n2 .. " (" .. addNode ..")")
+-- be "smart" and detect if adding before or after either n1 or n2 wourld result in a better path
+-- should remove most zigzags when backtracking or enhancing existing paths
+function HarvestRoute.getSmartInsertNode(n1, n2, addNode)
+  --if not n2 then return n1 end
+  --if n1 == n2 then return n1 end
+  HarvestRoute.debug("getSmartInsertNode")
+  -- HarvestRoute.debug("checkPathHeuristic " .. n1 .." / ".. n2 .. " (" .. addNode ..")")
+  local result = n1
   if Farm.path then
-    local n1next = HarvestRoute:GetNextPathNode(n1)
-    local n2next = HarvestRoute:GetNextPathNode(n2)
-    if n1next and n2next then
-      local d1 = HarvestRoute:getNodeDistance(n1, addNode) + HarvestRoute:getNodeDistance(n1next, addNode) - HarvestRoute:getNodeDistance(n1, n1next)
-      local d2 = HarvestRoute:getNodeDistance(n2, addNode) + HarvestRoute:getNodeDistance(n2next, addNode) - HarvestRoute:getNodeDistance(n2, n2next)
-      HarvestRoute:debug("Lastpathnode " .. d1 .. " / Lastinsertnode " .. d2 )
-      if d2 < d1 then
-        return n2
+    local n1_result, n1_distance = HarvestRoute.getBetterInsertNode(n1, addNode)
+    result = n1_result
+    if n2 and n1 ~= n2 then
+      local n2_result, n2_distance = HarvestRoute.getBetterInsertNode(n2, addNode)
+      if n2_distance < n1_distance then
+        result = n2_result
       end
+    end
+  end
+  return result
+end
+
+-- return previous node, if the resulting path would be better (basically insert before node, instead of insert after)
+function HarvestRoute.getBetterInsertNode(node, addNode)
+  local nextnode = HarvestRoute.GetNextPathNode(node)
+  local prevnode = HarvestRoute.GetPreviousPathNode(node)
+  if prevnode and nextnode then
+    -- using stackoverflow https://stackoverflow.com/questions/33155240/how-to-check-if-a-point-is-between-two-other-points-but-not-limited-to-be-align
+    -- add = A, node = B, next / prev = C
+    local a_prev_to_node = HarvestRoute.getNodeDistance(prevnode, node)
+    local a_node_to_next = HarvestRoute.getNodeDistance(node, nextnode)
+    local b_prev_to_add  = HarvestRoute.getNodeDistance(prevnode, addNode)
+    local b_add_to_next  = HarvestRoute.getNodeDistance(addNode, nextnode)
+    local c_node_to_add  = HarvestRoute.getNodeDistance(node, addNode)
+
+    -- path length changes for both node and prevnode
+    local d_next = a_prev_to_node + c_node_to_add + b_add_to_next - a_node_to_next
+    local d_prev = b_prev_to_add + c_node_to_add + a_node_to_next - a_prev_to_node
+
+    -- some mathemagics which are not needed
+    --[[
+    local node_between_next = HarvestRoute.NodeAisBetweenBandCusingDistances(a_node_to_next, b_add_to_next, c_node_to_add)
+    local node_between_prev = HarvestRoute.NodeAisBetweenBandCusingDistances(a_prev_to_node, b_prev_to_add, c_node_to_add)
+
+    local prev_crossed = HarvestRoute.linesAreCrossed(prevnode, addNode, node, nextnode)
+    local next_crossed = HarvestRoute.linesAreCrossed(prevnode, node, addNode, nextnode)
+    --]]
+    if d_prev < d_next then
+      HarvestRoute.debug("PrevNode " .. prevnode .. " (prevnode shorter " .. d_prev .. " vs. " .. d_next .. ")")
+      return prevnode, d_prev
     else
-      HarvestRoute:debug("NOT IN PATH n1 and n2")
+      HarvestRoute.debug("Node " .. node .. " (node shorter " .. d_prev .. " vs. " .. d_next.. ")")
+      return node, d_next
     end
-  end
-  return n1
+  end
+  return node, 0
+end
+
+-- checks if the two line-segments defined by points [a/b] to [c/d] and [p/q] to [r/s] do intersect or not
+function HarvestRoute.intersects(a,b,c,d, p,q,r,s)
+  local det, gamma, lambda;
+  det = (c - a) * (s - q) - (r - p) * (d - b);
+  if det == 0 then return false end
+  lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / det;
+  gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / det;
+  return (0 < lambda and lambda < 1) and (0 < gamma and gamma < 1);
+end
+
+function HarvestRoute.linesAreCrossed(node_a, node_b, node_c, node_d)
+  local wx = MapCache.worldX
+  local wy = MapCache.worldY
+  return HarvestRoute.intersects(
+      wx[node_a],wy[node_a],
+      wx[node_b],wy[node_b],
+      wx[node_c],wy[node_c],
+      wx[node_d],wy[node_d])
 end

+function HarvestRoute.NodeAisBetweenBandC(nodeA, nodeB, nodeC)
+  local a = HarvestRoute.getNodeDistance(nodeB, nodeC)
+  local b = HarvestRoute.getNodeDistance(nodeC, nodeA)
+  local c = HarvestRoute.getNodeDistance(nodeA, nodeB)
+  return HarvestRoute.NodeAisBetweenBandCusingDistances(a, b, c)
+end
+
+function HarvestRoute.NodeAisBetweenBandCusingDistances(a, b, c)
+  return (a*a + b*b >= c*c and a*a + c*c >= b*b)
+end
 -- get the closest node to the player, using HarvestMap MapCache Divisions (and only check 4 divisions, from -50 to +50 from current position)
-function HarvestRoute:GetClosestNode()
+function HarvestRoute.GetClosestNode()
     local bestNodeId = nil
     if not MapCache then --yeah, no cache no cookies.
       return nil, nil
@@ -453,19 +547,19 @@ function HarvestRoute:OnMoveStop( )
   HarvestRoute.savedVars.wm.height = self:GetHeight()
 end

-function HarvestRoute:ApplyStyle()
+function HarvestRoute.ApplyStyle()
   HarvestRouteTracker:SetAnchor( TOPLEFT, GuiRoot, TOPLEFT, HarvestRoute.savedVars.wm.x, HarvestRoute.savedVars.wm.y )
 end


-function HarvestRoute:SetTrackerHidden(value)
+function HarvestRoute.SetTrackerHidden(value)
   isTrackerVisible = false
   if not HarvestRoute.savedVars.enableTrackerWindow then value = true end
   HarvestRouteTracker:SetHidden(value)
   isTrackerVisible = not value
 end

-function HarvestRoute:GetNodeDescription(nodeId)
+function HarvestRoute.GetNodeDescription(nodeId)
     local description = HarvestRoute.GetLocalization( "nodeinfounknown" )
     local angle = nil
     if MapCache and MapCache.worldX and MapCache.worldX[nodeId] then
@@ -486,7 +580,7 @@ function HarvestRoute:GetNodeDescription(nodeId)
     return description, angle
 end

-function HarvestRoute:InitTracker()
+function HarvestRoute.InitTracker()
   if isActive then
       HarvestRouteButton:SetText(HarvestRoute.GetLocalization( "buttonstoptracker" ))
       HarvestRouteTrackerButton:SetText(HarvestRoute.GetLocalization( "buttonstoptracker" ))
@@ -511,20 +605,20 @@ function HarvestRoute:InitTracker()
   end
 end

-function HarvestRoute:UpdateTracker()
-    -- HarvestRoute:debug('UpdateTracker')
+function HarvestRoute.UpdateTracker()
+    -- HarvestRoute.debug('UpdateTracker')
     if not HarvestRoute.savedVars.enableTrackerWindow then
       if isTrackerVisible then
-        HarvestRoute:SetTrackerHidden( true )
+        HarvestRoute.SetTrackerHidden( true )
       end
       return
     end
     if not isTrackerVisible then
-      HarvestRoute:debug('tracker not visible')
+      HarvestRoute.debug('tracker not visible')
       return
     end
     if isActive then
-      nearestText, nearestAngle = HarvestRoute:GetNodeDescription(closestOutOfPathId)
+      nearestText, nearestAngle = HarvestRoute.GetNodeDescription(closestOutOfPathId)
       HarvestRouteTrackerNearestNode:SetText( nearestText )
       if nearestAngle then
         HarvestRouteTrackerNearestArrow:SetHidden(false)
@@ -541,7 +635,7 @@ function HarvestRoute:UpdateTracker()
       if isActive then
         HarvestRouteTrackerLastPathNodeTitle:SetHidden( false )
         HarvestRouteTrackerLastPathNode:SetHidden( false )
-        pathText, pathAngle = HarvestRoute:GetNodeDescription(pathNodeId)
+        pathText, pathAngle = HarvestRoute.GetNodeDescription(pathNodeId)
         HarvestRouteTrackerLastPathNode:SetText( pathText )
         if pathAngle then
           HarvestRouteTrackerPathArrow:SetHidden(false)
@@ -559,7 +653,7 @@ function HarvestRoute:UpdateTracker()

 end

-function HarvestRoute:debug(text)
+function HarvestRoute.debug(text)
   --CHAT_SYSTEM:AddMessage(text)
 end