Planets.nu - Meteor's Homeworlds Plugin

Plugin for Planets.nu which helps to find homeworlds and show areas

// ==UserScript==
// @name          Planets.nu - Meteor's Homeworlds Plugin
// @description   Plugin for Planets.nu which helps to find homeworlds and show areas
// @namespace     Planets.nu
// @version       2.4
// @grant         none
// @date          2022-01-02
// @author        meteor
// @include       http://planets.nu/*
// @include       http://*.planets.nu/*
// @include       https://planets.nu/*
// @include       https://*.planets.nu/*
// @exclude       http://help.planets.nu/*
// @exclude       https://help.planets.nu/*
// @exclude       http://profile*.planets.nu/*
// @exclude       https://profile*.planets.nu/*
// @exclude       http://planets.nu/_library/*
// @exclude       http://api.planets.nu/*

// ==/UserScript==

/* -----------------------------------------------------------------------
 Change log:
 2.0:
 - Feature: Support for new/mobile client added
 - Bug-Fix: checks for Meteor's Library improved
 2.1:
 - Bug-Fix: Treatment of intersection points for parallel lines was incorrect
 2.2:
 - Feature: New switch to use only very close planets (81 ly)
 2.3:
 - Feature from 2.2. removed
 - Feature: 6 levels of potential HW prediction and button to switch through them
 2.4:
 - Feature Improved delta comparison (less false positives)
    - incl. Bug-Fix: some ambiguous planets between 80..82 ly and 161..163 ly were counted or ignored twice
    - Details: no negative delta; positive delta = 1 / sqrt(2); positive delta applies for veryclose-, close- and next-planets
 - Bug-Fix: A selected HW had a brown square around the red square when it was a high-level (thick-square) potential HW 
      and both potential and selected were shown. Not nice to look at and maybe confusing => only the red one now
 - Change: tools menu interaction 
    - Exit-Button added (all clients)
    - In the old client (play.planets.nu): 
        - the way the homeworldtools button group interacts with the tool menu is renewed.
        - Blocking the map tools from closing was removed
            - incl. Bug-Fix: after exiting the game while the homeworldtools were open and entering any game (same or other)
                the map tools didn't work any more
        - "side effect": 
            - Now, when you close the maptools while the homeworldtools are open, the homeworldtools will stay open.
            - You can close them with the new exit button, or reopen the maptools and toggle the HW-button there.
            - While not perfect, this is usable and will not be changed any more (if you don't like it, call it a known bug ;))   
 - Feature: Simple support for debris disks (alpha)
    - Details:
        - The central planetoid of a debris disk counts as a planet assuming it is in the position of an original planet.
        - Other planetoids do not count.
        - There is no attempt to incorporate planets drift due to the creation of a debris disks/explosion of the planet.  
     
 ----------------------------------------------------------------------- */

"use strict";

function wrapper(plugin_version)
{
    let fullName = "Meteor's Homeworlds Plugin";

    if (typeof xLibrary == 'undefined')
    {
        window.alert("Cannot start " + fullName + "!\n\nThe plugin requires Meteor's Library.\nIf the library is installed already make sure it runs before the plugin.");
        throw "Cannot start " + fullName + ". Meteor's Library not found. Plugin disabled.";
    }

    let plugin = new XPlugin(fullName, "xHomeworlds", plugin_version, -20160918, 2.1);

    plugin.setLogEnabled(false);

    let containerRight = xUtils.isMobileClient() ? 40 : 240;

    let css = "<style type='text/css'>";

    css += "#HWToolsContainer {position: absolute; right: " + containerRight + "px; z-index: 16; height: 30px;}";
    css += "#HWToolsContainer .mapbutton {margin-left: 10px; margin-bottom: 10px; float: left;}";
    css += ".HWTools::before {position: absolute; left: 7px; top: 7px; width: 16px; height: 16px; content: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAV0lEQVQ4jWNgYGD4TyHGLflWRoU0AwhpwCGPXTEyINkL6JoIGILqNFyKkcXRvILfdiJcQVx0UWQASWFAjGa8YUCCzVQ2gMhki00dDbyAbgsB1xGXDnBhAEpcONJRmdhMAAAAAElFTkSuQmCC');}";
    css += ".HWPotential::before {position: absolute; left: 7px; top: 7px; width: 16px; height: 16px; content: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAQ0lEQVQ4jWNgYGD4TyGmkgH/DzSQhLEaQKytRBnA+P8/CibJAHTN6IbgNQCXZmRDhrsBFAciVaKR7IREUVKmODORiwGQfMmwgj8zPAAAAABJRU5ErkJggg==');}";
    css += ".HWSelected::before {position: absolute; left: 7px; top: 7px; width: 16px; height: 16px; content: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAQ0lEQVQ4jWNgYGD4TyGmkgFvZVRIwlgNINZWogxg/P8fBZNkALpmdEPwGoBLM7Ihw90AigORKtFIdkKiKClTnJnIxQDXSJqYYNY0/QAAAABJRU5ErkJggg==');}";
    css += ".HWPieslices::before {position: absolute; left: 7px; top: 7px; width: 16px; height: 16px; content: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAQCAYAAADwMZRfAAAASklEQVQ4jWNgYGD4Twn+////fwYYgxLAgGQauum4bEXh09YQYg2mjyHI4vjkCRpCjByKIZQA6rqEJmGCLoZLDW0NIeQ92hpCCQAAX3xOztMy/IoAAAAASUVORK5CYII=');}";
    css += ".HWAreas::before {position: absolute; left: 7px; top: 7px; width: 16px; height: 16px; content: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAZElEQVQ4jZWSMRIAIQgD9/+fzhVe4aiEUNAgLBECIEkCrqjyR9TFHeB/9w2BOt8QQO8HB3kA+8kVGNDKmn/a5tcOpvsoAQNv1HIjyLmDUPY+sJfeODIrjK6QeD820uC8c8Be/wE4e5R6/RMhbwAAAABJRU5ErkJggg==');}";
    css += ".HWSelectMode::before {position: absolute; left: 7px; top: 7px; width: 16px; height: 16px; content: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAATUlEQVQ4jbXSMQ4AIAwCQP7/aVyN2gIaB5YON1AAgHNIcr2J7ECInIEAqQET6QED0YBAPKBBfKBAPnUQbKJfYlzixTL12qIvfAEE8gYMPuqsYu1i0jgAAAAASUVORK5CYII=');}";
    css += ".HWSave::before {position: absolute; left: 7px; top: 7px; width: 16px; height: 16px; content: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAS0lEQVQ4jWNgYGD4TyGGMBwc/pOEcRpACOA0ACb5//////X19VgxshrquwDZAEIuoE0YUM0AiqORYgMYGA7gxSQYwDDUDKA4N5KLAVUbBv9JdSpFAAAAAElFTkSuQmCC');}";

    css += "</style>";
    $("head:first").append(css);

    const DELTA = 1 / Math.sqrt(2);

    const MAX_LEVEL = 6;
    const LEVEL_COLORS = ["#806020", "#C09050", "#FFC080", "#FFC080", "#FFC080", "#FFE0A0"];
    
    plugin.toolsContainerTop = 12;
    
    plugin.processload = function()
    {
        plugin.selectedHomeworlds = plugin.getObjectFromNote(0);
        if (plugin.selectedHomeworlds == null)
        {
            plugin.selectedHomeworlds = new Array();
        }

        plugin.isSelectedHomeworldsModified = false;

        plugin.show = plugin.getObjectFromNote(1);
        if (plugin.show == null)
        {
            plugin.show =
                {
                    potentialHomeworlds: false,
                    selectedHomeworlds: false,
                    pieslices: false,
                    areas: false,
                };
        }

        if (plugin.show.level == null)
        {
            plugin.show.level = (vgap.settings.nextplanets > 0) ? 2 : 3;
        }
        
        plugin.isInSelectMode = false;

        if (plugin.gameId != vgap.game.id)
        {
            plugin.gameId = vgap.game.id;
        }

        plugin.potentialHomeworlds = null;
        plugin.homeworldAreas = null;

        plugin.initPotentialHomeworlds();
    };

    plugin.initPotentialHomeworlds = function()
    {
        let planets = [];

        for (let i = 0; i < vgap.planets.length; i++)
        {
            let planet = vgap.planets[i];
            if (planet.id < 0)
            {
                continue;
            }

            if (planet.debrisdisk > 0)
            {
                continue;
            }

            planets.push(planet);
        }
        
        for (let planet of vgap.debrisdisks)
        {
            if (planet.id < 0)
            {
                continue;
            }

            planets.push(planet);
        }
        
        let counts = [];
        
        for (let i = 0; i < planets.length; i++)
        {
            counts.push(
                {
                    veryClose: 0,
                    veryCloseAmbiguous: 0,
                    totalClose: 0,
                    totalCloseAmbiguous: 0,
                    totalNext: 0,
                    totalNextAmbiguous: 0
                });
        }

        for (let i = 0; i < planets.length; i++)
        {
            let p1 = planets[i];

            for (let k = i + 1; k < planets.length; k++)
            {
                let p2 = planets[k];

                let dist = xMapUtils.getSphereDistance(p1, p2);

                if (plugin.isLogEnabled() //
                    && (   ((dist > 80) && (dist < 82)) //
                        || ((dist > 161) && (dist < 163)) //
                        || ((dist > 323) && (dist < 325))))
                {
                    plugin.log(p1.id + " <-> " + p2.id + " = " + dist);
                }

                if (dist < 81 + DELTA)
                {
                    counts[i].veryClose++;
                    counts[i].totalClose++;
                    counts[i].totalNext++;

                    counts[k].veryClose++;
                    counts[k].totalClose++;
                    counts[k].totalNext++;

                    if (dist > vgap.settings.otherplanetsminhomeworlddist)
                    {
                        counts[i].veryCloseAmbiguous++;
                        counts[i].totalCloseAmbiguous++;
                        counts[i].totalNextAmbiguous++;

                        counts[k].veryCloseAmbiguous++;
                        counts[k].totalCloseAmbiguous++;
                        counts[k].totalNextAmbiguous++;
                    }
                    else if (dist > 81)
                    {
                        counts[i].veryCloseAmbiguous++;
                        counts[k].veryCloseAmbiguous++;
                    }
                }
                else if (dist < 162 + DELTA)
                {
                    counts[i].totalClose++;
                    counts[i].totalNext++;

                    counts[k].totalClose++;
                    counts[k].totalNext++;

                    if (dist > vgap.settings.otherplanetsminhomeworlddist)
                    {
                        counts[i].totalCloseAmbiguous++;
                        counts[i].totalNextAmbiguous++;

                        counts[k].totalCloseAmbiguous++;
                        counts[k].totalNextAmbiguous++;
                    }
                    else if (dist > 162)
                    {
                        counts[i].totalCloseAmbiguous++;
                        counts[k].totalCloseAmbiguous++;
                    }
                }
                else if (dist < 324 + DELTA)
                {
                    counts[i].totalNext++;
                    counts[k].totalNext++;

                    if ((dist > 324) || (dist > vgap.settings.otherplanetsminhomeworlddist))
                    {
                        counts[i].totalNextAmbiguous++;
                        counts[k].totalNextAmbiguous++;
                    }
                }
            }
        }

        plugin.potentialHomeworlds = [];
        for (let level = 0; level < MAX_LEVEL; level++)
        {
            plugin.potentialHomeworlds.push([]);
        }    

        if (plugin.isLogEnabled())
        {
            for (let i = 0; i < counts.length; i++)
            {
                if (planets[i].id >= 0)
                {
                    plugin.log(planets[i].id + ": [" + counts[i].veryClose + ", " + counts[i].veryCloseAmbiguous 
                               + ", " + counts[i].totalClose + ", " + counts[i].totalCloseAmbiguous
                               + ", " + counts[i].totalNext + ", " + counts[i].totalNextAmbiguous + "]");
                }
            }
        }

        let veryClose = vgap.settings.verycloseplanets;
        let totalClose = veryClose + vgap.settings.closeplanets;
        let totalNext = totalClose + (vgap.settings.nextplanets > 0 ? vgap.settings.nextplanets : 0);
        
        for (let i = 0; i < counts.length; i++)
        {
            let count = counts[i];

            if ((count.veryClose >= veryClose) && (count.veryClose - count.veryCloseAmbiguous <= veryClose))
            {
                plugin.potentialHomeworlds[0].push(planets[i].id);
                
                if (count.totalClose >= totalClose)
                {
                    plugin.potentialHomeworlds[1].push(planets[i].id);
                    
                    let isCloseExact = false;
                    
                    if (count.totalClose - count.totalCloseAmbiguous <= totalClose)
                    {
                        isCloseExact = true;
                        plugin.potentialHomeworlds[3].push(planets[i].id);
                    }

                    if ((vgap.settings.nextplanets > 0) && (count.totalNext >= totalNext))
                    {
                        plugin.potentialHomeworlds[2].push(planets[i].id);
                        
                        if (isCloseExact)
                        {
                            plugin.potentialHomeworlds[4].push(planets[i].id);
                            
                            if (count.totalNext - count.totalNextAmbiguous <= totalNext)
                            {
                                plugin.potentialHomeworlds[5].push(planets[i].id);
                            }
                        }
                    }
                }
            }
        }

        if (plugin.isLogEnabled())
        {
            for (let level = 0; level < MAX_LEVEL; level++)
            {
                if (!plugin.isLevelAvailable(level))
                {
                    continue;
                } 
                
                let potentialHomeworldIds = "potentialHomeworldIds[" + (level + 1) + "] = [";
                for (let i = 0; i < plugin.potentialHomeworlds[level].length; i++)
                {
                    potentialHomeworldIds += ((i > 0) ? ", " : "") + plugin.potentialHomeworlds[level][i];
                }
    
                plugin.log(potentialHomeworldIds + "]");
            }
        }
    };

    plugin.isLevelAvailable = function(level)
    {
        return (vgap.settings.nextplanets > 0) || (level == 0) || (level == 1) || (level == 3);
    };
    
    plugin.calculateAreas = function()
    {
        if (plugin.selectedHomeworlds.length == 0)
        {
            return;
        }

        plugin.homeworldAreas = [];

        let mapBoundingRect = xMapUtils.getMapBoundingRect();
        let mapWidth = mapBoundingRect.getWidth();
        let mapHeight = mapBoundingRect.getHeight();

        let allLines = [];
        for (let i = 0; i < plugin.selectedHomeworlds.length; i++)
        {
            let p1 = XPoint.fromPoint(vgap.getPlanet(plugin.selectedHomeworlds[i]));
            let lines = [];

            for (let j = 0; j < plugin.selectedHomeworlds.length; j++)
            {
                if (j == i)
                {
                    continue;
                }

                let p2 = XPoint.fromPoint(vgap.getPlanet(plugin.selectedHomeworlds[j]));

                lines.push(XLine.getPerpendicularBisector(p1, p2));

                if (vgap.settings.sphere)
                {
                    lines.push(XLine.getPerpendicularBisector(p1, p2.offsetXY(-mapWidth, -mapHeight)));
                    lines.push(XLine.getPerpendicularBisector(p1, p2.offsetXY(-mapWidth, 0)));
                    lines.push(XLine.getPerpendicularBisector(p1, p2.offsetXY(-mapWidth, +mapHeight)));
                    lines.push(XLine.getPerpendicularBisector(p1, p2.offsetXY(0, -mapHeight)));
                    lines.push(XLine.getPerpendicularBisector(p1, p2.offsetXY(0, +mapHeight)));
                    lines.push(XLine.getPerpendicularBisector(p1, p2.offsetXY(+mapWidth, -mapHeight)));
                    lines.push(XLine.getPerpendicularBisector(p1, p2.offsetXY(+mapWidth, 0)));
                    lines.push(XLine.getPerpendicularBisector(p1, p2.offsetXY(+mapWidth, +mapHeight)));
                }
            }

            if (vgap.settings.sphere)
            {
                lines.push(XLine.getPerpendicularBisector(p1, p1.offsetXY(-mapWidth, -mapHeight)));
                lines.push(XLine.getPerpendicularBisector(p1, p1.offsetXY(-mapWidth, 0)));
                lines.push(XLine.getPerpendicularBisector(p1, p1.offsetXY(-mapWidth, +mapHeight)));
                lines.push(XLine.getPerpendicularBisector(p1, p1.offsetXY(0, -mapHeight)));
                lines.push(XLine.getPerpendicularBisector(p1, p1.offsetXY(0, +mapHeight)));
                lines.push(XLine.getPerpendicularBisector(p1, p1.offsetXY(+mapWidth, -mapHeight)));
                lines.push(XLine.getPerpendicularBisector(p1, p1.offsetXY(+mapWidth, 0)));
                lines.push(XLine.getPerpendicularBisector(p1, p1.offsetXY(+mapWidth, +mapHeight)));
            }
            else
            {
                lines.push(mapBoundingRect.getLeftSection().getLine());
                lines.push(mapBoundingRect.getRightSection().getLine());
                lines.push(mapBoundingRect.getTopSection().getLine());
                lines.push(mapBoundingRect.getBottomSection().getLine());
            }

            allLines.push(lines);
        }

        iterate_homeworlds: //
        for (let i = 0; i < plugin.selectedHomeworlds.length; i++)
        {
            let homeworld = vgap.getPlanet(plugin.selectedHomeworlds[i]);
            let p1 = XPoint.fromPoint(homeworld);
            lines = allLines[i];

            let closestP2 = null;
            let closestDist = mapWidth + mapHeight;

            let startLine;
            let startPoint;

            if (plugin.selectedHomeworlds.length == 1)
            {
                let virtualP2 = new XPoint(p1.x - mapWidth, p1.y);
                startLine = lines[1]; // intentionally not 0, suits to virtualP2 in both cases
                startPoint = startLine.getIntersectionPoint(XLine.fromPoints(p1, virtualP2));
            }
            else
            {
                for (let j = 0; j < plugin.selectedHomeworlds.length; j++)
                {
                    if (j == i)
                    {
                        continue;
                    }

                    let p2 = XPoint.fromPoint(vgap.getPlanet(plugin.selectedHomeworlds[j]));
                    let dist = xMapUtils.getSphereDistance(p1, p2);

                    if (dist < closestDist)
                    {
                        closestDist = dist;
                        closestP2 = p2;
                    }
                }

                if (vgap.settings.sphere)
                {
                    if (closestP2.x - p1.x > mapWidth / 2)
                    {
                        closestP2.x -= mapWidth;
                    }
                    if (closestP2.x - p1.x < -mapWidth / 2)
                    {
                        closestP2.x += mapWidth;
                    }
                    if (closestP2.y - p1.y > mapHeight / 2)
                    {
                        closestP2.y -= mapHeight;
                    }
                    if (closestP2.y - p1.y < -mapHeight / 2)
                    {
                        closestP2.y += mapHeight;
                    }
                }

                startLine = XLine.getPerpendicularBisector(p1, closestP2);
                startPoint = startLine.getIntersectionPoint(XLine.fromPoints(p1, closestP2));
            }

            let heading = xMapUtils.getHeading(p1, startPoint) + 90;
            if (heading >= 360)
            {
                heading -= 360;
            }

            let area = [];
            let maxCycles = lines.length + 2;

            for (var cycle = 0; cycle < maxCycles; cycle++)
            {
                let bestPoint = null;
                let bestLine = null;
                let bestHeading = null;

                for (let k = 0; k < lines.length; k++)
                {
                    let curLine = lines[k];

                    let curPoint = startLine.getIntersectionPoint(curLine);

                    if (curPoint != null)
                    {
                        if (((bestPoint != null) && (curPoint.x == bestPoint.x) && (curPoint.y == bestPoint.y) && this.isBetterHeading(this.getNewHeading(curLine, heading), bestHeading, heading)) || this.isCloser(curPoint, bestPoint, startPoint, heading))
                        {
                            bestPoint = curPoint;
                            bestLine = curLine;
                            bestHeading = this.getNewHeading(bestLine, heading);
                        }
                    }
                }

                if (bestPoint == null)
                {
                    plugin.logWarning("bestPoint not found");

                    continue iterate_homeworlds;
                }

                if ((area.length > 0) && (area[0].x == bestPoint.x) && (area[0].y == bestPoint.y))
                {
                    plugin.homeworldAreas.push(
                        {
                            homeworld: homeworld,
                            area: area
                        });

                    break;
                }

                area.push(bestPoint);

                startPoint = bestPoint;
                startLine = bestLine;
                heading = bestHeading;
            }

            if (cycle >= maxCycles)
            {
                plugin.logWarning("Cannot calculate area!");

                for (i = 0; i < area.length; i++)
                {
                    plugin.logWarning("area[" + i + "] = (" + area[i].x + ", " + area[i].y + ")");
                }
            }
        }
    };

    plugin.isCloser = function(curPoint, bestPoint, startPoint, heading)
    {
        if (curPoint == null)
        {
            return false;
        }

        if ((heading >= 45) && (heading < 135))
        {
            return ((curPoint.x > startPoint.x) && ((bestPoint == null) || (curPoint.x < bestPoint.x)));
        }

        if ((heading >= 135) && (heading < 225))
        {
            return ((curPoint.y < startPoint.y) && ((bestPoint == null) || (curPoint.y > bestPoint.y)));
        }

        if ((heading >= 225) && (heading < 315))
        {
            return ((curPoint.x < startPoint.x) && ((bestPoint == null) || (curPoint.x > bestPoint.x)));
        }

        return ((curPoint.y > startPoint.y) && ((bestPoint == null) || (curPoint.y < bestPoint.y)));
    };

    plugin.getNewHeading = function(line, curHeading)
    {
        let newHeading = line.getHeading();
        while (newHeading < curHeading)
        {
            newHeading += 180;

        }
        if (newHeading >= 360)
        {
            newHeading -= 360;
        }

        return newHeading;
    };

    plugin.isBetterHeading = function(newHeading, bestHeading, curHeading)
    {
        let h2 = newHeading;
        if (h2 - curHeading < 0)
        {
            h2 += 360;
        }

        let h1 = bestHeading;
        if (h1 - curHeading < 0)
        {
            h1 += 360;
        }

        return h2 > h1;
    };

    plugin.loadmap = function()
    {
        xMapUtils.addMapTool("Homeworlds", "HWTools", plugin.toggleHomeworldTools);
    };

    plugin.toggleHomeworldTools = function(e)
    {
        if ($("#HWToolsContainer").length == 0)
        {
            plugin.openHomeworldTools(e);
        }
        else
        {
            plugin.closeHomeworldTools(e);
        }
    };

    plugin.openHomeworldTools = function(e)
    {
        if (e != null)
        {
            e.stopPropagation();
            e.preventDefault();
        }

        $(".HWTools").toggleClass(xUtils.isMobileClient() ?  "toolactive" : "selectedmaptool", true);

        plugin.updateHomeworldTools();
    }

    plugin.closeHomeworldTools = function(e)
    {
        if (e != null)
        {
            e.stopPropagation();
            e.preventDefault();
        }

        if (plugin.isInSelectMode)
        {
            plugin.toggleSelectMode(e);
        }

        plugin.saveObjectAsNote(1, plugin.show);
        
        $("#HWToolsContainer").remove();
        
        $(".HWTools").toggleClass(xUtils.isMobileClient() ?  "toolactive" : "selectedmaptool", false);
        vgap.map.closeTools();
    };

    plugin.updateHomeworldTools = function()
    {
        $("#HWToolsContainer").remove();
        let showPieslicesButton = ((vgap.settings.hwdistribution == 2) // hw in a circle
        || (vgap.settings.hwdistribution == 4)); // center + circle (MvM)

        $("<div id='HWToolsContainer'></div>").appendTo("#MapControls");
        if (xUtils.isMobileClient() || $("#MapTools").is(":visible"))
        {
            plugin.toolsContainerTop = document.getElementsByClassName("HWTools")[0].offsetTop;
        }
        
        $("#HWToolsContainer").css("top", plugin.toolsContainerTop + "px").css("width", "160px");

        xMapUtils.addMapTool("Show Potential Homeworlds", "HWPotential" + (plugin.show.potentialHomeworlds ? " toolactive" : ""), plugin.toggleShowPotentialHomeworlds, "#HWToolsContainer");
        xMapUtils.addMapTool("Show Selected Homeworlds", "HWSelected" + (plugin.show.selectedHomeworlds ? " toolactive" : ""), plugin.toggleShowSelectedHomeworlds, "#HWToolsContainer");
        if (showPieslicesButton)
        {
            xMapUtils.addMapTool("Show Pieslices", "HWPieslices" + (plugin.show.pieslices ? " toolactive" : ""), plugin.toggleShowPieslices, "#HWToolsContainer");
        }
                    
        xMapUtils.addMapTool("Show Areas", "HWAreas" + (plugin.show.areas ? " toolactive" : ""), plugin.toggleShowAreas, "#HWToolsContainer");
        xMapUtils.addMapTool("Select HW", "HWSelectMode" + (plugin.isInSelectMode ? " toolactive" : ""), plugin.toggleSelectMode, "#HWToolsContainer");
        xMapUtils.addMapTool("Save Selected HWs", "HWSave", plugin.saveSelectedHomeworlds, "#HWToolsContainer");
        if (plugin.isSelectedHomeworldsModified)
        {
            $("#HWToolsContainer .HWSave").css("background", "#00FF00");
        }
        xMapUtils.addMapTool("Potential Homeworlds Level", "HWLevel", plugin.toggleLevel, "#HWToolsContainer");
        $("#HWToolsContainer .HWLevel").append("<div style='margin-top: 5px;'>" + (plugin.show.level + 1) + "</div>");
        
        xMapUtils.addMapTool("Exit", "HWExit", plugin.closeHomeworldTools, "#HWToolsContainer");
        $("#HWToolsContainer .HWExit").append("<div style='margin-top: 5px;'>X</div>");

        if (!showPieslicesButton)
        {
            $("#HWToolsContainer .HWExit").detach().insertAfter("#HWToolsContainer .HWAreas");
        }
        
        $("body").css("cursor", (plugin.isInSelectMode ? "crosshair" : ""));
    };

    plugin.showdashboard = function()
    {
        $("#HWToolsContainer").hide();
    };

    plugin.showmap = function()
    {
        $("#HWToolsContainer").show();
    };

    plugin.toggleShowPotentialHomeworlds = function(e)
    {
        if (e != null)
        {
            e.stopPropagation();
            e.preventDefault();
        }

        plugin.show.potentialHomeworlds = !plugin.show.potentialHomeworlds;
        plugin.updateHomeworldTools();
        vgap.map.draw();
    };

    plugin.toggleShowSelectedHomeworlds = function(e)
    {
        if (e != null)
        {
            e.stopPropagation();
            e.preventDefault();
        }
        
        plugin.show.selectedHomeworlds = !plugin.show.selectedHomeworlds;
        plugin.updateHomeworldTools();
        vgap.map.draw();
    };

    plugin.toggleShowPieslices = function(e)
    {
        if (e != null)
        {
            e.stopPropagation();
            e.preventDefault();
        }

        plugin.show.pieslices = !plugin.show.pieslices;
        plugin.updateHomeworldTools();
        vgap.map.draw();
    };

    plugin.toggleShowAreas = function(e)
    {
        if (e != null)
        {
            e.stopPropagation();
            e.preventDefault();
        }

        plugin.show.areas = !plugin.show.areas;
        plugin.updateHomeworldTools();
        vgap.map.draw();
    };

    plugin.toggleSelectMode = function(e)
    {
        if (e != null)
        {
            e.stopPropagation();
            e.preventDefault();
        }

        plugin.isInSelectMode = !plugin.isInSelectMode;
        plugin.updateHomeworldTools();
        vgap.map.draw();
    };

    plugin.saveSelectedHomeworlds = function(e)
    {
        if (e != null)
        {
            e.stopPropagation();
            e.preventDefault();
        }

        if (plugin.isSelectedHomeworldsModified)
        {
            plugin.saveObjectAsNote(0, plugin.selectedHomeworlds);
            plugin.isSelectedHomeworldsModified = false;
            plugin.updateHomeworldTools();
        }
    };

    plugin.toggleLevel = function(e)
    {
        if (e != null)
        {
            e.stopPropagation();
            e.preventDefault();
        }
        
        let level = plugin.show.level;
        do
        {
            level--;
            if (level < 0)
            {
                level = MAX_LEVEL - 1;
            }
        }
        while (!plugin.isLevelAvailable(level));
        
        plugin.show.level = level;
        plugin.updateHomeworldTools();
        vgap.map.draw();
    };

    plugin.draw = function()
    {
        let map = vgap.map;
        let mapBoundingRect = xMapUtils.getMapBoundingRect();
        let homeworldCenter = mapBoundingRect.getCenterPoint();

        // draw rectangles around potential HWs
        if ((plugin.show.potentialHomeworlds || plugin.isInSelectMode))
        {
            for (let level = plugin.show.level; level < MAX_LEVEL; level++)
            {
                if (!plugin.isLevelAvailable(level))
                {
                    continue;
                }
                
                let drawParams = new XDrawParams().setStrokeStyle(LEVEL_COLORS[level]);
                
                for (let i = 0; i < plugin.potentialHomeworlds[level].length; i++)
                {
                    let planet = vgap.getPlanet(plugin.potentialHomeworlds[level][i]);
    
                    let rad = (map.planetRad(planet) + 3) / map.zoom;
                    let rect = new XRect(planet.x - rad, planet.y - rad, planet.x + rad, planet.y + rad);
    
                    xMapUtils.drawRect(rect, drawParams);
                    
                    if (level > 3)
                    {
                        if ((plugin.show.selectedHomeworlds || plugin.isInSelectMode) && plugin.selectedHomeworlds.includes(planet.id))
                        {
                            continue;
                        }

                        let rad = (map.planetRad(planet) + 4) / map.zoom;
                        let rect = new XRect(planet.x - rad, planet.y - rad, planet.x + rad, planet.y + rad);
    
                        xMapUtils.drawRect(rect, drawParams);
                    }
                }
            }
        }

        // draw rectangles around selected HWs
        if (plugin.show.selectedHomeworlds || plugin.isInSelectMode)
        {
            let drawParams = new XDrawParams().setStrokeStyle("#FF0000");

            for (let i = 0; i < plugin.selectedHomeworlds.length; i++)
            {
                let planet = vgap.getPlanet(plugin.selectedHomeworlds[i]);

                let rad = (map.planetRad(planet) + 3) / map.zoom;
                let rect = new XRect(planet.x - rad, planet.y - rad, planet.x + rad, planet.y + rad);

                xMapUtils.drawRect(rect, drawParams);
            }
        }

        // draw circles to assist the user in localizing HWs
        if (plugin.isInSelectMode)
        {
            if (vgap.settings.hwdistribution == 1) // HW random spaced
            {
                let rad = Math.sqrt((mapBoundingRect.getWidth() * mapBoundingRect.getHeight()) / vgap.players.length);

                for (let i = 0; i < vgap.planets.length; i++)
                {
                    let planet = vgap.planets[i];
                    let id = planet.id < 0 ? -planet.id : planet.id;

                    if (plugin.selectedHomeworlds.indexOf(id) >= 0)
                    {
                        map.drawCircle(map.ctx, xMapUtils.screenX(planet.x), xMapUtils.screenY(planet.y), rad * map.zoom, "#FFFFFF", 1);
                    }
                }
            }
            else if ((vgap.settings.hwdistribution == 2) // HW in a circle
                     || (vgap.settings.hwdistribution == 4)) // center + circle (MvM)
            {
                let centerRadius = mapBoundingRect.getWidth() / 6;

                if (vgap.settings.hwdistribution == 4)
                {
                    map.drawCircle(map.ctx, xMapUtils.screenX(homeworldCenter.x), xMapUtils.screenY(homeworldCenter.y), centerRadius * map.zoom, "#0000FF", 1);
                }

                let countCenterHomeworlds = 0;

                let dist = 0;
                for (let i = 0; i < plugin.selectedHomeworlds.length; i++)
                {
                    let planet = vgap.getPlanet(plugin.selectedHomeworlds[i]);
                    let curDist = xMapUtils.getSphereDistance(homeworldCenter, planet);
                    if ((vgap.settings.hwdistribution == 4) && (curDist < centerRadius))
                    {
                        countCenterHomeworlds += 1;
                    }
                    else
                    {
                        dist += curDist;
                    }
                }

                if (plugin.selectedHomeworlds.length - countCenterHomeworlds > 0)
                {
                    dist /= (plugin.selectedHomeworlds.length - countCenterHomeworlds);

                    map.drawCircle(map.ctx, xMapUtils.screenX(homeworldCenter.x), xMapUtils.screenY(homeworldCenter.y), dist * map.zoom, "#FFFFFF", 1);

                    let rad = 2 * Math.sin(Math.PI / (vgap.players.length - ((vgap.settings.hwdistribution == 4) ? 1 : 0))) * dist;

                    for (let i = 0; i < plugin.selectedHomeworlds.length; i++)
                    {
                        let planet = vgap.getPlanet(plugin.selectedHomeworlds[i]);

                        if ((vgap.settings.hwdistribution != 4) || (xMapUtils.getSphereDistance(homeworldCenter, planet) >= centerRadius))
                        {
                            map.drawCircle(map.ctx, xMapUtils.screenX(planet.x), xMapUtils.screenY(planet.y), rad * map.zoom, "#FFFFFF", 1);
                        }
                    }
                }
            }
        }

        // draw pie slices
        if (plugin.show.pieslices)
        {
            let centerRadius = mapBoundingRect.getWidth() / 6;

            if (vgap.settings.hwdistribution == 4)
            {
                map.drawCircle(map.ctx, xMapUtils.screenX(homeworldCenter.x), xMapUtils.screenY(homeworldCenter.y), centerRadius * map.zoom, "#FFFFFF", 1);
            }

            let mapAngles = new Array();
            mapAngles.push(xMapUtils.getHeading(homeworldCenter, mapBoundingRect.getRightTopPoint()));
            mapAngles.push(xMapUtils.getHeading(homeworldCenter, mapBoundingRect.getRightBottomPoint()));
            mapAngles.push(xMapUtils.getHeading(homeworldCenter, mapBoundingRect.getLeftBottomPoint()));
            mapAngles.push(xMapUtils.getHeading(homeworldCenter, mapBoundingRect.getLeftTopPoint()));

            let angles = new Array();
            for (let i = 0; i < plugin.selectedHomeworlds.length; i++)
            {
                let planet = vgap.getPlanet(plugin.selectedHomeworlds[i]);
                let curDist = xMapUtils.getSphereDistance(homeworldCenter, planet);
                if ((vgap.settings.hwdistribution != 4) || (curDist >= centerRadius))
                {
                    angles.push(xMapUtils.getHeading(homeworldCenter, planet));
                }
            }

            angles.sort(function(a, b)
            {
                return a - b;
            });

            let sections = [];

            for (let i = 0; i < angles.length; i++)
            {
                let angle1 = angles[i];
                let angle2 = angles[i == angles.length - 1 ? 0 : i + 1] + (i == angles.length - 1 ? 360 : 0);
                let angle = (angle1 + angle2) / 2;
                let angleRad = angle * Math.PI / 180;

                let x1 = (vgap.settings.hwdistribution == 4) ? homeworldCenter.x + centerRadius * Math.sin(angleRad) : homeworldCenter.x;
                let y1 = (vgap.settings.hwdistribution == 4) ? homeworldCenter.y + centerRadius * Math.cos(angleRad) : homeworldCenter.y;

                let x2;
                let y2;
                if ((angle <= mapAngles[0]) || (angle > mapAngles[3]))
                {
                    y2 = mapBoundingRect.top;
                    x2 = homeworldCenter.x + Math.tan(angleRad) * (y2 - homeworldCenter.y);
                }
                else if ((angle > mapAngles[0]) && (angle <= mapAngles[1]))
                {
                    x2 = mapBoundingRect.right;
                    y2 = homeworldCenter.y + (x2 - homeworldCenter.x) / Math.tan(angleRad);
                }
                else if ((angle > mapAngles[1]) && (angle <= mapAngles[2]))
                {
                    y2 = mapBoundingRect.bottom;
                    x2 = homeworldCenter.x + Math.tan(angleRad) * (y2 - homeworldCenter.y);
                }
                else if ((angle > mapAngles[2]) && (angle <= mapAngles[3]))
                {
                    x2 = mapBoundingRect.left;
                    y2 = homeworldCenter.y + (x2 - homeworldCenter.x) / Math.tan(angleRad);
                }

                sections.push(new XLineSection(x1, y1, x2, y2));
            }

            let drawParams = new XDrawParams().setStrokeStyle("#FFFFFF").setSphereDuplication(xConst.sphereDuplication.NONE);

            for (let i = 0; i < sections.length; i++)
            {
                xMapUtils.drawLineSection(sections[i], drawParams);
            }

            xMapUtils.drawRect(mapBoundingRect, drawParams);
        }

        // draw areas
        if (plugin.show.areas && (plugin.selectedHomeworlds.length > 0))
        {
            if (plugin.homeworldAreas == null)
            {
                plugin.calculateAreas();
            }

            let drawParams = new XDrawParams().setStrokeStyle("#FFFFFF");

            for (let i = 0; i < plugin.homeworldAreas.length; i++)
            {
                let area = plugin.homeworldAreas[i].area;

                let lastPoint = area[area.length - 1];
                for (let j = 0; j < area.length; j++)
                {
                    let curPoint = area[j];

                    xMapUtils.drawLineSection(XLineSection.fromPoints(lastPoint, curPoint), drawParams);

                    lastPoint = curPoint;
                }
            }
        }
    };

    plugin.oldVgapMapClick = vgapMap.prototype.click;
    vgapMap.prototype.click = function(_shift)
    {
        let map = vgap.map;

        if (plugin.isInSelectMode)
        {
            if ((map.over != null) && map.over.isPlanet)
            {
                let planet = map.over;
                let id = planet.id < 0 ? -planet.id : planet.id;
                let found = false;

                for (let i = 0; i < plugin.selectedHomeworlds.length; i++)
                {
                    if (id == plugin.selectedHomeworlds[i])
                    {
                        plugin.selectedHomeworlds.splice(i, 1);
                        found = true;

                        break;
                    }
                }

                if (!found)
                {
                    plugin.selectedHomeworlds.push(id);
                }

                plugin.isSelectedHomeworldsModified = true;
                plugin.homeworldAreas = null;

                plugin.updateHomeworldTools();
                map.draw();
            }
        }
        else
        {
            plugin.oldVgapMapClick.apply(this, arguments);
        }

    };
} // wrapper for injection

let script = document.createElement("script");
script.type = "application/javascript";
script.textContent = "(" + wrapper + ")(\"" + GM_info.script.version + "\");";
document.body.appendChild(script);
document.body.removeChild(script);