--[[ ******************************************************************************** Routes v1.6.7 16 October 2014 (Originally written for Live Servers v4.3.0.15050) (Hotfixed for v6.0.2.19034) Author: Xaroz @ EU Emerald Dream Alliance & Xinhuan @ US Blackrock Alliance ******************************************************************************** Description: Routes allow you to draw lines on the worldmap linking nodes together into an efficient farming route from existing databases. The route will be shown (by default) on the minimap and zone map as well. Features: - Select node-types to build a line upon. The following are supported * Cartographer_Fishing * Cartographer_Mining * Cartographer_Herbalism * Cartographer_ExtractGas * Cartographer_Treasure * GatherMate * GatherMate2 * Gatherer * HandyNotes - Optimize your route using the traveling salesmen problem (TSP) ant colony optimization (ACO) algorithm - Background (nonblocking) and foreground (blocking) optimization - Select color/thickness/transparency/visibility for each route - For any route created, finding a new node will try to add that as optimal as possible - Quick clustering algorithm to merge nearby nodes into a single traveling point - Quickly mark entire areas/regions as "out of bounds" or "taboo" to Routes, meaning your routes will ignore nodes in those areas and avoid cross them - Fubar plugin available to quickly access your routes - Cartographer_Waypoints and TomTom support for quickly following a route - Works with Chinchilla's Expander minimap and SexyMap's HudMap! - Full in-game help file and FAQ, guiding you step by step on what to do! Download: The latest version of Routes is always available on - http://www.wowace.com/projects/routes/ - http://wow.curse.com/downloads/wow-addons/details/routes.aspx - http://www.wowinterface.com/downloads/info11401-Routes.html Localization: You can contribute by updating/adding localizations using the system on - http://www.wowace.com/projects/routes/localization/ Contact: If you find any bugs or have any suggestions, you can contact us on: Forum: http://forums.wowace.com/showthread.php?t=10369 IRC : Grum or Xinhuan on irc://irc.freenode.org/wowace Email: Grum ( routes AT grum DOT nl ) Xinhuan ( xinhuan AT gmail DOT com ) Paypal donations are welcome ;) ]] Routes = LibStub("AceAddon-3.0"):NewAddon("Routes", "AceConsole-3.0", "AceEvent-3.0", "AceHook-3.0") local Routes = Routes local L = LibStub("AceLocale-3.0"):GetLocale("Routes", false) local G = {} -- was Graph-1.0, but we removed the dependency Routes.G = G Routes.Dragons = LibStub("HereBeDragons-2.0") local WoW90 = select(4, GetBuildInfo()) >= 90000 -- database defaults local db local defaults = { global = { routes = { ['*'] = { -- zone name, stored as the MapFile string constant ['*'] = { -- route name route = {}, -- point, point, point color = nil, -- defaults to db.defaults.color if nil width = nil, -- defaults to db.defaults.width if nil width_minimap = nil, -- defaults to db.defaults.width_minimap if nil width_battlemap = nil, -- defaults to db.defaults.width_battlemap if nil hidden = false, -- boolean looped = 1, -- looped? 1 is used (instead of true) because initial early code used 1 inside route creation code visible = true, -- visible? length = 0, -- length selection = { ['**'] = false -- Node we're interested in tracking }, db_type = { ['**'] = false -- db_types used for use with auto show/hide }, taboos = { ['**'] = false -- taboo regions in effect }, taboolist = {} -- point, point, point }, }, }, taboo = { ['*'] = { -- zone name, stored as the MapFile string constant ['*'] = { -- route name route = {}, -- point, point, point }, }, }, defaults = { -- r, g, b, a color = { 1, 0.75, 0.75, 1 }, hidden_color = { 1, 1, 1, 0.5 }, width = 30, width_minimap = 25, width_battlemap = 15, show_hidden = false, update_distance = 1, fake_point = -1, fake_data = 'dummy', draw_minimap = true, draw_worldmap = true, draw_battlemap = true, draw_indoors = false, tsp = { initial_pheromone = 0.1, -- Initial pheromone trail value alpha = 1, -- Likelihood of ants to follow pheromone trails (larger value == more likely) beta = 6, -- Likelihood of ants to choose closer nodes (larger value == more likely) local_decay = 0.2, -- Governs local trail decay rate [0, 1] local_update = 0.4, -- Amount of pheromone to reinforce local trail update by global_decay = 0.2, -- Governs global trail decay rate [0, 1] twoopt_passes = 3, -- Number of times to perform 2-opt passes two_point_five_opt = false, -- Perform optimized 2-opt pass }, prof_options = { ['*'] = "Always", }, use_auto_showhide = false, waypoint_hit_distance = 50, line_gaps = true, line_gaps_skip_cluster = true, cluster_dist = 60, callbacks = { ['*'] = true } }, } } -- Ace Options Table for our addon local options -- Plugins table Routes.plugins = {} -- Lookup table for aceoptkey-route/taboo conversion Routes.routekeys = setmetatable({}, { __index = function(t,k) if k == "string" and tonumber(k) then return t[tonumber(k)] end return nil end }) Routes.tabookeys = setmetatable({}, { __index = function(t,k) if k == "string" and tonumber(k) then return t[tonumber(k)] end return nil end }) -- localize some globals local pairs, next = pairs, next local tinsert, tremove = tinsert, tremove local floor = math.floor local format = string.format local math_abs = math.abs local math_sin = math.sin local math_cos = math.cos local Minimap = Minimap local GetPlayerFacing = GetPlayerFacing ------------------------------------------------------------------------------------------------------ -- Data for Localized Zone Names local function GetZoneName(uiMapID) local name = Routes.Dragons:GetLocalizedMap(uiMapID) if uiMapID == 104 or uiMapID == 107 then -- Outland SMV and Nagrand name = format("%s (%s)", name, Routes.Dragons:GetLocalizedMap(101)) elseif uiMapID == 125 then -- Northrend Dalaran name = format("%s (%s)", name, Routes.Dragons:GetLocalizedMap(113)) end return name end local function GetZoneNameSafe(uiMapID) local name = GetZoneName(uiMapID) return name or ("Zone #%s"):format(tostring(uiMapID)) end Routes.LZName = setmetatable({}, { __index = function() return 0 end}) local function processMapChildrenRecursive(parent) local children = C_Map.GetMapChildrenInfo(parent) if children and #children > 0 then for i = 1, #children do local id = children[i].mapID if id then if children[i].mapType == Enum.UIMapType.Zone or children[i].mapType == Enum.UIMapType.Continent then local name = GetZoneName(id) Routes.LZName[name] = id processMapChildrenRecursive(id) elseif children[i].mapType == Enum.UIMapType.World then processMapChildrenRecursive(id) end end end end end local COSMIC_MAP_ID = 946 local WORLD_MAP_ID = 947 if WOW_PROJECT_ID == WOW_PROJECT_CLASSIC then processMapChildrenRecursive(WORLD_MAP_ID) else processMapChildrenRecursive(COSMIC_MAP_ID) end ------------------------------------------------------------------------------------------------------ -- Core Routes functions --[[ Our coordinate format for Routes Warning: These are convenience functions, most of the :getXY() and :getID() code are inlined in critical code paths in various functions, changing the coord storage format requires changing the inlined code in numerous locations in addition to these 2 functions ]] function Routes:getID(x, y) return floor(x * 10000 + 0.5) * 10000 + floor(y * 10000 + 0.5) end function Routes:getXY(id) return floor(id / 10000) / 10000, (id % 10000) / 10000 end local MinimapShapes = { -- quadrant booleans (same order as SetTexCoord) -- {upper-left, lower-left, upper-right, lower-right} -- true = rounded, false = squared ["ROUND"] = { true, true, true, true}, ["SQUARE"] = {false, false, false, false}, ["CORNER-TOPLEFT"] = { true, false, false, false}, ["CORNER-TOPRIGHT"] = {false, false, true, false}, ["CORNER-BOTTOMLEFT"] = {false, true, false, false}, ["CORNER-BOTTOMRIGHT"] = {false, false, false, true}, ["SIDE-LEFT"] = { true, true, false, false}, ["SIDE-RIGHT"] = {false, false, true, true}, ["SIDE-TOP"] = { true, false, true, false}, ["SIDE-BOTTOM"] = {false, true, false, true}, ["TRICORNER-TOPLEFT"] = { true, true, true, false}, ["TRICORNER-TOPRIGHT"] = { true, false, true, true}, ["TRICORNER-BOTTOMLEFT"] = { true, true, false, true}, ["TRICORNER-BOTTOMRIGHT"] = {false, true, true, true}, } local minimap_radius local minimap_rotate local indoors = "indoor" local MinimapSize = { -- radius of minimap indoor = { [0] = 150, [1] = 120, [2] = 90, [3] = 60, [4] = 40, [5] = 25, }, outdoor = { [0] = 233 + 1/3, [1] = 200, [2] = 166 + 2/3, [3] = 133 + 1/3, [4] = 100, [5] = 66 + 2/3, }, } local function is_round( dx, dy ) local map_shape = GetMinimapShape and GetMinimapShape() or "ROUND" local q = 1 if dx > 0 then q = q + 2 end -- right side -- XXX Tripple check this if dy > 0 then q = q + 1 end -- bottom side return MinimapShapes[map_shape][q] end local function is_inside( sx, sy, cx, cy, radius ) local dx = sx - cx local dy = sy - cy if is_round( dx, dy ) then return dx*dx+dy*dy <= radius*radius else return math_abs( dx ) <= radius and math_abs( dy ) <= radius end end local function GetFacing() if GetPlayerFacing then return GetPlayerFacing() end return -MiniMapCompassRing:GetFacing() end local last_X, last_Y, last_facing = math.huge, math.huge, math.huge -- implementation of cache - use zone in the key for an unique identifier -- because every zone has a different X/Y location and possible yardsizes local cache_zone, cache_zoneW, cache_zoneH local X_cache = {} local Y_cache = {} local XY_cache_mt = { __index = function(t, key) local zone, coord = (';'):split( key ) if cache_zone ~= zone then cache_zoneW, cache_zoneH = Routes.Dragons:GetZoneSize(tonumber(zone)) cache_zone = zone end local X = cache_zoneW * floor(coord / 10000) / 10000 local Y = cache_zoneH * (coord % 10000) / 10000 X_cache[key] = X Y_cache[key] = Y -- figure out which one to return if t == X_cache then return X else return Y end end } setmetatable( X_cache, XY_cache_mt ) setmetatable( Y_cache, XY_cache_mt ) function Routes:DrawMinimapLines(forceUpdate) if not db.defaults.draw_minimap then G:HideLines(Minimap) return end local _x, _y, currentZoneID = self.Dragons:GetPlayerZonePosition(true) -- invalid coordinates - clear map if not _x or not _y then G:HideLines(Minimap) return end -- if we are indoors, or the zone we are in is not defined in our tables ... no routes -- double check zoom as onload doesnt get you the map zoom indoors = GetCVar("minimapZoom")+0 == Minimap:GetZoom() and "outdoor" or "indoor" if not db.defaults.draw_indoors and indoors == "indoor" then G:HideLines(Minimap) return end local defaults = db.defaults local zoneW, zoneH = self.Dragons:GetZoneSize(currentZoneID) if not zoneW or zoneW == 0 then return end local cx, cy = zoneW * _x, zoneH * _y local facing, sin, cos if minimap_rotate then facing = GetFacing() end if (not forceUpdate) and facing == last_facing and (last_X-cx)^2 + (last_Y-cy)^2 < defaults.update_distance^2 then -- no update! return end G:HideLines(Minimap) last_X = cx last_Y = cy last_facing = facing if minimap_rotate then sin = math_sin(facing) cos = math_cos(facing) end if WoW90 then minimap_radius = C_Minimap.GetViewRadius() else minimap_radius = MinimapSize[indoors][Minimap:GetZoom()] end local radius = minimap_radius local radius2 = radius * radius local minX = cx - radius local maxX = cx + radius local minY = cy - radius local maxY = cy + radius local div_by_zero_nudge = 0.000001 local minimap_w = Minimap:GetWidth() local minimap_h = Minimap:GetHeight() local scale_x = minimap_w / (radius*2) local scale_y = minimap_h / (radius*2) local minimapScale = Minimap:GetScale() for route_name, route_data in pairs( db.routes[ currentZoneID ] ) do if type(route_data) == "table" and type(route_data.route) == "table" and #route_data.route > 1 then -- store color/width local width = (route_data.width_minimap or defaults.width_minimap) / (minimapScale) local color = route_data.color or defaults.color -- unless we show hidden if (not route_data.hidden and (route_data.visible or not defaults.use_auto_showhide)) or defaults.show_hidden then -- use this special color if route_data.hidden then color = defaults.hidden_color end -- some state data local last_x = nil local last_y = nil local last_inside = nil -- if we loop - make sure the 'last' gets filled with the right info if route_data.looped and route_data.route[ #route_data.route ] ~= defaults.fake_point then local key = format("%s;%s", currentZoneID, route_data.route[ #route_data.route ]) last_x, last_y = X_cache[key], Y_cache[key] if minimap_rotate then local dx = last_x - cx local dy = last_y - cy last_x = cx + dx*cos - dy*sin last_y = cy + dx*sin + dy*cos end last_inside = is_inside( last_x, last_y, cx, cy, radius ) end -- loop over the route for i = 1, #route_data.route do local point = route_data.route[i] local cur_x, cur_y, cur_inside -- if we have a 'fake point' (gap) - clear current values if point == defaults.fake_point then cur_x = nil cur_y = nil cur_inside = false else local key = format("%s;%s", currentZoneID, point) cur_x, cur_y = X_cache[key], Y_cache[key] if minimap_rotate then local dx = cur_x - cx local dy = cur_y - cy cur_x = cx + dx*cos - dy*sin cur_y = cy + dx*sin + dy*cos end cur_inside = is_inside( cur_x, cur_y, cx, cy, radius ) end -- check if we have any nil values (we cant draw) and check boundingbox if cur_x and cur_y and last_x and last_y and not ( ( cur_x < minX and last_x < minX ) or ( cur_x > maxX and last_x > maxX ) or ( cur_y < minY and last_y < minY ) or ( cur_y > maxY and last_y > maxY ) ) then -- default all to not drawing local draw_sx = nil local draw_sy = nil local draw_ex = nil local draw_ey = nil -- both inside - easy! draw if cur_inside and last_inside then draw_sx = last_x draw_sy = last_y draw_ex = cur_x draw_ey = cur_y else -- direction of line local dx = last_x - cur_x local dy = last_y - cur_y -- calculate point on perpendicular line local zx = cx - dy local zy = cy + dx -- nudge it a bit so we dont get div by 0 problems if dx == 0 then dx = div_by_zero_nudge end if dy == 0 then dy = div_by_zero_nudge end -- calculate intersection point local nd = ((cx -last_x)*(cy-zy) - (cx-zx)*(cy -last_y)) / ((cur_x-last_x)*(cy-zy) - (cx-zx)*(cur_y-last_y)) -- perpendicular point (closest to center on the line given) local px = last_x + nd * -dx local py = last_y + nd * -dy -- check range of intersect point local dpc_x = cx - px local dpc_y = cy - py -- distance^2 of the perpendicular point local lenpc = dpc_x*dpc_x + dpc_y*dpc_y -- the line can only intersect if the perpendicular point is at -- least closer than the furthest away point (one of the corners) if lenpc < 2*radius2 then -- if inside - ready to draw if cur_inside then draw_ex = cur_x draw_ey = cur_y else -- if we're not inside we can still be in the square - if so dont do any intersection -- calculations yet if math_abs( cur_x - cx ) < radius and math_abs( cur_y - cy ) < radius then draw_ex = cur_x draw_ey = cur_y else -- need to intersect against the square -- likely x/y to intersect with local minimap_cur_x = cx + radius * (dx < 0 and 1 or -1) local minimap_cur_y = cy + radius * (dy < 0 and 1 or -1) -- which intersection is furthest? local delta_cur_x = (minimap_cur_x - cur_x) / -dx local delta_cur_y = (minimap_cur_y - cur_y) / -dy -- dark magic - needs to be changed to positive signs whenever i can care about it if delta_cur_x < delta_cur_y and delta_cur_x < 0 then draw_ex = minimap_cur_x draw_ey = cur_y + -dy*delta_cur_x else draw_ex = cur_x + -dx*delta_cur_y draw_ey = minimap_cur_y end -- check if we didn't calculate some wonky offset - has to be inside with -- some slack on accuracy if math_abs( draw_ex - cx ) > radius*1.01 or math_abs( draw_ey - cy ) > radius*1.01 then draw_ex = nil draw_ey = nil end end -- we might have a round corner here - lets see if the quarter with the intersection is round if draw_ex and draw_ey and is_round( draw_ex - cx, draw_ey - cy ) then -- if we are also within the circle-range if lenpc < radius2 then -- circle intersection local dcx = cx - cur_x local dcy = cy - cur_y local len_dc = dcx*dcx + dcy*dcy local len_d = dx*dx + dy*dy local len_ddc = dx*dcx + dy*dcy -- discriminant local d_sqrt = ( len_ddc*len_ddc - len_d * (len_dc - radius2) )^0.5 -- calculate point draw_ex = cur_x - dx * (-len_ddc + d_sqrt) / len_d draw_ey = cur_y - dy * (-len_ddc + d_sqrt) / len_d -- have to be on the *same* side of the perpendicular point else it's fake if (draw_ex - px)/math_abs(draw_ex - px) ~= (cur_x- px)/math_abs(cur_x - px) or (draw_ey - py)/math_abs(draw_ey - py) ~= (cur_y- py)/math_abs(cur_y - py) then draw_ex = nil draw_ey = nil end else draw_ex = nil draw_ey = nil end end end -- if inside - ready to draw if last_inside then draw_sx = last_x draw_sy = last_y else -- if we're not inside we can still be in the square - if so dont do any intersection -- calculations yet if math_abs( last_x - cx ) < radius and math_abs( last_y - cy ) < radius then draw_sx = last_x draw_sy = last_y else -- need to intersect against the square -- likely x/y to intersect with local minimap_last_x = cx + radius * (dx > 0 and 1 or -1) local minimap_last_y = cy + radius * (dy > 0 and 1 or -1) -- which intersection is furthest? local delta_last_x = (minimap_last_x - last_x) / dx local delta_last_y = (minimap_last_y - last_y) / dy -- dark magic - needs to be changed to positive signs whenever i can care about it if delta_last_x < delta_last_y and delta_last_x < 0 then draw_sx = minimap_last_x draw_sy = last_y + dy*delta_last_x else draw_sx = last_x + dx*delta_last_y draw_sy = minimap_last_y end -- check if we didn't calculate some wonky offset - has to be inside with -- some slack on accuracy if math_abs( draw_sx - cx ) > radius*1.01 or math_abs( draw_sy - cy ) > radius*1.01 then draw_sx = nil draw_sy = nil end end -- we might have a round corner here - lets see if the quarter with the intersection is round if draw_sx and draw_sy and is_round( draw_sx - cx, draw_sy - cy ) then -- if we are also within the circle-range if lenpc < radius2 then -- circle intersection local dcx = cx - cur_x local dcy = cy - cur_y local len_dc = dcx*dcx + dcy*dcy local len_d = dx*dx + dy*dy local len_ddc = dx*dcx + dy*dcy -- discriminant local d_sqrt = ( len_ddc*len_ddc - len_d * (len_dc - radius2) )^0.5 -- calculate point draw_sx = cur_x - dx * (-len_ddc - d_sqrt) / len_d draw_sy = cur_y - dy * (-len_ddc - d_sqrt) / len_d -- have to be on the *same* side of the perpendicular point else it's fake if (draw_sx - px)/math_abs(draw_sx - px) ~= (last_x- px)/math_abs(last_x - px) or (draw_sy - py)/math_abs(draw_sy - py) ~= (last_y- py)/math_abs(last_y - py) then draw_sx = nil draw_sy = nil end else draw_sx = nil draw_sy = nil end end end end end if draw_sx and draw_sy and draw_ex and draw_ey then -- translate to left bottom corner and apply scale draw_sx = (draw_sx - minX) * scale_x draw_sy = minimap_h - (draw_sy - minY) * scale_y draw_ex = (draw_ex - minX) * scale_x draw_ey = minimap_h - (draw_ey - minY) * scale_y if defaults.line_gaps then -- shorten the line by 5 pixels (scaled) on endpoints inside the Minimap local gapConst = 5 / minimapScale local dx = draw_sx - draw_ex local dy = draw_sy - draw_ey local l = (dx*dx + dy*dy)^0.5 local x = gapConst * dx / l local y = gapConst * dy / l local shorten1, shorten2 if last_inside then shorten1 = true else shorten1 = false end if cur_inside then shorten2 = true else shorten2 = false end if shorten2 and route_data.metadata and defaults.line_gaps_skip_cluster and #route_data.metadata[i] > 1 then shorten2 = false end if shorten1 and route_data.metadata and defaults.line_gaps_skip_cluster and #route_data.metadata[(i-1 == 0) and #route_data.route or i-1] > 1 then shorten1 = false end if shorten1 and shorten2 and l > (gapConst*2) then -- draw if line is 10 or more pixels (scaled) G:DrawLine( Minimap, draw_sx-x, draw_sy-y, draw_ex+x, draw_ey+y, width, color, "ARTWORK") elseif shorten1 and not shorten2 and l > gapConst then G:DrawLine( Minimap, draw_sx-x, draw_sy-y, draw_ex, draw_ey, width, color, "ARTWORK") elseif shorten2 and not shorten1 and l > gapConst then G:DrawLine( Minimap, draw_sx, draw_sy, draw_ex+x, draw_ey+y, width, color, "ARTWORK") elseif not shorten1 and not shorten2 then G:DrawLine( Minimap, draw_sx, draw_sy, draw_ex, draw_ey, width, color, "ARTWORK") end else G:DrawLine( Minimap, draw_sx, draw_sy, draw_ex, draw_ey, width, color, "ARTWORK") end end end -- store last point last_x = cur_x last_y = cur_y last_inside = cur_inside end end end end end -- This frame is to throttle InsertNode() and DeleteNode() calls so that -- redrawing the map lines are delayed by 1 frame. These 2 functions can -- potentially be spammed by a source database importing nodes. local throttleFrame = CreateFrame("Frame") throttleFrame:Hide() throttleFrame:SetScript("OnUpdate", function(self, elapsed) Routes:DrawWorldmapLines() Routes:DrawMinimapLines(true) self:Hide() end) -- Accepts a zone name, coord and node_name -- for inserting into relevant routes -- Zone name must be localized, node_name can be english or localized function Routes:InsertNode(zone, coord, node_name) for route_name, route_data in pairs( db.routes[ self.LZName[zone] ] ) do -- for every route check if the route is created with this node if route_data.selection then for k, v in pairs(route_data.selection) do if k == node_name or v == node_name then -- Add the node local x, y = self:getXY(coord) local flag = false for tabooname, used in pairs(route_data.taboos) do if used and self:IsNodeInTaboo(x, y, db.taboo[ self.LZName[zone] ][tabooname]) then flag = true end end if flag then tinsert(route_data.taboolist, coord) else route_data.length = self.TSP:InsertNode(route_data.route, route_data.metadata, self.LZName[zone], coord, route_data.cluster_dist or 65) -- 65 is the old default throttleFrame:Show() end break end end end end end -- Accepts a zone name, coord and node_name -- for deleting into relevant routes -- Zone name must be localized, node_name can be english or localized function Routes:DeleteNode(zone, coord, node_name) for route_name, route_data in pairs( db.routes[ self.LZName[zone] ] ) do -- for every route check if the route is created with this node if route_data.selection then local flag = false for k, v in pairs(route_data.selection) do if k == node_name or v == node_name then -- Delete the node if it exists in this route if route_data.metadata then -- this is a clustered route for i = 1, #route_data.route do local num_data = #route_data.metadata[i] for j = 1, num_data do if coord == route_data.metadata[i][j] then -- recalcuate centroid local x, y = self:getXY(coord) local cx, cy = self:getXY(route_data.route[i]) if num_data > 1 then -- more than 1 node in this cluster cx, cy = (cx * num_data - x) / (num_data-1), (cy * num_data - y) / (num_data-1) tremove(route_data.metadata[i], j) route_data.route[i] = self:getID(cx, cy) else -- only 1 node in this cluster, just remove it tremove(route_data.metadata, i) tremove(route_data.route, i) end route_data.length = self.TSP:PathLength(route_data.route, self.LZName[zone]) throttleFrame:Show() flag = true break end end if flag then break end end else -- this is not a clustered route for i = 1, #route_data.route do if coord == route_data.route[i] then tremove(route_data.route, i) route_data.length = self.TSP:PathLength(route_data.route, self.LZName[zone]) throttleFrame:Show() flag = true break end end end if not flag then -- node not found yet, so search the taboolist for i = 1, #route_data.taboolist do if route_data.taboolist[i] == coord then tremove(route_data.taboolist, i) flag = true break end end end end if flag then break end end end end end -- This function upgrades the Routes old storage format which used mapFiles -- to the new format using uiMapIDs in WoW 8.0 local HBDMigrate = LibStub("HereBeDragons-Migrate") function Routes:UpgradeStorageFormat2() local t = {} for zone, zone_table in pairs(db.routes) do if type(zone) == "string" then -- This zone is a string, not a uiMapID, obtain the ID from HBD-Migrate local uiMapID = HBDMigrate:GetUIMapIDFromMapFile(zone) -- if no mapfile was found, maybe this was a named zone from way before if not uiMapID then uiMapID = self.LZName[zone] -- 0 is invalid from the metatable if uiMapID == 0 then uiMapID = nil end end if not uiMapID then -- invalid zone, delete the whole zone db.routes[zone] = nil else -- We found a match, store the zone_table temporarily first -- and delete the whole zone (because we cannot insert new -- keys into db.routes[] while iterating over it) t[uiMapID] = zone_table db.routes[zone] = nil end end end for uiMapID, zone_table in pairs(t) do -- Now assign the new zone uiMapID keys db.routes[uiMapID] = zone_table end table.wipe(t) -- Do the same with the taboo table for zone, zone_table in pairs(db.taboo) do if type(zone) == "string" then -- This zone is a string, not a uiMapID, obtain the ID from HBD-Migrate local uiMapID = HBDMigrate:GetUIMapIDFromMapFile(zone) -- if no mapfile was found, maybe this was a named zone from way before if not uiMapID then uiMapID = self.LZName[zone] -- 0 is invalid from the metatable if uiMapID == 0 then uiMapID = nil end end if not uiMapID then -- invalid zone, delete the whole zone db.taboo[zone] = nil else -- We found a match, store the zone_table temporarily first -- and delete the whole zone (because we cannot insert new -- keys into db.routes[] while iterating over it) t[uiMapID] = zone_table db.taboo[zone] = nil end end end for uiMapID, zone_table in pairs(t) do -- Now assign the new zone uiMapID keys db.taboo[uiMapID] = zone_table end -- Reclaim memory for this function self.UpgradeStorageFormat2 = nil end -- Common subtables for zone and table description local route_zone_args_desc_table = { type = "description", name = function(info) local zone = tonumber(info[2]) local count = 0 for route_name, route_table in pairs(db.routes[zone]) do if #route_table.route > 0 then count = count + 1 end end return L["You have |cffffd200%d|r route(s) in |cffffd200%s|r."]:format(count, GetZoneNameSafe(zone)) end, order = 0, } local taboo_zone_args_desc_table = { type = "description", name = function(info) local zone = tonumber(info[2]) local count = 0 for taboo_name, taboo_table in pairs(db.taboo[zone]) do if #taboo_table.route > 0 then count = count + 1 end end return L["You have |cffffd200%d|r taboo region(s) in |cffffd200%s|r."]:format(count, GetZoneNameSafe(zone)) end, order = 0, } ------------------------------------------------------------------------------------------------------ -- General event functions function Routes:OnInitialize() -- Initialize database self.db = LibStub("AceDB-3.0"):New("RoutesDB", defaults, true) db = self.db.global self.options = options -- Initialize the ace options table LibStub("AceConfigRegistry-3.0"):RegisterOptionsTable("Routes", options) local f = function() LibStub("AceConfigDialog-3.0"):Open("Routes") end self:RegisterChatCommand(L["routes"], f) if L["routes"] ~= "routes" then self:RegisterChatCommand("routes", f) end -- Upgrade old storage format (which was dependant on LibBabble-Zone-3.0 -- to the new format that doesn't require it -- Also delete any invalid zones self:UpgradeStorageFormat2() -- Generate ace options table for each route local opts = options.args.routes_group.args for zone, zone_table in pairs(db.routes) do if next(zone_table) == nil then -- cleanup the empty zone db.routes[zone] = nil else local localizedZoneName = GetZoneNameSafe(zone) opts[tostring(zone)] = { type = "group", name = localizedZoneName, desc = L["Routes in %s"]:format(localizedZoneName), args = { desc = route_zone_args_desc_table, }, } self.routekeys[zone] = {} for route, route_table in pairs(zone_table) do local routekey = route:gsub("%s", "\255") -- can't have spaces in the key self.routekeys[zone][routekey] = route opts[tostring(zone)].args[routekey] = self:GetAceOptRouteTable() route_table.editing = nil -- in case server crashes during edit. end end end -- Generate ace options table for each taboo region local opts = options.args.taboo_group.args for zone, zone_table in pairs(db.taboo) do if next(zone_table) == nil then -- cleanup the empty zone db.taboo[zone] = nil else local localizedZoneName = GetZoneNameSafe(zone) opts[tostring(zone)] = { type = "group", name = localizedZoneName, desc = L["Taboos in %s"]:format(localizedZoneName), args = { desc = taboo_zone_args_desc_table, }, } self.tabookeys[zone] = {} for taboo in pairs(zone_table) do local tabookey = taboo:gsub("%s", "\255") -- can't have spaces in the key self.tabookeys[zone][tabookey] = taboo opts[tostring(zone)].args[tabookey] = self:GetAceOptTabooTable() end end end self:SetupSourcesOptTables() self:RegisterEvent("ADDON_LOADED") -- Reclaim memory for this function self.OnInitialize = nil end local timerFrame = CreateFrame("Frame") timerFrame:Hide() timerFrame.elapsed = 0 timerFrame:SetScript("OnUpdate", function(self, elapsed) self.elapsed = self.elapsed + elapsed if self.elapsed > 0.025 or self.force then -- throttle to a max of 40 redraws per sec self.elapsed = 0 -- kinda unnecessary since at default 1 yard refresh, its limited to 36 redraws/sec Routes:DrawMinimapLines(self.force) -- only need 25 redraws/sec to perceive smooth motion anyway self.force = nil end end) local function SetZoomHook() timerFrame.force = true end function Routes:MINIMAP_UPDATE_ZOOM() if not WoW90 then local zoom = Minimap:GetZoom() if GetCVar("minimapZoom") == GetCVar("minimapInsideZoom") then Minimap:SetZoom(zoom < 2 and zoom + 1 or zoom - 1) end indoors = GetCVar("minimapZoom")+0 == Minimap:GetZoom() and "outdoor" or "indoor" Minimap:SetZoom(zoom) end timerFrame.force = true end function Routes:CVAR_UPDATE(event, cvar, value) if cvar == "ROTATE_MINIMAP" then minimap_rotate = value == "1" end end local RoutesDataProviderMixin = CreateFromMixins(MapCanvasDataProviderMixin) function RoutesDataProviderMixin:OnAdded(mapCanvas) MapCanvasDataProviderMixin.OnAdded(self, mapCanvas) self:GetMap():GetPinFrameLevelsManager():InsertFrameLevelAbove("PIN_FRAME_LEVEL_ROUTES", "PIN_FRAME_LEVEL_FOG_OF_WAR") -- a single permanent pin local pin = self:GetMap():AcquirePin("RoutesPinTemplate", self.battleField) pin:SetPosition(0.5, 0.5); self.pin = pin; -- Taboo pin for the main map if not self.battleField then self:GetMap():GetPinFrameLevelsManager():AddFrameLevel("PIN_FRAME_LEVEL_ROUTES_TABOO") self.tabooPin = self:GetMap():AcquirePin("RoutesTabooPinTemplate") self.tabooPin:SetPosition(0.5, 0.5) end end function RoutesDataProviderMixin:OnRemoved(mapCanvas) MapCanvasDataProviderMixin.OnRemoved(self, mapCanvas); self:GetMap():RemoveAllPinsByTemplate("RoutesPinTemplate"); self:GetMap():RemoveAllPinsByTemplate("RoutesTabooPinTemplate"); end function RoutesDataProviderMixin:OnMapChanged() self:RefreshAllData() end function RoutesDataProviderMixin:RemoveAllData() G:HideLines(self.pin) end function RoutesDataProviderMixin:RefreshAllData() self.pin:DrawLines() end RoutesPinMixin = CreateFromMixins(MapCanvasPinMixin) function RoutesPinMixin:OnLoad() self:SetIgnoreGlobalPinScale(true) self:UseFrameLevelType("PIN_FRAME_LEVEL_ROUTES") end function RoutesPinMixin:OnAcquired(battleField) if battleField then self.width_key = "width_battlemap" self.draw_key = "draw_battlemap" else self.width_key = "width" self.draw_key = "draw_worldmap" end end function RoutesPinMixin:OnCanvasSizeChanged() self:SetSize(self:GetMap():DenormalizeHorizontalSize(1.0), self:GetMap():DenormalizeVerticalSize(1.0)); end function RoutesPinMixin:DrawLines() -- setup locals local fh, fw = self:GetHeight(), self:GetWidth() local defaults = db.defaults -- clear all the lines G:HideLines(self) -- get the scale of the worldmap canvas, so the lines have the same size everywhere local canvasScale = self:GetEffectiveScale() / self:GetMap():GetEffectiveScale() -- check for conditions not to draw the world map lines local flag = defaults[self.draw_key] and self:GetMap():IsShown() -- Draw worldmap lines? if not flag then return end -- Nothing to draw local uiMapID = self:GetMap():GetMapID() if not uiMapID then return end for route_name, route_data in pairs( db.routes[uiMapID] ) do if type(route_data) == "table" and type(route_data.route) == "table" and #route_data.route > 1 then local width = (route_data[self.width_key] or defaults[self.width_key]) / canvasScale local color = route_data.color or defaults.color if (not route_data.hidden and not route_data.editing and (route_data.visible or not defaults.use_auto_showhide)) or defaults.show_hidden then if route_data.hidden then color = defaults.hidden_color end local last_point local sx, sy if route_data.looped then last_point = route_data.route[ #route_data.route ] sx, sy = floor(last_point / 10000) / 10000, (last_point % 10000) / 10000 sy = (1 - sy) end for i = 1, #route_data.route do local point = route_data.route[i] if point == defaults.fake_point then point = nil end if last_point and point then local ex, ey = floor(point / 10000) / 10000, (point % 10000) / 10000 ey = (1 - ey) G:DrawLine(self, sx*fw, sy*fh, ex*fw, ey*fh, width, color , "OVERLAY") sx, sy = ex, ey end last_point = point end end end end end RoutesTabooPinMixin = CreateFromMixins(MapCanvasPinMixin) function RoutesTabooPinMixin:OnLoad() self:SetIgnoreGlobalPinScale(true) self:UseFrameLevelType("PIN_FRAME_LEVEL_ROUTES_TABOO") end function RoutesTabooPinMixin:OnCanvasSizeChanged() self:SetSize(self:GetMap():DenormalizeHorizontalSize(1.0), self:GetMap():DenormalizeVerticalSize(1.0)); end function Routes:DrawWorldmapLines() self.DataProvider:RefreshAllData() if self.BattleFieldDataProvider then self.BattleFieldDataProvider:RefreshAllData() end end function Routes:OnEnable() -- World Map line drawing if not self.DataProvider then self.DataProvider = CreateFromMixins(RoutesDataProviderMixin) end WorldMapFrame:AddDataProvider(self.DataProvider) -- Battlefield Map if BattlefieldMapFrame then self:ADDON_LOADED("ADDON_LOADED", "Blizzard_BattlefieldMap") end -- Minimap line drawing if not WoW90 then self:SecureHook(Minimap, "SetZoom", SetZoomHook) end if db.defaults.draw_minimap then self:RegisterEvent("MINIMAP_UPDATE_ZOOM") self:RegisterEvent("CVAR_UPDATE") timerFrame:Show() Routes.Dragons.RegisterCallback(Routes, "PlayerZoneChanged", function() Routes:DrawMinimapLines(true) end) minimap_rotate = GetCVar("rotateMinimap") == "1" self:MINIMAP_UPDATE_ZOOM() end for addon, plugin_table in pairs(Routes.plugins) do if db.defaults.callbacks[addon] and plugin_table.IsActive() then plugin_table.AddCallbacks() end end end function Routes:OnDisable() -- Ace3 unregisters all events and hooks for us on disable for addon, plugin_table in pairs(Routes.plugins) do if db.defaults.callbacks[addon] and plugin_table.IsActive() then plugin_table.RemoveCallbacks() end end timerFrame:Hide() WorldMapFrame:RemoveDataProvider(self.DataProvider) if BattlefieldMapFrame then BattlefieldMapFrame:RemoveDataProvider(self.BattleFieldDataProvider) end end function Routes:ADDON_LOADED(event, addon) if self.plugins[addon] then options.args.add_group.args[addon].disabled = false options.args.add_group.args[addon].guiHidden = false if db.defaults.callbacks[addon] and self.plugins[addon].IsActive() then self.plugins[addon].AddCallbacks() end end if addon == "Blizzard_BattlefieldMap" then if not self.BattleFieldDataProvider then self.BattleFieldDataProvider = CreateFromMixins(RoutesDataProviderMixin) self.BattleFieldDataProvider.battleField = true end BattlefieldMapFrame:AddDataProvider(self.BattleFieldDataProvider) end end ------------------------------------------------------------------------------------------------------ -- Ace options table stuff do -- Helper functions for setting/clearing keybinds in our option tables local KeybindHelper = {} Routes.KeybindHelper = KeybindHelper local t = {} function KeybindHelper:MakeKeyBindingTable(...) wipe(t) for i = 1, select("#", ...) do local key = select(i, ...) if key ~= "" then tinsert(t, key) end end return t end function KeybindHelper:GetKeybind(info) return table.concat(self:MakeKeyBindingTable(GetBindingKey(info.arg)), ", ") end function KeybindHelper:SetKeybind(info, key) if key == "" then local t = self:MakeKeyBindingTable(GetBindingKey(info.arg)) for i = 1, #t do SetBinding(t[i]) end else local oldAction = GetBindingAction(key) local frame = LibStub("AceConfigDialog-3.0").OpenFrames["Routes"] if frame then if ( oldAction ~= "" and oldAction ~= info.arg ) then frame:SetStatusText(KEY_UNBOUND_ERROR:format(GetBindingText(oldAction, "BINDING_NAME_"))) else frame:SetStatusText(KEY_BOUND) end end SetBinding(key, info.arg) end SaveBindings(GetCurrentBindingSet()) end end options = { type = "group", name = L["Routes"], get = function(k) return db.defaults[k.arg] end, set = function(k, v) db.defaults[k.arg] = v; Routes:DrawWorldmapLines(); Routes:DrawMinimapLines(true); end, args = { options_group = { type = "group", name = L["Options"], desc = L["Options"], order = 0, --args = {}, -- defined later }, add_group = { type = "group", name = L["Add"], desc = L["Add"], order = 100, --args = {}, -- defined later }, routes_group = { type = "group", name = L["Routes"], desc = L["Routes"], order = 200, args = {}, -- populated in Routes:OnInitialize() }, taboo_group = { type = "group", name = L["Taboos"], desc = L["Taboos"], order = 250, args = {}, -- populated in Routes:OnInitialize() }, faq_group = { type = "group", name = L["Help File"], desc = L["Help File"], order = 300, args = { overview = { type = "group", name = L["Overview"], desc = L["Overview"], order = 10, args = { header = { type = "header", name = L["Overview"], order = 0, }, desc = { type = "description", name = L["OVERVIEW_TEXT"], order = 1, }, }, }, create_route = { type = "group", name = L["Creating a route"], desc = L["Creating a route"], order = 20, args = { header = { type = "header", name = L["Creating a route"], order = 0, }, desc = { type = "description", name = L["CREATE_ROUTE_TEXT"], order = 1, }, }, }, optimizing_route = { type = "group", name = L["Optimizing a route"], desc = L["Optimizing a route"], order = 30, args = { header = { type = "header", name = L["Optimizing a route"], order = 0, }, desc = { type = "description", name = L["OPTIMIZING_ROUTE_TEXT"], order = 1, }, }, }, customizing_route = { type = "group", name = L["Customizing route display"], desc = L["Customizing route display"], order = 40, args = { header = { type = "header", name = L["Customizing route display"], order = 0, }, desc = { type = "description", name = L["CUSTOMIZING_ROUTE_TEXT"], order = 1, }, }, }, create_taboos = { type = "group", name = L["Creating a taboo region"], desc = L["Creating a taboo region"], order = 50, args = { header = { type = "header", name = L["Creating a taboo region"], order = 0, }, desc = { type = "description", name = L["CREATE_TABOOS_TEXT"], order = 1, }, }, }, waypoints_integration = { type = "group", name = L["Waypoints Integration"], desc = L["Waypoints Integration"], order = 60, args = { header = { type = "header", name = L["Waypoints Integration"], order = 0, }, desc = { type = "description", name = L["WAYPOINTS_INTEGRATION_TEXT"], order = 1, }, }, }, auto_update = { type = "group", name = L["Automatic route updating"], desc = L["Automatic route updating"], order = 70, args = { header = { type = "header", name = L["Automatic route updating"], order = 0, }, desc = { type = "description", name = L["AUTOMATIC_UPDATE_TEXT"], order = 1, }, }, }, faq = { type = "group", name = L["FAQ"], desc = L["Frequently Asked Questions"], order = 100, args = { header = { type = "header", name = L["Frequently Asked Questions"], order = 0, }, desc = { type = "description", name = L["FAQ_TEXT"], order = 1, }, }, }, }, }, } } options.args.options_group.args = { -- Mapdrawing menu entry drawing = { name = L["Map Drawing"], type = "group", desc = L["Map Drawing"], order = 100, args = { linedisplay_group = { name = L["Toggle drawing on each of the maps."], type = "group", desc = L["Toggle drawing on each of the maps."], inline = true, order = 100, args = { worldmap_toggle = { name = L["Worldmap"], desc = L["Worldmap drawing"], type = "toggle", order = 100, arg = "draw_worldmap", }, minimap_toggle = { name = L["Minimap"], desc = L["Minimap drawing"], type = "toggle", order = 200, get = function(info) return db.defaults.draw_minimap end, set = function(info, v) db.defaults.draw_minimap = v if v then Routes:RegisterEvent("MINIMAP_UPDATE_ZOOM") Routes:RegisterEvent("CVAR_UPDATE") timerFrame:Show() Routes.Dragons.RegisterCallback(Routes, "PlayerZoneChanged", function() Routes:DrawMinimapLines(true) end) minimap_rotate = GetCVar("rotateMinimap") == "1" Routes:MINIMAP_UPDATE_ZOOM() -- This has a DrawMinimapLines(true) call in it, and sets an "indoors" variable else Routes:UnregisterEvent("MINIMAP_UPDATE_ZOOM") Routes:UnregisterEvent("CVAR_UPDATE") timerFrame:Hide() Routes.Dragons.UnregisterCallback(Routes, "PlayerZoneChanged") G:HideLines(Minimap) end end, }, battlemap_toggle = { name = L["Zone Map"], desc = L["Zone Map drawing"], type = "toggle", order = 300, arg = "draw_battlemap", }, indoors_toggle = { name = L["Minimap when indoors"], desc = L["Draw on minimap when indoors"], type = "toggle", order = 400, arg = "draw_indoors", disabled = function() return not db.defaults.draw_minimap end, }, }, }, default_group = { name = L["Set the width of lines on each of the maps."], type = "group", desc = L["Normal lines"], inline = true, order = 200, args = { width = { name = L["Worldmap"], type = "range", desc = L["Width of the line in the Worldmap"], min = 10, max = 100, step = 1, arg = "width", order = 100, }, width_minimap = { name = L["Minimap"], type = "range", desc = L["Width of the line in the Minimap"], min = 10, max = 100, step = 1, arg = "width_minimap", order = 110, }, width_battlemap = { name = L["Zone Map"], type = "range", desc = L["Width of the line in the Zone Map"], min = 10, max = 100, step = 1, arg = "width_battlemap", order = 120, }, }, }, color_group = { name = L["Color of lines"], type = "group", desc = L["Color of lines"], inline = true, order = 300, get = function(info) return unpack(db.defaults[info.arg]) end, set = function(info, r, g, b, a) local c = db.defaults[info.arg] c[1] = r; c[2] = g; c[3] = b; c[4] = a Routes:DrawWorldmapLines() Routes:DrawMinimapLines(true) end, args = { color = { name = L["Default route"], type = "color", desc = L["Change default route color"], arg = "color", hasAlpha = true, order = 200, }, hidden_color = { name = L["Hidden route"], type = "color", desc = L["Change default hidden route color"], arg = "hidden_color", hasAlpha = true, order = 400, }, }, }, line_gaps_group = { name = L["Line gaps"], type = "group", desc = L["Line gaps"], inline = true, order = 400, args = { line_gaps = { name = L["Draw line gaps"], type = "toggle", desc = L["Shorten the lines drawn on the minimap slightly so that they do not overlap the icons and minimap tracking blips."], arg = "line_gaps", order = 400, }, line_gaps_skip_cluster = { name = L["Skip clustered node points"], type = "toggle", desc = L["Do not draw gaps for clustered node points in routes."], arg = "line_gaps_skip_cluster", disabled = function() return not db.defaults.line_gaps end, order = 400, }, }, }, show_hidden = { name = L["Show hidden routes"], type = "toggle", desc = L["Show hidden routes?"], arg = "show_hidden", order = 450, }, update_distance = { name = L["Update distance"], type = "range", desc = L["Yards to move before triggering a minimap update"], min = 0, max = 10, step = 0.1, arg = "update_distance", order = 500, }, }, }, } -- Set of functions we use to edit route configs local ConfigHandler = {} function ConfigHandler:GetColor(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] return unpack(db.routes[zone][route].color or db.defaults.color) end function ConfigHandler:SetColor(info, r, g, b, a) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] local t = db.routes[zone][route] t.color = t.color or {} t = t.color t[1] = r; t[2] = g; t[3] = b; t[4] = a; Routes:DrawWorldmapLines() Routes:DrawMinimapLines(true) end function ConfigHandler:GetHidden(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] return db.routes[zone][route].hidden end function ConfigHandler:SetHidden(info, v) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] db.routes[zone][route].hidden = v Routes:DrawWorldmapLines() Routes:DrawMinimapLines(true) end function ConfigHandler:GetWidth(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] return db.routes[zone][route].width or db.defaults.width end function ConfigHandler:SetWidth(info, v) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] db.routes[zone][route].width = v Routes:DrawWorldmapLines() end function ConfigHandler:GetWidthMinimap(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] return db.routes[zone][route].width_minimap or db.defaults.width_minimap end function ConfigHandler:SetWidthMinimap(info, v) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] db.routes[zone][route].width_minimap = v Routes:DrawMinimapLines(true) end function ConfigHandler:GetWidthBattleMap(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] return db.routes[zone][route].width_battlemap or db.defaults.width_battlemap end function ConfigHandler:SetWidthBattleMap(info, v) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] db.routes[zone][route].width_battlemap = v Routes:DrawWorldmapLines() end function ConfigHandler:DeleteRoute(info) local zone = tonumber(info[2]) local zoneKey = info[2] local routekey = info[3] local route = Routes.routekeys[zone][routekey] local is_running, route_table = Routes.TSP:IsTSPRunning() if is_running and route_table == db.routes[zone][route].route then Routes:Print(L["You may not delete a route that is being optimized in the background."]) return end db.routes[zone][route] = nil --local routekey = route:gsub("%s", "\255") -- can't have spaces in the key options.args.routes_group.args[zoneKey].args[routekey] = nil -- delete route from aceopt Routes.routekeys[zone][routekey] = nil if next(db.routes[zone]) == nil then db.routes[zone] = nil options.args.routes_group.args[zoneKey] = nil -- delete zone from aceopt if no routes remaining Routes.routekeys[zone] = nil end Routes:DrawWorldmapLines() Routes:DrawMinimapLines(true) end function ConfigHandler:RecreateRoute(info) local zone = tonumber(info[2]) local routekey = info[3] local route = Routes.routekeys[zone][routekey] local is_running, route_table = Routes.TSP:IsTSPRunning() if is_running and route_table == db.routes[zone][route].route then Routes:Print(L["You may not delete a route that is being optimized in the background."]) return end Routes:RecreateRoute(zone, route) Routes:DrawWorldmapLines() Routes:DrawMinimapLines(true) end function ConfigHandler:ClusterRoute(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] local t = db.routes[zone][route] t.route, t.metadata, t.length = Routes.TSP:ClusterRoute(db.routes[zone][route].route, zone, db.defaults.cluster_dist) t.cluster_dist = db.defaults.cluster_dist Routes:DrawWorldmapLines() Routes:DrawMinimapLines(true) end function ConfigHandler:UnClusterRoute(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] local t = db.routes[zone][route] local num = 0 for i = 1, #t.metadata do for j = 1, #t.metadata[i] do num = num+1 t.route[num] = t.metadata[i][j] end end t.metadata = nil t.cluster_dist = nil t.length = Routes.TSP:PathLength(t.route, zone) Routes:DrawWorldmapLines() Routes:DrawMinimapLines(true) end function ConfigHandler:IsCluster(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] local t = db.routes[zone][route] if t.metadata then return true else return false end end function ConfigHandler:IsNotCluster(info) return not self:IsCluster(info) end function ConfigHandler:GetDefaultClusterDist() return db.defaults.cluster_dist end function ConfigHandler:SetDefaultClusterDist(info, v) db.defaults.cluster_dist = v end function ConfigHandler:ResetLineSettings(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] local t = db.routes[zone][route] t.color = nil t.width = nil t.width_minimap = nil t.width_battlemap = nil Routes:DrawWorldmapLines() Routes:DrawMinimapLines(true) end function ConfigHandler.GetRouteDesc(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] local t = db.routes[zone][route] return L["This route has |cffffd200%d|r nodes and is |cffffd200%d|r yards long."]:format(#t.route, t.length) end function ConfigHandler.GetShortClusterDesc(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] local t = db.routes[zone][route] if not t.metadata then return L["This route is not a clustered route."] end local numNodes = 0 for i = 1, #t.metadata do numNodes = numNodes + #t.metadata[i] end return L["This route is a clustered route, down from the original |cffffd200%d|r nodes."]:format(numNodes) end function ConfigHandler.GetRouteClusterRadiusDesc(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] local t = db.routes[zone][route] if t.metadata then return L["The cluster radius of this route is |cffffd200%d|r yards."]:format(t.cluster_dist or 65) -- 65 was an old default end end do local str = {} function ConfigHandler.GetDataDesc(info) wipe(str) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] local t = db.routes[zone][route] local num = 1 str[num] = L["This route has nodes that belong to the following categories:"] for k in pairs(t.db_type) do num = num + 1 str[num] = "|cffffd200 "..L[k].."|r" end num = num + 1 str[num] = L["This route contains the following nodes:"] for k, v in pairs(t.selection) do num = num + 1 if v == true then v = k end str[num] = "|cffffd200 "..v.."|r" end return table.concat(str, "\n") end local data = {} function ConfigHandler.GetClusterDesc(info) wipe(str) wipe(data) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] local t = db.routes[zone][route] if not t.metadata then return L["This route is not a clustered route."] end local numNodes = 0 local maxt = 0 local zoneW, zoneH = Routes.Dragons:GetZoneSize(zone) for i = 1, #t.metadata do local numData = #t.metadata[i] numNodes = numNodes + numData local x, y = floor(t.route[i] / 10000) / 10000, (t.route[i] % 10000) / 10000 for j = 1, numData do local x2, y2 = floor(t.metadata[i][j] / 10000) / 10000, (t.metadata[i][j] % 10000) / 10000 -- to round off the coordinate local t = (((x2 - x)*zoneW)^2 + ((y2 - y)*zoneH)^2)^0.5 - 0.0001 t = floor(t / 10) data[t] = (data[t] or 0) + 1 if t > maxt then maxt = t end end end for i = 0, maxt do str[i+4] = L["|cffffd200 %d|r node(s) are between |cffffd200%d|r-|cffffd200%d|r yards of a cluster point"]:format(data[i] or 0, i*10+1, i*10+10) end str[1] = L["This route is a clustered route, down from the original |cffffd200%d|r nodes."]:format(numNodes) str[2] = L["The cluster radius of this route is |cffffd200%d|r yards."]:format(t.cluster_dist or 65) -- 65 was an old default str[3] = L["|cffffd200 %d|r node(s) are at |cffffd2000|r yards of a cluster point"]:format(data[-1] or 0) return table.concat(str, "\n") end function ConfigHandler.GetTabooDesc(info) wipe(str) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] local t = db.routes[zone][route] local num = 1 str[num] = L["This route has the following taboo regions:"] for k, v in pairs(t.taboos) do if v then num = num + 1 str[num] = "|cffffd200 "..k.."|r" else t.taboos[k] = nil -- set the false value to nil, so we don't pairs() over it in the future end end if num == 1 then str[num] = L["This route has no taboo regions."] end num = num + 1 str[num] = L["This route contains |cffffd200%d|r nodes that have been tabooed."]:format(#t.taboolist) return table.concat(str, "\n") end end function ConfigHandler:GetTwoPointFiveOpt() return db.defaults.tsp.two_point_five_opt end function ConfigHandler:SetTwoPointFiveOpt(info, v) db.defaults.tsp.two_point_five_opt = v end function ConfigHandler:DoForeground(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] local t = db.routes[zone][route] if #t.route > 724 then -- Lua has 4mb limit on table size. 725x725 will result in a table of size 525625 -- 524288 (or 2^19) is the max as 8 bytes per entry will give exactly 4 Mb Routes:Print(L["TOO_MANY_NODES_ERROR"]) return end local taboos = {} for tabooname, used in pairs(t.taboos) do if used then tinsert(taboos, db.taboo[zone][tabooname]) end end local output, meta, length, iter, timetaken = Routes.TSP:SolveTSP(t.route, t.metadata, taboos, zone, db.defaults.tsp) t.route = output t.length = length t.metadata = meta Routes:Print(L["Path with %d nodes found with length %.2f yards after %d iterations in %.2f seconds."]:format(#output, length, iter, timetaken)) -- redraw lines local AutoShow = Routes:GetModule("AutoShow", true) if AutoShow and db.defaults.use_auto_showhide then AutoShow:ApplyVisibility() end Routes:DrawWorldmapLines() Routes:DrawMinimapLines(true) end function ConfigHandler:DoBackground(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] local t = db.routes[zone][route] if #t.route > 724 then Routes:Print(L["TOO_MANY_NODES_ERROR"]) return end local taboos = {} for tabooname, used in pairs(t.taboos) do if used then tinsert(taboos, db.taboo[zone][tabooname]) end end local running, errormsg = Routes.TSP:SolveTSPBackground(t.route, t.metadata, taboos, zone, db.defaults.tsp) if (running == 1) then Routes:Print(L["Now running TSP in the background..."]) local dispLength; Routes.TSP:SetStatusFunction(function(pass, progress, length) local frame = LibStub("AceConfigDialog-3.0").OpenFrames["Routes"] if frame then if length then dispLength = length end if dispLength then frame:SetStatusText(L["Pass %d: %d%% - %d yards"]:format(pass, progress*100, dispLength)) else frame:SetStatusText(L["Pass %d: %d%%"]:format(pass, progress*100)) end end end) Routes.TSP:SetFinishFunction(function(output, meta, length, iter, timetaken) t.route = output t.length = length t.metadata = meta local msg = L["Path with %d nodes found with length %.2f yards after %d iterations in %.2f seconds."]:format(#output, length, iter, timetaken) Routes:Print(msg) local frame = LibStub("AceConfigDialog-3.0").OpenFrames["Routes"] if frame then frame:SetStatusText(msg) end -- redraw lines local AutoShow = Routes:GetModule("AutoShow", true) if AutoShow and db.defaults.use_auto_showhide then AutoShow:ApplyVisibility() end Routes:DrawWorldmapLines() Routes:DrawMinimapLines(true) end) elseif (running == 2) then Routes:Print(L["There is already a TSP running in background. Wait for it to complete first."]) elseif (running == 3) then -- This should never happen, but is here as a fallback Routes:Print(L["The following error occured in the background path generation coroutine, please report to Grum or Xinhuan:"]); Routes:Print(errormsg); end end do local t = {} function ConfigHandler:GetTabooRegions(info) local zone = tonumber(info[2]) for k, v in pairs(t) do t[k] = nil end for k, v in pairs(db.taboo[zone]) do t[k] = k end return t end end function ConfigHandler:GetTabooRegionStatus(info, k) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] return db.routes[zone][route].taboos[k] end function ConfigHandler:SetTabooRegionStatus(info, k, v) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] if v == false then v = nil end local route_data = db.routes[zone][route] local taboo_data = db.taboo[zone][k] if route_data.taboos[k] ~= v then -- toggle it route_data.taboos[k] = v if v then Routes:ApplyTabooToRoute(zone, taboo_data, route_data) else Routes:UnTabooRoute(zone, route_data) end end end function ConfigHandler:IsBeingManualEdited(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] return db.routes[zone][route].editing end function ConfigHandler.GetRouteName(info) local zone = tonumber(info[2]) return Routes.routekeys[zone][ info[3] ] end function ConfigHandler:IsDisableRecreateRoute(info) -- One of these 2 plugins must be active to recreate routes local disableRecreate = true if Routes.plugins["GatherMate2"] and Routes.plugins["GatherMate2"].IsActive() then disableRecreate = false end if Routes.plugins["Gatherer"] and Routes.plugins["Gatherer"].IsActive() then disableRecreate = false end return disableRecreate or self:IsBeingManualEdited(info) end do local routeTable = { type = "group", name = ConfigHandler.GetRouteName, desc = ConfigHandler.GetRouteName, childGroups = "tab", handler = ConfigHandler, args = { info_group = { type = "group", name = L["Information"], order = 0, args = { desc1 = { type = "description", name = ConfigHandler.GetRouteDesc, order = 0, }, desc2 = { type = "description", name = ConfigHandler.GetDataDesc, order = 10, }, desc3 = { type = "description", name = ConfigHandler.GetClusterDesc, order = 20, }, desc4 = { type = "description", name = ConfigHandler.GetTabooDesc, order = 30, }, delete = { name = L["Delete"], type = "execute", desc = L["Permanently delete a route"], func = "DeleteRoute", confirm = true, confirmText = L["Are you sure you want to delete this route?"], order = 100, disabled = "IsBeingManualEdited", }, recreate = { name = L["Recreate Route"], type = "execute", desc = L["Recreate this route with the same creation settings. NOTE: This only works for data from GatherMate2 and Gatherer."], func = "RecreateRoute", confirm = true, confirmText = L["Are you sure you want to recreate this route? This will delete all customized settings for this route."], order = 110, disabled = "IsDisableRecreateRoute", }, }, }, setting_group = { type = "group", name = L["Line Settings"], order = 100, disabled = "IsBeingManualEdited", args = { desc = { type = "description", name = L["These settings control the visibility and look of the drawn route."], order = 0, }, color = { name = L["Line Color"], type = "color", desc = L["Change the line color"], get = "GetColor", set = "SetColor", order = 100, hasAlpha = true, }, hidden = { name = L["Hide Route"], type = "toggle", desc = L["Hide the route from being shown on the maps"], get = "GetHidden", set = "SetHidden", order = 200, }, width = { name = L["Width (Map)"], type = "range", desc = L["Width of the line in the map"], min = 10, max = 100, step = 1, get = "GetWidth", set = "SetWidth", order = 300, }, width_minimap = { name = L["Width (Minimap)"], type = "range", desc = L["Width of the line in the Minimap"], min = 10, max = 100, step = 1, get = "GetWidthMinimap", set = "SetWidthMinimap", order = 310, }, width_battlemap = { name = L["Width (Zone Map)"], type = "range", desc = L["Width of the line in the Zone Map"], min = 10, max = 100, step = 1, get = "GetWidthBattleMap", set = "SetWidthBattleMap", order = 320, }, blankline = { name = "", type = "description", order = 325, }, reset_all = { name = L["Reset"], type = "execute", desc = L["Reset the line settings to defaults"], func = "ResetLineSettings", order = 500, }, }, }, optimize_group = { type = "group", order = 200, name = L["Optimize Route"], disabled = "IsBeingManualEdited", args = { desc = { type = "description", name = ConfigHandler.GetRouteDesc, order = 0, }, desc2 = { type = "description", name = ConfigHandler.GetShortClusterDesc, order = 1, }, desc3 = { type = "description", name = ConfigHandler.GetRouteClusterRadiusDesc, hidden = "IsNotCluster", disabled = "IsNotCluster", order = 2, }, cluster_header = { type = "header", name = L["Route Clustering"], order = 40, }, desc_cluster = { type = "description", name = L["CLUSTER_DESC"], order = 50, }, cluster_dist = { name = L["Cluster Radius"], type = "range", desc = L["CLUSTER_RADIUS_DESC"], min = 10, max = 200, step = 1, get = "GetDefaultClusterDist", set = "SetDefaultClusterDist", hidden = "IsCluster", disabled = "IsCluster", order = 60, }, cluster = { name = L["Cluster"], type = "execute", desc = L["Cluster this route"], func = "ClusterRoute", hidden = "IsCluster", disabled = "IsCluster", order = 70, }, uncluster = { name = L["Uncluster"], type = "execute", desc = L["Uncluster this route"], func = "UnClusterRoute", hidden = "IsNotCluster", disabled = "IsNotCluster", order = 80, }, optimize_header = { type = "header", name = L["Route Optimizing"], order = 100, }, two_point_five_group = { type = "group", order = 150, name = L["Extra optimization"], inline = true, args = { two_point_five_opt_disc = { name = L["ExtraOptDesc"], type = "description", order = 0, }, two_point_five_opt = { name = L["Extra optimization"], type = "toggle", desc = L["ExtraOptDesc"], get = "GetTwoPointFiveOpt", set = "SetTwoPointFiveOpt", disabled = false, -- to avoid inheriting from parent, so we don't have to use an arg= field order = 100, }, }, }, foreground_group = { type = "group", order = 200, name = L["Foreground"], inline = true, args = { foreground_disc = { type = "description", name = L["Foreground Disclaimer"], order = 0, }, foreground = { name = L["Foreground"], type = "execute", desc = L["Foreground Disclaimer"], func = "DoForeground", order = 100, }, }, }, background_group = { type = "group", order = 300, name = L["Background"], inline = true, args = { background_disc = { type = "description", name = L["Background Disclaimer"], order = 0, }, background = { name = L["Background"], type = "execute", desc = L["Background Disclaimer"], func = "DoBackground", order = 100, }, }, }, }, }, taboo_group = { type = "group", order = 300, name = L["Taboos"], disabled = "IsBeingManualEdited", args = { desc = { type = "description", name = L["TABOO_DESC2"], order = 0, }, taboos = { name = L["Select taboo regions to apply:"], type = "multiselect", order = 100, values = "GetTabooRegions", get = "GetTabooRegionStatus", set = "SetTabooRegionStatus", }, }, }, --edit_group = Routes:GetAceOptRouteEditTable(), }, } function Routes:GetAceOptRouteTable() routeTable.args.edit_group = Routes:GetAceOptRouteEditTable() return routeTable end end local source_data = {} options.args.routes_group.args.desc = { type = "description", name = L["When the following data sources add or delete node data, update my routes automatically by inserting or removing the same node in the relevant routes."]..L[" Gatherer/HandyNotes currently does not support callbacks, so this is impossible for Gatherer/HandyNotes."], order = 0, } options.args.routes_group.args.callbacks = { type = "multiselect", name = L["Select sources of data"], order = 100, values = source_data, get = function(info, k) if Routes.plugins[k].IsActive() and k ~= "Gatherer" and k ~= "HandyNotes" then return db.defaults.callbacks[k] else return nil end end, set = function(info, k, v) -- If plugin is not active, don't toggle anything if not Routes.plugins[k].IsActive() or k == "Gatherer" or k == "HandyNotes" then return end if v == nil then v = false end db.defaults.callbacks[k] = v if v then Routes.plugins[k].AddCallbacks() else Routes.plugins[k].RemoveCallbacks() end end, tristate = true, } -- AceOpt config table for route creation do -- Some upvalues used in the aceopts[] table for creating new routes local create_name = "" local create_zones = {} local create_zone local last_zone = {} local create_choices = {} local create_data = {} local empty_table = {} local source_data_choice = {} local function deep_copy_table(a, b) for k, v in pairs(b) do if type(v) == "table" then --a[k] = {} -- no need this, AceDB defaults should handle it deep_copy_table(a[k], v) else a[k] = v end end end local function get_source_values(info) if not create_zone then return empty_table end local create_data = create_data[info.arg] if last_zone[info.arg] == create_zone then return create_data end -- reuse table wipe(create_data) -- extract data from plugin if Routes.plugins[info.arg].IsActive() then Routes.plugins[info.arg].Summarize(create_data, create_zone) end -- found no data - insert dummy message if not next(create_data) then create_data[ db.defaults.fake_data ..";;;" ] = L["No data found"] end last_zone[info.arg] = create_zone -- Remove invalid entries due to updated data so we don't pairs over it during route creation if create_choices[create_zone] then for k in pairs(create_choices[create_zone]) do if not create_data[k] then create_choices[create_zone][k] = nil end end end return create_data end local function get_source_value(info, key) --Routes:Print(("Getting choice for: %s"):format(key or "nil")); if not create_zone then return end if key == db.defaults.fake_data then return end if not create_choices[create_zone] then create_choices[create_zone] = {} end return create_choices[create_zone][key] end local function set_source_value(info, key, value) if not create_zone then return end if key == db.defaults.fake_data then return end if not create_choices[create_zone] then create_choices[create_zone] = {} end create_choices[create_zone][key] = value --Routes:Print(("Setting choice: %s to %s"):format(key or "nil", value and "true" or "false")); end function Routes:SetupSourcesOptTables() -- reuse table wipe(source_data) -- create a checkbox for each plugin, then setup the aceopt table local order = 300 for addon, plugin_table in pairs(Routes.plugins) do local addonkey = addon:gsub("%s", "_") source_data[addonkey] = addon if not options.args.add_group.args[addonkey] then order = order + 1 create_data[addonkey] = {} options.args.add_group.args[addonkey] = { name = addon..L[" Data"], type = "multiselect", order = order, arg = addon, values = get_source_values, get = get_source_value, set = set_source_value, width = "full", disabled = not plugin_table.IsActive(), guiHidden = not plugin_table.IsActive(), } end end end options.args.add_group.args = { route_name = { type = "input", name = L["Name of Route"], desc = L["Name of the route to add"], validate = function(info, name) if name == "" or strtrim(name) == "" then return L["No name given for new route"] end return true end, get = function() return create_name end, set = function(info, v) create_name = strtrim(v) end, order = 100, }, zone_choice = { name = L["Select Zone"], type = "select", desc = L["Zone to create route in"], order = 150, values = function() if not next(create_zones) then for zoneName in pairs(Routes.LZName) do create_zones[zoneName] = zoneName end end return create_zones end, get = function() if create_zone then return create_zone end -- Use currently viewed map on first view. local mapID = WorldMapFrame:GetMapID() if not mapID then return nil end create_zone = GetZoneName(mapID) return create_zone end, set = function(info, key) create_zone = key end, }, header_bare = { type = "header", name = L["Create Bare Route"], order = 200, }, info_bare = { type = "description", name = L["CREATE_BARE_ROUTE_DESC"], order = 201, }, add_route_bare = { name = L["Create Bare Route"], type = "execute", desc = L["CREATE_BARE_ROUTE_DESC"], order = 202, func = function() create_name = strtrim(create_name) if not create_name or create_name == "" then Routes:Print(L["No name given for new route"]) return end local new_route = { route = {71117111, 12357823, 11171123}, selection = {}, db_type = {} } -- Perform a deep copy instead so that db defaults apply local mapID = Routes.LZName[create_zone] local mapIDKey = tostring(mapID) db.routes[mapID][create_name] = nil -- overwrite old route new_route.route = Routes.TSP:DecrossRoute(new_route.route) deep_copy_table(db.routes[mapID][create_name], new_route) db.routes[mapID][create_name].length = Routes.TSP:PathLength(new_route.route, mapID) -- Create the aceopts table entry for our new route local opts = options.args.routes_group.args if not opts[mapIDKey] then opts[mapIDKey] = { -- use a 3 digit string which is alphabetically sorted zone names by continent type = "group", name = create_zone, desc = L["Routes in %s"]:format(create_zone), args = { desc = route_zone_args_desc_table, }, } Routes.routekeys[mapID] = {} end local routekey = create_name:gsub("%s", "\255") -- can't have spaces in the key Routes.routekeys[mapID][routekey] = create_name opts[mapIDKey].args[routekey] = Routes:GetAceOptRouteTable() -- Draw it local AutoShow = Routes:GetModule("AutoShow", true) if AutoShow and db.defaults.use_auto_showhide then AutoShow:ApplyVisibility() end Routes:DrawWorldmapLines() Routes:DrawMinimapLines(true) -- clear stored name create_name = "" create_zone = nil end, disabled = function() return not create_name or strtrim(create_name) == "" end, confirm = function() if #db.routes[ Routes.LZName[create_zone] ][create_name].route > 0 then return true end return false end, confirmText = L["A route with that name already exists. Overwrite?"], }, header_normal = { type = "header", name = L["Create Route from Data Sources"], order = 225, }, source_choices = { name = L["Select sources of data"], type = "multiselect", order = 250, values = source_data, get = function(info, k) if Routes.plugins[k].IsActive() then if source_data_choice[k] == nil then source_data_choice[k] = true end return source_data_choice[k] else return nil end end, set = function(info, k, v) -- If plugin is not active, don't toggle anything if not Routes.plugins[k].IsActive() then return end if v == nil then v = false end source_data_choice[k] = v options.args.add_group.args[k].disabled = not v options.args.add_group.args[k].guiHidden = not v end, tristate = true, }, add_route = { name = L["Create Route"], type = "execute", desc = L["Create Route"], order = 400, func = function() create_name = strtrim(create_name) if not create_name or create_name == "" then Routes:Print(L["No name given for new route"]) return end -- the real 'action', we use a temporary table in case of data corruption and only commit this to the db if successful local new_route = { route = {}, selection = {}, db_type = {} } -- if for every selected nodetype on this map if type(create_choices[create_zone]) == "table" then for data_string, wanted in pairs(create_choices[create_zone]) do local db_src, db_type, node_type, amount = (';'):split(data_string); local addonkey = db_src:gsub("%s", "_") -- if we want em if (wanted and source_data_choice[addonkey]) then --Routes:Print(("found %s %s %s %s"):format( db_src,db_type,node_type,amount )) if db_src ~= db.defaults.fake_data then -- ignore any fake data -- extract data from plugin local plugin = Routes.plugins[db_src] if plugin.IsActive() then local english_node, localized_node, type = plugin.AppendNodes(new_route.route, create_zone, db_type, node_type) new_route.selection[english_node] = localized_node new_route.db_type[type] = true end end end end end if #new_route.route == 0 then Routes:Print(L["No data selected for new route"]) return end -- Perform a deep copy instead so that db defaults apply local mapID = Routes.LZName[create_zone] local mapIDKey = tostring(mapID) db.routes[mapID][create_name] = nil -- overwrite old route new_route.route = Routes.TSP:DecrossRoute(new_route.route) deep_copy_table(db.routes[mapID][create_name], new_route) db.routes[mapID][create_name].length = Routes.TSP:PathLength(new_route.route, mapID) -- Create the aceopts table entry for our new route local opts = options.args.routes_group.args if not opts[mapIDKey] then opts[mapIDKey] = { -- use a 3 digit string which is alphabetically sorted zone names by continent type = "group", name = create_zone, desc = L["Routes in %s"]:format(create_zone), args = { desc = route_zone_args_desc_table, }, } Routes.routekeys[mapID] = {} end local routekey = create_name:gsub("%s", "\255") -- can't have spaces in the key Routes.routekeys[mapID][routekey] = create_name opts[mapIDKey].args[routekey] = Routes:GetAceOptRouteTable() -- Draw it local AutoShow = Routes:GetModule("AutoShow", true) if AutoShow and db.defaults.use_auto_showhide then AutoShow:ApplyVisibility() end Routes:DrawWorldmapLines() Routes:DrawMinimapLines(true) -- clear stored name create_name = "" create_zone = nil end, disabled = function() return not create_name or strtrim(create_name) == "" end, confirm = function() if #db.routes[ Routes.LZName[create_zone] ][create_name].route > 0 then return true end return false end, confirmText = L["A route with that name already exists. Overwrite?"], }, } -- Add another 'Create button' options.args.add_group.args.add_route_copy = {} for k,v in pairs(options.args.add_group.args.add_route) do options.args.add_group.args.add_route_copy[k] = v end options.args.add_group.args.add_route_copy.order = options.args.add_group.args.source_choices.order + 1 function Routes:RecreateRoute(mapFile, routeName) create_name = routeName create_zone = GetZoneName(mapFile) if type(create_choices[create_zone]) == "table" then wipe(create_choices[create_zone]) else create_choices[create_zone] = {} end for k, v in pairs(source_data_choice) do source_data_choice[k] = false end -- Load data for GatherMate2 if Routes.plugins["GatherMate2"] and Routes.plugins["GatherMate2"].IsActive() then for englishName, translatedName in pairs(db.routes[mapFile][create_name].selection) do local NL = LibStub("AceLocale-3.0"):GetLocale("GatherMate2Nodes",true) translatedName = NL[englishName] for db_type, db_data in pairs(GatherMate2.gmdbs) do local nodeID = GatherMate2:GetIDForNode(db_type, translatedName) if nodeID then local key = ("%s;%s;%s;%s"):format("GatherMate2", db_type, nodeID, 0) set_source_value(nil, key, true) source_data_choice["GatherMate2"] = true end end end end -- Load data for Gatherer if Routes.plugins["Gatherer"] and Routes.plugins["Gatherer"].IsActive() then for englishName, translatedName in pairs(db.routes[mapFile][create_name].selection) do local nodeID = Gatherer.Nodes.Names[translatedName] local db_type = Gatherer.Nodes.Objects[nodeID] if nodeID and db_type then local key = ("%s;%s;%s;%s"):format("Gatherer", db_type, nodeID, 0) set_source_value(nil, key, true) source_data_choice["Gatherer"] = true end end end -- Do it options.args.add_group.args.add_route.func() end end ------------------------------------------------------------------------------------------------------ -- Taboo code do local intersection = {} local pool = setmetatable({}, {__mode="kv"}) local function SortIntersection(a, b) return a.x < b.x end -- This function takes a taboo (a route basically), and draws it on screen and shades the inside function RoutesTabooPinMixin:DrawTaboo(route_data, width, color) local fh, fw = self:GetHeight(), self:GetWidth() local canvasScale = self:GetEffectiveScale() / self:GetMap():GetEffectiveScale() width = width or db.defaults.width / canvasScale color = color or db.defaults.color -- This part just draws the taboo outline, its the same code as the one that draws routes do local last_point local sx, sy last_point = route_data.route[ #route_data.route ] sx, sy = floor(last_point / 10000) / 10000, (last_point % 10000) / 10000 sy = (1 - sy) for i = 1, #route_data.route do local point = route_data.route[i] local ex, ey = floor(point / 10000) / 10000, (point % 10000) / 10000 ey = (1 - ey) G:DrawLine(self, sx*fw, sy*fh, ex*fw, ey*fh, width, color , "OVERLAY") sx, sy = ex, ey last_point = point end end if route_data.isroute then return end -- The shade-lines get half-alpha and 66% width color[4] = color[4] / 2 width = 2/3 * width for z = 0, 1 do -- loop twice, once for upper half, once for lower half for k = 0, 1, 0.01 do for i = 1, #intersection do pool[tremove(intersection)] = true end local last_point local sx, sy last_point = route_data.route[ #route_data.route ] sx, sy = floor(last_point / 10000) / 10000, (last_point % 10000) / 10000 for i = 1, #route_data.route do local point = route_data.route[i] local ex, ey = floor(point / 10000) / 10000, (point % 10000) / 10000 if sx + sy == z + k then -- check for endpoint 1 local vector = next(pool) or {} pool[vector] = nil vector.x, vector.y = sx, sy tinsert(intersection, vector) elseif ex + ey == z + k then -- check for endpoint 2 --[[local vector = next(pool) or {} pool[vector] = nil vector.x, vector.y = ex, ey tinsert(intersection, vector)]] elseif ex+ey-sx-sy ~= 0 then -- 0 indicates a parallel line local u, t if z == 0 and k ~= 0 then u = (k - sx - sy)/(ex+ey-sx-sy) t = (sx + (ex-sx)*u)/k elseif z == 1 and k ~= 1 then u = (1 - sx - sy + k)/(ex+ey-sx-sy) t = (sx + (ex-sx)*u - k)/(1-k) else u, t = -1, -1 -- invalid end if t >= 0 and t <= 1 and u >= 0 and u <= 1 then local vector = next(pool) or {} pool[vector] = nil vector.x, vector.y = sx + (ex-sx)*u, sy + (ey-sy)*u tinsert(intersection, vector) end end sx, sy = ex, ey last_point = point end table.sort(intersection, SortIntersection) --[[for j = #intersection, 2, -1 do -- this loop removes identical intersection points if intersection[j].x == intersection[j-1].x then pool[tremove(intersection, j)] = true end end]] for j = 1, #intersection - (#intersection % 2), 2 do -- this loop draws the pairs of intersections G:DrawLine(self, intersection[j].x*fw, (1-intersection[j].y)*fh, intersection[j+1].x*fw, (1-intersection[j+1].y)*fh, width, color , "OVERLAY") end end end -- restore default alpha color[4] = color[4] * 2 end local taboo_edit_list = {} function Routes:DrawTaboos() local TabooPin = self.DataProvider.tabooPin G:HideLines(TabooPin) for taboo_orig, taboo_copy in pairs(taboo_edit_list) do TabooPin:DrawTaboo(taboo_copy) end end -- Upvalues used for our taboo node functions local taboo_cache = {} local TEXTURE, DATA, COORD, CURRENT, REAL, X, Y = 1, 2, 3, 4, 5, 6, 7 local GetOrCreateTabooNode -- Define our functions for a node pin local NodeHelper = {} function NodeHelper:StartMoving() self:StartMoving() self:SetScript("OnUpdate", NodeHelper.OnUpdate) self.elapsed = 0 end function NodeHelper:OnDragStop() self:StopMovingOrSizing() self:SetScript("OnUpdate", nil) self.elapsed = nil self:SetParent(Routes.DataProvider.tabooPin) self:ClearAllPoints() self:SetPoint("CENTER", Routes.DataProvider.tabooPin, "TOPLEFT", self[X]*Routes.DataProvider.tabooPin:GetWidth(), -self[Y]*Routes.DataProvider.tabooPin:GetHeight()) end function NodeHelper:OnUpdate(elapsed) self.elapsed = self.elapsed + elapsed if self.elapsed < 0.05 then return end -- get current location local id = self[COORD] local x, y = self:GetCenter() local parent = self:GetParent() local pw, ph = parent:GetWidth(), parent:GetHeight() x = (x - parent:GetLeft()) / pw y = (parent:GetTop() - y) / ph -- Only within our frame if x < 0.0001 then x = 0.0001 end -- don't allow 0 values, because our intersection function doesn't like it. if x > 0.999 then x = 0.999 end -- don't use 0.9999 because we want some slack space of 10 nodes in case user drags multiple nodes on top of each other if y < 0.0001 then y = 0.0001 end if y > 0.999 then y = 0.999 end -- we can't have y == 1 because of coord storage format local new_id = Routes:getID(x,y) if id == new_id then return end -- position didn't change, no updates x, y = Routes:getXY(new_id) -- edit the route local current = self[CURRENT] self[DATA].route[current] = new_id self[COORD], self[X], self[Y] = new_id, x, y -- Relocate the before helper pin local nodenum = current == 1 and #self[DATA].route or current-1 local node = self[DATA].fakenodes[nodenum] local x2, y2 = Routes:getXY( self[DATA].route[nodenum] ) local new_id = Routes:getID( (x+x2)/2, (y+y2)/2 ) x2, y2 = Routes:getXY(new_id) node[COORD], node[X], node[Y] = new_id, x2, y2 node:SetPoint("CENTER", Routes.DataProvider.tabooPin, "TOPLEFT", x2*pw, -y2*ph) -- Relocate the after helper pin nodenum = current == #self[DATA].route and 1 or current+1 node = self[DATA].fakenodes[current] x2, y2 = Routes:getXY( self[DATA].route[nodenum] ) new_id = Routes:getID( (x+x2)/2, (y+y2)/2 ) x2, y2 = Routes:getXY(new_id) node[COORD], node[X], node[Y] = new_id, x2, y2 node:SetPoint("CENTER", Routes.DataProvider.tabooPin, "TOPLEFT", x2*pw, -y2*ph) -- redraw Routes:DrawTaboos() end function NodeHelper:OnClick(button, down) if button == "LeftButton" and not self[REAL] then -- Promote helper node to a real node self[REAL] = true self:SetWidth(16 * self.scale) self:SetHeight(16 * self.scale) self:SetAlpha(1) local current = self[CURRENT]+1 for i = current, #self[DATA].route do self[DATA].nodes[i][CURRENT] = i+1 self[DATA].fakenodes[i][CURRENT] = i+1 end self[CURRENT] = current tinsert(self[DATA].route, current, self[COORD]) tinsert(self[DATA].nodes, current, self) tremove(self[DATA].fakenodes, current-1) local x, y = Routes:getXY(self[COORD]) local w, h = Routes.DataProvider.tabooPin:GetWidth(), Routes.DataProvider.tabooPin:GetHeight() -- Now create the before helper pin local nodenum = current == 1 and #self[DATA].route or current-1 local x2, y2 = Routes:getXY( self[DATA].route[nodenum] ) local new_id = Routes:getID( (x+x2)/2, (y+y2)/2 ) local node = GetOrCreateTabooNode(self[DATA], new_id) x2, y2 = Routes:getXY(new_id) node:SetPoint("CENTER", Routes.DataProvider.tabooPin, "TOPLEFT", x2*w, -y2*h) node:SetWidth(10 * node.scale) node:SetHeight(10 * node.scale) node:SetAlpha(0.75) node[REAL] = false node[CURRENT] = nodenum tinsert(self[DATA].fakenodes, nodenum, node) -- Create the after helper pin nodenum = current == #self[DATA].route and 1 or current+1 x2, y2 = Routes:getXY( self[DATA].route[nodenum] ) new_id = Routes:getID( (x+x2)/2, (y+y2)/2 ) node = GetOrCreateTabooNode(self[DATA], new_id) x2, y2 = Routes:getXY(new_id) node:SetPoint("CENTER", Routes.DataProvider.tabooPin, "TOPLEFT", x2*w, -y2*h) node:SetWidth(10 * node.scale) node:SetHeight(10 * node.scale) node:SetAlpha(0.75) node[REAL] = false node[CURRENT] = current tinsert(self[DATA].fakenodes, current, node) elseif button == "RightButton" and self[REAL] and #self[DATA].route > 3 then -- Delete node if we have more than 3 nodes local current = self[CURRENT] for i = current+1, #self[DATA].route do self[DATA].nodes[i][CURRENT] = i-1 self[DATA].fakenodes[i][CURRENT] = i-1 end tremove(self[DATA].route, current) local a = tremove(self[DATA].nodes, current) local b = tremove(self[DATA].fakenodes, current) -- Relocate the before helper pin local w, h = Routes.DataProvider.tabooPin:GetWidth(), Routes.DataProvider.tabooPin:GetHeight() local nodenum = current == 1 and #self[DATA].route or current-1 local nodenum2 = current > #self[DATA].route and 1 or current local node = self[DATA].fakenodes[nodenum] local x, y = Routes:getXY( self[DATA].route[nodenum] ) local x2, y2 = Routes:getXY( self[DATA].route[nodenum2] ) local new_id = Routes:getID( (x+x2)/2, (y+y2)/2 ) x2, y2 = Routes:getXY(new_id) node[COORD], node[X], node[Y] = new_id, x2, y2 node:SetPoint("CENTER", Routes.DataProvider.tabooPin, "TOPLEFT", x2*w, -y2*h) -- Recycle ourselves a:Hide() b:Hide() taboo_cache[a] = true taboo_cache[b] = true Routes:DrawTaboos() end -- Check data for i = 1, #self[DATA].route do assert(self[DATA].nodes[i][CURRENT] == i) assert(self[DATA].fakenodes[i][CURRENT] == i) end end GetOrCreateTabooNode = function( route_data, coord ) local node = next( taboo_cache ) if node then taboo_cache[ node ] = nil else -- Create new node node = CreateFrame( "Button", nil, Routes.DataProvider.tabooPin ) node:SetFrameLevel( Routes.DataProvider.tabooPin:GetFrameLevel() + 6 ) -- we need to be above others (GatherMate nodes are @ 5) -- set it up local texture = node:CreateTexture( nil, "OVERLAY" ) texture:SetTexture("Interface\\WorldMap\\WorldMapPartyIcon") texture:SetAllPoints(node) node[TEXTURE] = texture node:EnableMouse(true) node:SetMovable(true) end node.scale = 1 / (Routes.DataProvider.tabooPin:GetEffectiveScale() / Routes.DataProvider.tabooPin:GetMap():GetEffectiveScale()) node:SetWidth(16 * node.scale) node:SetHeight(16 * node.scale) -- store data node[X], node[Y] = Routes:getXY( coord ) node[COORD] = coord node[DATA] = route_data node:RegisterForDrag("LeftButton") node:RegisterForClicks("LeftButtonDown", "RightButtonUp") node:SetScript("OnDragStart", NodeHelper.StartMoving) node:SetScript("OnClick", NodeHelper.OnClick) node:SetScript("OnDragStop", NodeHelper.OnDragStop) node:Show() return node end local function TabooDeleteNode(menubutton, node) local route = node[DATA].route for i = 1, #route do if route[i] == node[COORD] then tremove(route, i) break end end node:Hide() taboo_cache[node] = true end local TabooHandler = {} function TabooHandler:EditTaboo(info) local zone = tonumber(info[2]) -- make a copy of the taboo for editing local taboo_data if info[1] == "routes_group" then local routeName = Routes.routekeys[zone][ info[3] ] taboo_data = db.routes[zone][routeName] else local tabooName = Routes.tabookeys[zone][ info[3] ] taboo_data = db.taboo[zone][tabooName] end local copy_of_taboo_data = {route = {}, nodes = {}, fakenodes = {}} if info[1] == "routes_group" then local is_running, route_table = Routes.TSP:IsTSPRunning() if is_running and route_table == taboo_data.route then return end if ConfigHandler:IsCluster(info) then return end copy_of_taboo_data.isroute = true taboo_data.editing = true throttleFrame:Show() -- To remove the route from the map end for i = 1, #taboo_data.route do copy_of_taboo_data.route[i] = taboo_data.route[i] end taboo_edit_list[taboo_data] = copy_of_taboo_data -- open the WorldMapFlame on the right zone if WOW_PROJECT_ID == WOW_PROJECT_MAINLINE then OpenWorldMap(zone) else ShowUIPanel(WorldMapFrame) WorldMapFrame:SetMapID(zone) end local fh, fw = Routes.DataProvider.tabooPin:GetHeight(), Routes.DataProvider.tabooPin:GetWidth() local route = copy_of_taboo_data.route -- Pin the real nodes for i=1, #route do local node = GetOrCreateTabooNode(copy_of_taboo_data, route[i]) local x, y = node[X], node[Y] node:SetPoint("CENTER", Routes.DataProvider.tabooPin, "TOPLEFT", x*fw, -y*fh) node[CURRENT] = i node[REAL] = true copy_of_taboo_data.nodes[i] = node node:SetWidth(16 * node.scale) node:SetHeight(16 * node.scale) node:SetAlpha(1) end -- Pin the helper nodes for i=1, #route do local beforeX, beforeY = Routes:getXY(route[i]) local afterX, afterY = Routes:getXY(route[i == #route and 1 or i+1]) local new_id = Routes:getID( (beforeX+afterX)/2, (beforeY+afterY)/2 ) local node = GetOrCreateTabooNode(copy_of_taboo_data, new_id) local x, y = Routes:getXY(new_id) node:SetPoint("CENTER", Routes.DataProvider.tabooPin, "TOPLEFT", x*fw, -y*fh) node[CURRENT] = i node[REAL] = false copy_of_taboo_data.fakenodes[i] = node node:SetWidth(10 * node.scale) node:SetHeight(10 * node.scale) node:SetAlpha(0.75) end -- and draw taboos Routes:DrawTaboos() end function TabooHandler:SaveEditTaboo(info) local zone = tonumber(info[2]) if info[1] == "routes_group" then local route = Routes.routekeys[zone][ info[3] ] local route_data = db.routes[zone][route] local copy_of_taboo = self:CancelEditTaboo(info) for i = 1, #copy_of_taboo.route do route_data.route[i] = copy_of_taboo.route[i] end for i = #copy_of_taboo.route + 1, #route_data.route do route_data.route[i] = nil end for tabooname, used in pairs(route_data.taboos) do if used then local taboo_data = db.taboo[zone][tabooname] Routes:ApplyTabooToRoute(zone, taboo_data, route_data) end end else local taboo = Routes.tabookeys[zone][ info[3] ] local taboo_data = db.taboo[zone][taboo] local copy_of_taboo = self:CancelEditTaboo(info) taboo_data.route = copy_of_taboo.route -- Update all routes with this taboo for route_name, route_data in pairs(db.routes[zone]) do if route_data.taboos[taboo] then Routes:ApplyTabooToRoute(zone, taboo_data, route_data) Routes:UnTabooRoute(zone, route_data) else -- Set the false value (acedb generated default) to nil, so we don't pairs() over it route_data.taboos[taboo] = nil end end end throttleFrame:Show() -- Redraw the changes end function TabooHandler:CancelEditTaboo(info) local zone = tonumber(info[2]) local taboo if info[1] == "routes_group" then local routeName = Routes.routekeys[zone][ info[3] ] taboo = db.routes[zone][routeName] else local tabooName = Routes.tabookeys[zone][ info[3] ] taboo = db.taboo[zone][tabooName] end if info[1] == "routes_group" then taboo.editing = nil throttleFrame:Show() -- Redraw the route end local copy_of_taboo = taboo_edit_list[taboo] taboo_edit_list[taboo] = nil for i = 1, #copy_of_taboo.route do -- Return the pool of pins representing real nodes local node = copy_of_taboo.nodes[i] node:Hide() taboo_cache[node] = true copy_of_taboo.nodes[i] = nil -- Return the pool of pins representing helper nodes node = copy_of_taboo.fakenodes[i] node:Hide() taboo_cache[node] = true copy_of_taboo.fakenodes[i] = nil end assert(not next(copy_of_taboo.fakenodes)) assert(not next(copy_of_taboo.nodes)) Routes:DrawTaboos() return copy_of_taboo -- return the edited table end function TabooHandler:DeleteTaboo(info) if self:IsBeingEdited(info) then Routes:Print(L["You may not delete a taboo that is being edited."]) return end local zone = tonumber(info[2]) local zoneKey = info[2] local tabookey = info[3] local taboo = Routes.tabookeys[zone][tabookey] db.taboo[zone][taboo] = nil --local tabookey = taboo:gsub("%s", "\255") -- can't have spaces in the key options.args.taboo_group.args[zoneKey].args[tabookey] = nil -- delete taboo from aceopt Routes.tabookeys[zone][tabookey] = nil if next(db.taboo[zone]) == nil then db.taboo[zone] = nil options.args.taboo_group.args[zoneKey] = nil -- delete zone from aceopt if no routes remaining Routes.tabookeys[zone] = nil end -- Now delete the taboo region from all routes in the zone that had it for route_name, route_data in pairs(db.routes[zone]) do if route_data.taboos[taboo] then route_data.taboos[taboo] = nil Routes:UnTabooRoute(zone, route_data) else -- Set the false value (acedb generated default) to nil, so we don't pairs() over it route_data.taboos[taboo] = nil end end end function TabooHandler:IsBeingEdited(info) local zone = tonumber(info[2]) local taboo if info[1] == "routes_group" then local routeName = Routes.routekeys[zone][ info[3] ] taboo = db.routes[zone][routeName] else local tabooName = Routes.tabookeys[zone][ info[3] ] taboo = db.taboo[zone][tabooName] end if taboo_edit_list[taboo] then return true end return false end function TabooHandler:IsNotBeingEdited(info) return not self:IsBeingEdited(info) end function TabooHandler.GetTabooName(info) local zone = tonumber(info[2]) return Routes.tabookeys[zone][ info[3] ] end do local tabooTable = { type = "group", name = TabooHandler.GetTabooName, desc = TabooHandler.GetTabooName, handler = TabooHandler, args = { desc = { type = "description", order = 0, name = L["TABOO_EDIT_DESC"], }, edit_taboo = { type = "execute", name = L["Edit taboo region"], desc = L["Edit this taboo region on the world map"], order = 1, func = "EditTaboo", disabled = "IsBeingEdited", }, save_edit_taboo = { type = "execute", name = L["Save taboo edit"], desc = L["Stop editing this taboo region on the world map and save the edits"], order = 2, func = "SaveEditTaboo", disabled = "IsNotBeingEdited", }, cancel_edit_taboo = { type = "execute", name = L["Cancel taboo edit"], desc = L["Stop editing this taboo region on the world map and abandon changes made"], order = 3, func = "CancelEditTaboo", disabled = "IsNotBeingEdited", }, delete_taboo = { type = "execute", name = L["Delete Taboo"], desc = L["Delete this taboo region permanently. This will also remove it from all routes that use it."], order = 4, func = "DeleteTaboo", disabled = "IsBeingEdited", confirm = true, confirmText = L["Are you sure you want to delete this taboo? This action will also remove the taboo from all routes that use it."], }, }, } function Routes:GetAceOptTabooTable() return tabooTable end end local taboo_name = "" local create_zone local create_zones = {} options.args.taboo_group.args = { desc = { name = L["TABOO_DESC"], type = "description", order = 0, }, taboo_name = { type = "input", name = L["Name of Taboo"], desc = L["Name of taboo region to add"], validate = function(info, name) if name == "" or strtrim(name) == "" then return L["No name given for new taboo region"] end return true end, get = function() return taboo_name end, set = function(info, v) taboo_name = strtrim(v) end, order = 100, }, zone_choice = { name = L["Select Zone"], type = "select", desc = L["Zone to create taboo in"], order = 200, values = function() if not next(create_zones) then for zoneName in pairs(Routes.LZName) do create_zones[zoneName] = zoneName end end return create_zones end, get = function() if create_zone then return create_zone end -- Use currently viewed map on first view. local mapID = WorldMapFrame:GetMapID() if not mapID then return nil end create_zone = GetZoneName(mapID) return create_zone end, set = function(info, key) create_zone = key end, }, add_taboo = { name = L["Create Taboo"], type = "execute", desc = L["Create Taboo"], order = 300, func = function() taboo_name = strtrim(taboo_name) if not taboo_name or taboo_name == "" then Routes:Print(L["No name given for new taboo region"]) return end local mapID = Routes.LZName[create_zone] local mapIDKey = tostring(mapID) db.taboo[mapID][taboo_name].route[1] = 81895171 db.taboo[mapID][taboo_name].route[2] = 51077512 db.taboo[mapID][taboo_name].route[3] = 40941800 -- Create the aceopts table entry for our new route local opts = options.args.taboo_group.args if not opts[mapIDKey] then opts[mapIDKey] = { type = "group", name = create_zone, desc = L["Taboos in %s"]:format(create_zone), args = { desc = taboo_zone_args_desc_table, }, } Routes.tabookeys[mapID] = {} end local tabookey = taboo_name:gsub("%s", "\255") -- can't have spaces in the key Routes.tabookeys[mapID][tabookey] = taboo_name opts[mapIDKey].args[tabookey] = Routes:GetAceOptTabooTable() -- clear stored name taboo_name = "" create_zone = nil end, disabled = function() return not taboo_name or strtrim(taboo_name) == "" end, confirm = function() if #db.taboo[ Routes.LZName[create_zone] ][taboo_name].route > 0 then return true end return false end, confirmText = L["A taboo with that name already exists. Overwrite?"], }, } function TabooHandler:IsNotEditAllowed(info) local zone = tonumber(info[2]) local route = Routes.routekeys[zone][ info[3] ] local route_table = db.routes[zone][route] if taboo_edit_list[route_table] then return true end local is_running, route_table2 = Routes.TSP:IsTSPRunning() if is_running and route_table2 == route_table.route then return true end if ConfigHandler:IsCluster(info) then return true end return false end do local routeEditTable = { type = "group", order = 400, name = L["Edit Route Manually"], handler = TabooHandler, args = { desc = { type = "description", order = 0, name = L["ROUTE_EDIT_DESC"], }, edit_route = { type = "execute", name = L["Edit route"], desc = L["Edit this route on the world map"], order = 1, func = "EditTaboo", disabled = "IsNotEditAllowed", }, save_edit_route = { type = "execute", name = L["Save route edit"], desc = L["Stop editing this route on the world map and save the edits"], order = 2, func = "SaveEditTaboo", disabled = "IsNotBeingEdited", }, cancel_edit_route = { type = "execute", name = L["Cancel route edit"], desc = L["Stop editing this route on the world map and abandon changes made"], order = 3, func = "CancelEditTaboo", disabled = "IsNotBeingEdited", }, }, } function Routes:GetAceOptRouteEditTable() return routeEditTable end end --/run Routes:TestFunc() --/run Routes:ClearTestFunc() --[[function Routes:TestFunc(taboo) taboo = taboo or Routes.db.global.taboo[ Routes.LZName["Shattrath City"][1] ]["abc"] local fw, fh = RoutesTabooFrame:GetWidth(), RoutesTabooFrame:GetHeight() for i = 0, 1, 0.02 do for j = 0, 0.99, 0.02 do local point = self:getID(i, j) local node = GetOrCreateTabooNode(taboo, point) local x, y = node[X], node[Y] node:SetPoint("CENTER", RoutesTabooFrame, "TOPLEFT", x*fw, -y*fh) if self:IsNodeInTaboo(x, y, taboo) then node[TEXTURE]:SetVertexColor( 1, 0, 0, 1 ) else node[TEXTURE]:SetVertexColor( 0, 1, 0, 1 ) end node[BEFORE] = point node[AFTER] = point node:SetAlpha(0.5) end end end function Routes:ClearTestFunc() for i = 0, 1, 0.02 do for j = 0, 0.99, 0.02 do local point = self:getID(i, j) local node = GetOrCreateTabooNode(taboo, point) node:Hide() taboo_cache[node] = true end end end]] end do -- This function tests if the node at location (x,y) is in a taboo region -- It does this by drawing a line from (0,0) to (x,y) and seeing how many times -- this line intersects the taboo polygon edges. If its even, its outside. If -- its odd its inside. function Routes:IsNodeInTaboo(x, y, taboo) -- our taboo regions have x and y between 0.0001 and 0.9999 if x <= 0 or y <= 0 or x >= 1 or y >= 1 then return false end local count = 0 local last_point = taboo.route[ #taboo.route ] local sx, sy = floor(last_point / 10000) / 10000, (last_point % 10000) / 10000 for i = 1, #taboo.route do local point = taboo.route[i] local ex, ey = floor(point / 10000) / 10000, (point % 10000) / 10000 -- check if (0,0)-(x,y) intersects with (sx,sy)-(ex,ey) if sx >= 0 and sx <= x and sx/sy == x/y then -- check for endpoint 1 count = count + 1 -- (sx,sy) lies on the line elseif ex >= 0 and ex <= x and ex/ey == x/y then -- check for endpoint 2 -- (ex,ey) lies on the line, do nothing else local d = (x*ey - x*sy - y*ex + y*sx) if d ~= 0 then local u = (sx*y - sy*x)/d local t = (sx + (ex-sx)*u)/x if t >= 0 and t <= 1 and u >= 0 and u <= 1 then count = count + 1 end end end sx, sy = ex, ey last_point = point end return count % 2 == 1 end function Routes:ApplyTabooToRoute(zone, taboo_data, route_data) if route_data.metadata then -- this is a clustered route for i = #route_data.route, 1, -1 do for j = #route_data.metadata[i], 1, -1 do local coord = route_data.metadata[i][j] local x, y = Routes:getXY(coord) if Routes:IsNodeInTaboo(x, y, taboo_data) then -- remove node -- recalcuate centroid local cx, cy = Routes:getXY(route_data.route[i]) local num_data = #route_data.metadata[i] if num_data > 1 then -- more than 1 node in this cluster cx, cy = (cx * num_data - x) / (num_data-1), (cy * num_data - y) / (num_data-1) tremove(route_data.metadata[i], j) route_data.route[i] = Routes:getID(cx, cy) else -- only 1 node in this cluster, just remove it tremove(route_data.metadata, i) tremove(route_data.route, i) end tinsert(route_data.taboolist, coord) route_data.length = Routes.TSP:PathLength(route_data.route, zone) throttleFrame:Show() end end end else -- this is not a clustered route for i = #route_data.route, 1, -1 do local coord = route_data.route[i] local x, y = Routes:getXY(coord) if Routes:IsNodeInTaboo(x, y, taboo_data) then -- remove node tremove(route_data.route, i) tinsert(route_data.taboolist, coord) route_data.length = self.TSP:PathLength(route_data.route, zone) throttleFrame:Show() end end end end function Routes:UnTabooRoute(zone, route_data) for i = #route_data.taboolist, 1, -1 do local coord = route_data.taboolist[i] local x, y = Routes:getXY(coord) local flag = false for tabooname, used in pairs(route_data.taboos) do if used and Routes:IsNodeInTaboo(x, y, db.taboo[zone][tabooname]) then flag = true end end if flag == false then route_data.length = Routes.TSP:InsertNode(route_data.route, route_data.metadata, zone, coord, route_data.cluster_dist or 65) -- 65 is the old default tremove(route_data.taboolist, i) throttleFrame:Show() end end end end ------------------------------------------------------------------------------------------------------ -- The following function is used with permission from Daniel Stephens -- with reference to TaxiFrame.lua in Blizzard's UI and Graph-1.0 Ace2 library (by Cryect) which I now -- maintain after porting it to LibGraph-2.0 LibStub library -- Xinhuan local TAXIROUTE_LINEFACTOR = 128/126; -- Multiplying factor for texture coordinates local TAXIROUTE_LINEFACTOR_2 = TAXIROUTE_LINEFACTOR / 2; -- Half of that -- T - Texture -- C - Canvas Frame (for anchoring) -- sx,sy - Coordinate of start of line -- ex,ey - Coordinate of end of line -- w - Width of line -- relPoint - Relative point on canvas to interpret coords (Default BOTTOMLEFT) function G:DrawLine(C, sx, sy, ex, ey, w, color, layer) local relPoint = "BOTTOMLEFT" if not C.Routes_Lines then C.Routes_Lines={} C.Routes_Lines_Used={} end local T = tremove(C.Routes_Lines) or C:CreateTexture(nil, "ARTWORK") T:SetTexture("Interface\\AddOns\\Routes\\line") T:SetTexelSnappingBias(0) T:SetSnapToPixelGrid(false) tinsert(C.Routes_Lines_Used,T) T:SetDrawLayer(layer or "ARTWORK") T:SetVertexColor(color[1],color[2],color[3],color[4]); -- Determine dimensions and center point of line local dx,dy = ex - sx, ey - sy; -- Calculate actual length of line local l = ((dx * dx) + (dy * dy)) ^ 0.5; -- Quick escape if it's zero length if l == 0 then T:ClearAllPoints(); T:SetTexCoord(0,0,0,0,0,0,0,0); T:SetPoint("BOTTOMLEFT", C, relPoint, cx, cy); T:SetPoint("TOPRIGHT", C, relPoint, cx, cy); return T; end local cx,cy = (sx + ex) / 2, (sy + ey) / 2; -- Normalize direction if necessary if (dx < 0) then dx,dy = -dx,-dy; end -- Sin and Cosine of rotation, and combination (for later) local s,c = -dy / l, dx / l; local sc = s * c; -- Calculate bounding box size and texture coordinates local Bwid, Bhgt, BLx, BLy, TLx, TLy, TRx, TRy, BRx, BRy; if (dy >= 0) then Bwid = ((l * c) - (w * s)) * TAXIROUTE_LINEFACTOR_2; Bhgt = ((w * c) - (l * s)) * TAXIROUTE_LINEFACTOR_2; BLx, BLy, BRy = (w / l) * sc, s * s, (l / w) * sc; BRx, TLx, TLy, TRx = 1 - BLy, BLy, 1 - BRy, 1 - BLx; TRy = BRx; else Bwid = ((l * c) + (w * s)) * TAXIROUTE_LINEFACTOR_2; Bhgt = ((w * c) + (l * s)) * TAXIROUTE_LINEFACTOR_2; BLx, BLy, BRx = s * s, -(l / w) * sc, 1 + (w / l) * sc; BRy, TLx, TLy, TRy = BLx, 1 - BRx, 1 - BLx, 1 - BLy; TRx = TLy; end -- Thanks Blizzard for adding (-)10000 as a hard-cap and throwing errors! -- The cap was added in 3.1.0 and I think it was upped in 3.1.1 -- (way less chance to get the error) if TLx > 10000 then TLx = 10000 elseif TLx < -10000 then TLx = -10000 end if TLy > 10000 then TLy = 10000 elseif TLy < -10000 then TLy = -10000 end if BLx > 10000 then BLx = 10000 elseif BLx < -10000 then BLx = -10000 end if BLy > 10000 then BLy = 10000 elseif BLy < -10000 then BLy = -10000 end if TRx > 10000 then TRx = 10000 elseif TRx < -10000 then TRx = -10000 end if TRy > 10000 then TRy = 10000 elseif TRy < -10000 then TRy = -10000 end if BRx > 10000 then BRx = 10000 elseif BRx < -10000 then BRx = -10000 end if BRy > 10000 then BRy = 10000 elseif BRy < -10000 then BRy = -10000 end -- Set texture coordinates and anchors T:ClearAllPoints(); --[[ When stuff did error local status, err = pcall( T.SetTexCoord, T, TLx, TLy, BLx, BLy, TRx, TRy, BRx, BRy ) if not status then error( ("SetTexCoord tossed an error, please report on http://wowace.com/projects/routes >> Error: %s TLx: %s TLy: %s BLx: %s BLy: %s TRx: %s TRy: %s BRx: %s BRy: %s"):format( err or "nil", TLx or "nil", TLy or "nil", BLx or "nil", BLy or "nil", TRx or "nil", TRy or "nil", BRx or "nil", BRy or "nil" )); end --]] T:SetTexCoord( TLx, TLy, BLx, BLy, TRx, TRy, BRx, BRy ) T:SetPoint("BOTTOMLEFT", C, relPoint, cx - Bwid, cy - Bhgt); T:SetPoint("TOPRIGHT", C, relPoint, cx + Bwid, cy + Bhgt); T:Show() return T end function G:HideLines(C) if C.Routes_Lines then for i = #C.Routes_Lines_Used, 1, -1 do C.Routes_Lines_Used[i]:Hide() tinsert(C.Routes_Lines,tremove(C.Routes_Lines_Used)) end end end function Routes:ReparentMinimap(minimap) self.G:HideLines(Minimap) Minimap = minimap throttleFrame:Show() end -- vim: ts=4 noexpandtab