Planets.nu - Meteor's Library

Library for Planets.nu with diverse globally available basic functions to serve other plugins.

目前为 2019-11-22 提交的版本。查看 最新版本

// ==UserScript==
// @name          Planets.nu - Meteor's Library
// @description   Library for Planets.nu with diverse globally available basic functions to serve other plugins.
// @namespace     Planets.nu
// @version       1.0
// @grant         none
// @date          2018-06-26
// @author        meteor
// @include       http://planets.nu/home
// @include       http://planets.nu/games/*
// @include       http://planets.nu/*
// @include       http://play.planets.nu/*
// @include       http://test.planets.nu/*
// @include       http://*.planets.nu/*
// @include       https://planets.nu/*
// @include       https://*.planets.nu/*
// ==/UserScript==

/* -----------------------------------------------------------------------
 Change log:
 ----------------------------------------------------------------------- */

"use strict";

function wrapper(plugin_version)
{
    xLibrary = new XPlugin("Meteor's Library", plugin_version, -20180626);
}

function global()
{
    var xLibrary;

    // -----------------------------------------------------------------------------------------------------------------

    function XPlugin(name, version, notetype)
    {
        this.name = name;
        this.version = version;
        this.notetype = notetype;

        if (vgap.version < 3.0)
        {
            console.log(name + "requires at least NU version 3.0. Plugin disabled.");
            return;
        }

        console.log(name + " version: v" + version);
    }

    XPlugin.prototype.processload = function()
    {
    };

    XPlugin.prototype.loadmap = function()
    {
    };

    XPlugin.prototype.draw = function()
    {
    };

    XPlugin.prototype.loaddashboard = function()
    {
    };

    XPlugin.prototype.showdashboard = function()
    {
    };

    XPlugin.prototype.showsummary = function()
    {
    };

    XPlugin.prototype.showmap = function()
    {
    };

    XPlugin.prototype.loadplanet = function()
    {
    };

    XPlugin.prototype.loadstarbase = function()
    {
    };

    XPlugin.prototype.loadship = function()
    {
    };

    XPlugin.prototype.saveObjectAsNote = function(id, obj)
    {
        let note = vgap.getNote(id, this.notetype);
        if (note == null)
        {
            note = vgap.addNote(id, this.notetype);
        }

        note.changed = 1;
        note.body = JSON.stringify(obj);

        vgap.save();
    };

    XPlugin.prototype.getObjectFromNote = function(id)
    {
        let note = vgap.getNote(id, this.notetype);
        if ((note == null) || (note.body == ""))
        {
            return null;
        }

        return JSON.parse(note.body);
    };

    XPlugin.prototype.logWarning = function(warning)
    {
        console.log(this.name + "> Warning: " + warning);
    };

    XPlugin.prototype.setLogEnabled = function(logEnabled)
    {
        this.logEnabled = logEnabled;
    };

    XPlugin.prototype.isLogEnabled = function()
    {
        return this.logEnabled;
    };

    // -----------------------------------------------------------------------------------------------------------------

    var xConst =
        {
            colRaceId:
                {
                    UNKNOWN: 0,
                    FEDERATION: 1,
                    LIZARDS: 2,
                    BIRDS: 3,
                    FASCISTS: 4,
                    PRIVATEERS: 5,
                    CYBORG: 6,
                    CRYSTALLINE: 7,
                    EMPIRE: 8,
                    ROBOTS: 9,
                    REBELS: 10,
                    COLONIES: 11,
                    HORWASP: 12
                },
            natRaceId:
                {
                    NONE: 0,
                    HUMANOID: 1,
                    BOVINOID: 2,
                    REPTILIAN: 3,
                    AVIAN: 4,
                    AMORPHOUS: 5,
                    INSECTOID: 6,
                    AMPHIBIAN: 7,
                    GHIPSOLDAL: 8,
                    SILICONOID: 9,
                    OTHER_PLAYER: 10
                },
            natGovernmentId:
                {
                    UNKNOWN: 0,
                    ANARCHY: 1,
                    PRE_TRIBAL: 2,
                    EARLY_TRIBAL: 3,
                    TRIBAL: 4,
                    FEUDAL: 5,
                    MONARCHY: 6,
                    REPRESENTATIVE: 7,
                    PARTICIPATORY: 8,
                    UNITY: 9,
                },
            sphereDuplication:
                {
                    NONE: 1,
                    FULL: 2,
                    ALPHA: 3
                },
            building:
                {
                    MINE: 200,
                    FACTORY: 100,
                    DEFENSE_POST: 50
                }

        };

    // -----------------------------------------------------------------------------------------------------------------

    var xUtils =
        {
            assume: function(object, properties)
            {
                var result = Object.create(object);

                Object.getOwnPropertyNames(properties).forEach(function(propertyName)
                {
                    result[propertyName] = properties[propertyName];
                });

                return result;
            },

            getLogString: function(object, arExcludedPropertyNames, maxRecursionDepth)
            {
                let getLogStringImpl = function(object, arObjectsSeen, arExcludedPropertyNames, maxRecursionDepth)
                {
                    let str = Object.getPrototypeOf(object).constructor.name + "[";
                    let isFirst = true;

                    for ( let propertyName in object)
                    {
                        let propertyValue = object[propertyName];

                        if (!(propertyValue instanceof Function))
                        {
                            let propertyValueStr = null;

                            if (propertyValue instanceof Object)
                            {
                                let cycle = arObjectsSeen.includes(propertyValue);
                                arObjectsSeen.push(propertyValue);

                                if (cycle)
                                {
                                    propertyValueStr = "(cycle)";
                                }
                                else if ((maxRecursionDepth > 0) && (!arExcludedPropertyNames.includes(propertyName)))
                                {
                                    propertyValueStr = getLogStringImpl(propertyValue, arObjectsSeen, arExcludedPropertyNames, maxRecursionDepth - 1);
                                }
                                else
                                {
                                    propertyValueStr = Object.getPrototypeOf(propertyValue).constructor.name + "[...]";
                                }
                            }
                            else
                            {
                                propertyValueStr = arExcludedPropertyNames.includes(propertyName) ? "..." : propertyValue;
                            }

                            if (isFirst)
                            {
                                isFirst = false;
                            }
                            else
                            {
                                str += ", ";
                            }

                            str += propertyName + " = " + propertyValueStr;
                        }
                    }

                    str += "]";

                    return str;
                };

                let arObjectsSeen = [];
                arObjectsSeen.push(object);

                if (!arExcludedPropertyNames)
                {
                    arExcludedPropertyNames = [];
                }

                if (maxRecursionDepth == undefined || maxRecursionDepth == null || maxRecursionDepth < 0)
                {
                    maxRecursionDepth = Infinity;
                }

                return getLogStringImpl(object, arObjectsSeen, arExcludedPropertyNames, maxRecursionDepth);
            },

            hasNatives: function(planet)
            {
                return planet.nativetype > 0;
            },

            getMaxColPop: function(planet)
            {
                let oldPlanet = vgap.planetScreen.planet;
                vgap.planetScreen.planet = planet;

                let maxCol = vgap.planetScreen.maxPop(false);

                vgap.planetScreen.planet = oldPlanet;

                return maxCol;
            },

            getMaxNatPop: function(planet)
            {
                if (!this.hasNatives(planet))
                {
                    return 0;
                }

                if (planet.nativetype == xConst.natRaceId.SILICONOID)
                {
                    return planet.temp * 1000;
                }

                return Math.round(Math.sin(3.14 * (100 - planet.temp) / 100) * 150000);
            },

            getNativeTaxRatio: function(planet)
            {
                let oldPlanet = vgap.planetScreen.planet;
                vgap.planetScreen.planet = this.assume(planet,
                    {
                        clans: 1,
                        nativetaxrate: 100
                    });

                let nativeTaxRatio = vgap.planetScreen.nativeTaxAmount();

                vgap.planetScreen.planet = oldPlanet;

                return nativeTaxRatio;
            },

            getMinColsForBuildings: function(countBuildings, buildingType)
            {
                if (countBuildings <= buildingType)
                {
                    return countBuildings;
                }

                return buildingType + ((countBuildings - buildingType) * (countBuildings - buildingType));
            },

            getMaxBuildings: function(planet, buildingType)
            {
                if (planet.clans <= buildingType)
                {
                    return planet.clans;
                }

                return Math.floor(buildingType + Math.sqrt(planet.clans - buildingType));
            },

            createColTaxTable: function(planet)
            {
                let oldPlanet = vgap.planetScreen.planet;
                vgap.planetScreen.planet = planet;

                let oldColTaxRate = planet.colonisttaxrate;

                let table = [];

                for (let taxRate = 0; taxRate < 101; taxRate++)
                {
                    planet.colonisttaxrate = taxRate;
                    let taxAmount = vgap.planetScreen.colonistTaxAmount();
                    let possibleTaxAmount = taxAmount;
                    let happinessChange = vgap.colonistTaxChange(planet);
                    let happiness = planet.colonisthappypoints + happinessChange;
                    if (happiness > 100)
                        happiness = 100;

                    let absoluteGrowth = vgap.planetScreen.colPopGrowth() * 100;
                    let relativeGrowth = absoluteGrowth / planet.clans;

                    table[taxRate] =
                        {
                            taxRate: taxRate,
                            taxAmount: taxAmount,
                            possibleTaxAmount: possibleTaxAmount,
                            happiness: happiness,
                            happinessChange: happinessChange,
                            absoluteGrowth: absoluteGrowth,
                            relativeGrowth: relativeGrowth
                        };
                }

                planet.colonisttaxrate = oldColTaxRate;
                vgap.planetScreen.planet = oldPlanet;

                return table;
            },

            createNatTaxTable: function(planet)
            {
                if (!this.hasNatives(planet))
                {
                    return null;
                }

                let oldPlanet = vgap.planetScreen.planet;
                vgap.planetScreen.planet = planet;

                let oldNatTaxRate = planet.nativetaxrate;

                let table = [];

                for (let taxRate = 0; taxRate < 101; taxRate++)
                {
                    planet.nativetaxrate = taxRate;
                    let taxAmount = vgap.planetScreen.nativeTaxAmount();
                    let possibleTaxAmount = vgap.planetScreen.nativeTaxAmount(true);
                    let happinessChange = vgap.nativeTaxChange(planet);
                    let happiness = planet.nativehappypoints + happinessChange;
                    if (happiness > 100)
                        happiness = 100;

                    let absoluteGrowth = vgap.planetScreen.nativePopGrowth() * 100;
                    let relativeGrowth = absoluteGrowth / planet.nativeclans;

                    table[taxRate] =
                        {
                            taxRate: taxRate,
                            taxAmount: taxAmount,
                            possibleTaxAmount: possibleTaxAmount,
                            happiness: happiness,
                            happinessChange: happinessChange,
                            absoluteGrowth: absoluteGrowth,
                            relativeGrowth: relativeGrowth
                        };
                }

                planet.nativetaxrate = oldNatTaxRate;
                vgap.planetScreen.planet = oldPlanet;

                return table;
            },

            getMaxFullCollectableTaxEntry: function(taxTable)
            {
                for (let i = taxTable.length - 1; i >= 0; i--)
                {
                    let entry = taxTable[i];
                    if (entry.taxAmount == entry.possibleTaxAmount)
                    {
                        return entry;
                    }
                }

                return taxTable[0];
            },

            getMaxTaxEntryByHappinessChange: function(taxTable, happinessChange)
            {
                for (let i = taxTable.length - 1; i >= 0; i--)
                {
                    let entry = taxTable[i];
                    if (entry.happinessChange >= happinessChange)
                    {
                        return entry;
                    }
                }

                return taxTable[0];
            }

        };

    // #################################################################################################################

    var xMapUtils =
        {
            getMapCenter: function()
            {
                return new XPoint(2000, 2000);
            },

            getHomeworldCenter: function()
            {
                return this.getMapCenter();
            },

            getSphereBorderAddition: function()
            {
                return new XDimension(10, 10);
            },

            getMapBoundingRect: function()
            {
                let mapWidth = vgap.settings.mapwidth + (vgap.settings.sphere ? 2 * this.getSphereBorderAddition().getWidth() : 0);
                let mapHeight = vgap.settings.mapheight + (vgap.settings.sphere ? 2 * this.getSphereBorderAddition().getHeight() : 0);
                let mapCenter = this.getMapCenter();

                let x1 = mapCenter.x - mapWidth / 2;
                let y1 = mapCenter.y - mapHeight / 2;
                let x2 = mapCenter.x + mapWidth / 2;
                let y2 = mapCenter.y + mapHeight / 2;

                return new XRect(x1, y1, x2, y2);
            },

            getSphereDuplicationBoundingRect: function()
            {
                return this.getMapBoundingRect().enlarge(new XDimension(vgap.accountsettings.sphereduplicate, vgap.accountsettings.sphereduplicate));
            },

            getHeading: function(point1, point2)
            {
                return vgap.getHeading(point1.x, point1.y, point2.x, point2.y);
            },

            getSphereDistance: function(point1, point2)
            {
                return vgap.getSphereDistance(point1.x, point1.y, point2.x, point2.y);
            },

            drawLineSection: function(section, drawParams)
            {
                if (section == null)
                {
                    return;
                }

                let mapBoundingRect = this.getMapBoundingRect();
                let sphereDuplicationBoundingRect = this.getSphereDuplicationBoundingRect();

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

                let sphereClipRects = [];
                let delta = XPoint.minDistance();
                sphereClipRects.push(new XRect(sphereDuplicationBoundingRect.left, sphereDuplicationBoundingRect.bottom, mapBoundingRect.left - delta, sphereDuplicationBoundingRect.top));
                sphereClipRects.push(new XRect(mapBoundingRect.right + delta, sphereDuplicationBoundingRect.bottom, sphereDuplicationBoundingRect.right, sphereDuplicationBoundingRect.top));
                sphereClipRects.push(new XRect(mapBoundingRect.left, mapBoundingRect.top + delta, mapBoundingRect.right, sphereDuplicationBoundingRect.top));
                sphereClipRects.push(new XRect(mapBoundingRect.left, sphereDuplicationBoundingRect.bottom, mapBoundingRect.right, mapBoundingRect.bottom - delta));

                let collectSections = function(section, normalSections, sphereSections)
                {
                    let clippedSection = section.clip(mapBoundingRect);
                    if (clippedSection)
                    {
                        normalSections.push(clippedSection);
                    }

                    if (vgap.settings.sphere)
                    {
                        for (let i = 0; i < sphereClipRects.length; i++)
                        {
                            clippedSection = section.clip(sphereClipRects[i]);
                            if (clippedSection)
                            {
                                sphereSections.push(clippedSection);
                            }
                        }
                    }
                };

                let normalSections = [];
                let sphereSections = [];

                collectSections(section, normalSections, sphereSections);

                if (vgap.settings.sphere)
                {
                    collectSections(section.offsetXY(-mapWidth, -mapHeight), normalSections, sphereSections);
                    collectSections(section.offsetXY(-mapWidth, 0), normalSections, sphereSections);
                    collectSections(section.offsetXY(-mapWidth, +mapHeight), normalSections, sphereSections);
                    collectSections(section.offsetXY(0, -mapHeight), normalSections, sphereSections);
                    collectSections(section.offsetXY(0, +mapHeight), normalSections, sphereSections);
                    collectSections(section.offsetXY(+mapWidth, -mapHeight), normalSections, sphereSections);
                    collectSections(section.offsetXY(+mapWidth, 0), normalSections, sphereSections);
                    collectSections(section.offsetXY(+mapWidth, +mapHeight), normalSections, sphereSections);
                }

                let ctx = vgap.map.ctx;

                ctx.strokeStyle = drawParams.strokeStyle;
                ctx.lineWidth = drawParams.lineWidth;

                for (let i = 0; i < normalSections.length; i++)
                {
                    let curSection = normalSections[i];

                    ctx.beginPath();
                    ctx.moveTo(vgap.map.screenX(curSection.point1.x), vgap.map.screenY(curSection.point1.y));
                    ctx.lineTo(vgap.map.screenX(curSection.point2.x), vgap.map.screenY(curSection.point2.y));
                    ctx.closePath();
                    ctx.stroke();
                }

                if ((drawParams.sphereDuplication == xConst.sphereDuplication.FULL) || (drawParams.sphereDuplication == xConst.sphereDuplication.ALPHA))
                {
                    if (drawParams.sphereDuplication == xConst.sphereDuplication.ALPHA)
                    {
                        ctx.strokeStyle = colorToRGBA(drawParams.strokeStyle, 0.5);
                    }

                    for (i = 0; i < sphereSections.length; i++)
                    {
                        curSection = sphereSections[i];

                        ctx.beginPath();
                        ctx.moveTo(vgap.map.screenX(curSection.point1.x), vgap.map.screenY(curSection.point1.y));
                        ctx.lineTo(vgap.map.screenX(curSection.point2.x), vgap.map.screenY(curSection.point2.y));
                        ctx.closePath();
                        ctx.stroke();
                    }
                }
            },

            drawRect: function(rect, drawParams)
            {
                if (rect == null)
                {
                    return;
                }

                this.drawLineSection(rect.getLeftSection(), drawParams);
                this.drawLineSection(rect.getRightSection(), drawParams);
                this.drawLineSection(rect.getTopSection(), drawParams);
                this.drawLineSection(rect.getBottomSection(), drawParams);
            }
        };

    // -----------------------------------------------------------------------------------------------------------------

    const X_POINTS_PER_LIGHTYEAR = 4096;

    function XPoint(x, y)
    {
        this.x = XPoint.roundCoordinate(x);
        this.y = XPoint.roundCoordinate(y);
    }

    XPoint.fromPoint = function(point)
    {
        return new XPoint(point.x, point.y);
    };

    XPoint.roundCoordinate = function(c)
    {
        return Math.round(c * X_POINTS_PER_LIGHTYEAR) / X_POINTS_PER_LIGHTYEAR;
    };

    XPoint.minDistance = function()
    {
        return 1 / X_POINTS_PER_LIGHTYEAR;
    };

    XPoint.prototype.equals = function(point)
    {
        if (point == null)
        {
            return false;
        }

        if (point instanceof XPoint)
        {
            return ((this.x == point.x) && (this.y == point.y));
        }

        return this.equals(XPoint.fromPoint(point));
    };

    XPoint.prototype.getLogString = function()
    {
        return "XPoint(" + this.x + ", " + this.y + ")";
    };

    XPoint.prototype.offsetXY = function(dx, dy)
    {
        return new XPoint(this.x + dx, this.y + dy);
    };

    // -----------------------------------------------------------------------------------------------------------------

    function XDimension(width, height)
    {
        this.width = XPoint.roundCoordinate(width);
        this.height = XPoint.roundCoordinate(height);
    }

    XDimension.prototype.equals = function(dimension)
    {
        if (dimension == null)
        {
            return false;
        }

        return (this.width == dimension.width) && (this.height == dimension.height);
    };

    XDimension.prototype.getLogString = function()
    {
        return "XDimension(" + this.width + ", " + this.height + ")";
    };

    XDimension.prototype.getWidth = function()
    {
        return this.width;
    };

    XDimension.prototype.getHeight = function()
    {
        return this.height;
    };

    // -----------------------------------------------------------------------------------------------------------------

    function XRect(x1, y1, x2, y2)
    {
        this.left = XPoint.roundCoordinate((x1 < x2) ? x1 : x2);
        this.bottom = XPoint.roundCoordinate((y1 < y2) ? y1 : y2);
        this.right = XPoint.roundCoordinate((x1 < x2) ? x2 : x1);
        this.top = XPoint.roundCoordinate((y1 < y2) ? y2 : y1);
    }

    XRect.fromPoints = function(point1, point2)
    {
        return new XRect(point1.x, point1.y, point2.x, point2.y);
    };

    XRect.prototype.equals = function(rect)
    {
        if (rect == null)
        {
            return false;
        }

        return ((this.left == rect.left) && (this.bottom == rect.bottom) && (this.right == rect.right) && (this.top == rect.top));
    };

    XRect.prototype.getLogString = function()
    {
        return "XRect(" + this.left + ", " + this.bottom + ", " + this.right + ", " + this.top + ")";
    };

    XRect.prototype.getWidth = function()
    {
        return this.right - this.left;
    };

    XRect.prototype.getHeight = function()
    {
        return this.top - this.bottom;
    };

    XRect.prototype.getDimension = function()
    {
        return new XDimension(this.getWidth(), this.getHeight());
    };

    XRect.prototype.getLeftSection = function()
    {
        return new XLineSection(this.left, this.bottom, this.left, this.top);
    };

    XRect.prototype.getBottomSection = function()
    {
        return new XLineSection(this.left, this.bottom, this.right, this.bottom);
    };

    XRect.prototype.getRightSection = function()
    {
        return new XLineSection(this.right, this.bottom, this.right, this.top);
    };

    XRect.prototype.getTopSection = function()
    {
        return new XLineSection(this.left, this.top, this.right, this.top);
    };

    XRect.prototype.getLeftBottomPoint = function()
    {
        return new XPoint(this.left, this.bottom);
    };

    XRect.prototype.getRightBottomPoint = function()
    {
        return new XPoint(this.right, this.bottom);
    };

    XRect.prototype.getLeftTopPoint = function()
    {
        return new XPoint(this.left, this.top);
    };

    XRect.prototype.getRightTopPoint = function()
    {
        return new XPoint(this.right, this.top);
    };

    XRect.prototype.enlarge = function(dimension)
    {
        return new XRect(this.left - dimension.width, this.bottom - dimension.height, this.right + dimension.width, this.top + dimension.height);
    };

    XRect.prototype.containsPoint = function(point)
    {
        if (point == null)
        {
            return false;
        }

        if (point instanceof XPoint)
        {
            return ((point.x >= this.left) && (point.x <= this.right) && (point.y >= this.bottom) && (point.y <= this.top));
        }

        return this.containsPoint(XPoint.fromPoint(point));
    };

    // -----------------------------------------------------------------------------------------------------------------

    function XLine(a, b, c)
    {
        let a1 = a;
        let b1 = b;
        let c1 = c;

        if (b != 0)
        {
            if (b != 1)
            {
                a1 = a / b;
                b1 = 1;
                c1 = c / b;
            }
        }
        else if (a != 0)
        {
            if (a != 1)
            {
                a1 = 1;
                b1 = 0;
                c1 = c / a;
            }
        }
        else
        {
            throw "Cannot create line. The specified parameters a and b both are 0."
        }

        this.a = a1;
        this.b = b1;
        this.c = c1;
    }

    XLine.prototype.getLogString = function()
    {
        return "XLine(" + this.a + ", " + this.b + ", " + this.c + ")";
    };

    XLine.fromXY = function(x1, y1, x2, y2)
    {
        if ((x1 == x2) && (y1 == y2))
        {
            throw "Cannot create line. The specified points are identical.";
        }

        return new XLine(y1 - y2, x2 - x1, x2 * y1 - x1 * y2);
    };

    XLine.fromPoints = function(point1, point2)
    {
        return XLine.fromXY(point1.x, point1.y, point2.x, point2.y);
    };

    XLine.getPerpendicularBisector = function(point1, point2)
    {
        let x1 = point1.x;
        let y1 = point1.y;
        let x2 = point2.x;
        let y2 = point2.y;

        if (y1 == y2)
        {
            if (x1 == x2)
            {
                throw "Cannot calculate the perpendicular bisector. The specified points are identical.";
            }

            return new XLine(1, 0, (x1 + x2) / 2);
        }

        let a = (x1 - x2) / (y1 - y2);
        let c = (x1 * x1 - x2 * x2 + y1 * y1 - y2 * y2) / (2 * (y1 - y2));

        return new XLine(a, 1, c);
    };

    XLine.prototype.getIntersectionPoint = function(lineOrLineSection)
    {
        if (lineOrLineSection instanceof (XLineSection))
        {
            return lineOrLineSection.getIntersectionPoint(this);
        }

        let line2 = lineOrLineSection;

        let a1 = this.a;
        let b1 = this.b;
        let c1 = this.c;

        let a2 = line2.a;
        let b2 = line2.b;
        let c2 = line2.c;

        let d = a1 * b2 - a2 * b1;

        if (d == null) // parallel (incl. identical)
        {
            return null;
        }

        let x = (b2 * c1 - b1 * c2) / d;
        let y = (a1 * c2 - a2 * c1) / d;

        return new XPoint(x, y);
    };

    XLine.prototype.getPointFromX = function(x)
    {
        if (this.b == 0)
        {
            return null;
        }

        return new XPoint(x, (this.c - this.a * x) / this.b);
    };

    XLine.prototype.getPointFromY = function(y)
    {
        if (this.a == 0)
        {
            return null;
        }

        return new XPoint((this.c - this.b * y) / this.a, y);
    };

    XLine.prototype.getHeading = function()
    {
        let heading = vgap.getHeading(0, 0, this.a, this.b) + 90;
        while (heading >= 180)
        {
            heading -= 180;
        }

        return heading;
    };

    XLine.prototype.clip = function(rect)
    {
        let ip = [];
        ip.push(rect.getLeftSection().getIntersectionPoint(this));
        ip.push(rect.getRightSection().getIntersectionPoint(this));
        ip.push(rect.getBottomSection().getIntersectionPoint(this));
        ip.push(rect.getTopSection().getIntersectionPoint(this));

        let p1 = null;
        let p2 = null;

        for (var i = 0; (p1 == null) && (i < ip.length); i++)
        {
            p1 = ip[i];
        }

        for (; (p2 == null) && (i < ip.length); i++)
        {
            p2 = ip[i];
        }

        if ((p1 == null) || (p2 == null) || p1.equals(p2))
        {
            return null;
        }

        return XLineSection.fromPoints(p1, p2);
    };

    // -----------------------------------------------------------------------------------------------------------------

    function XLineSection(x1, y1, x2, y2)
    {
        this.point1 = new XPoint(x1, y1);
        this.point2 = new XPoint(x2, y2);

        if (this.point1.equals(this.point2))
        {
            throw "Cannot create line section. The specified points are identical.";
        }
    }

    XLineSection.fromPoints = function(point1, point2)
    {
        return new XLineSection(point1.x, point1.y, point2.x, point2.y);
    };

    XLineSection.prototype.equals = function(lineSection)
    {
        if (lineSection == null)
        {
            return false;
        }

        return (this.point1.equals(lineSection.point1) && (this.point2.equals(lineSection.point2)));
    };

    XLineSection.prototype.getLogString = function()
    {
        return "XLineSection(" + this.point1.x + ", " + this.point1.y + ", " + this.point2.x + ", " + this.point2.y + ")";
    };

    XLineSection.prototype.getLine = function()
    {
        return XLine.fromPoints(this.point1, this.point2);
    };

    XLineSection.prototype.getBoundingRect = function()
    {
        return XRect.fromPoints(this.point1, this.point2);
    };

    XLineSection.prototype.offsetXY = function(dx, dy)
    {
        return new XLineSection(this.point1.x + dx, this.point1.y + dy, this.point2.x + dx, this.point2.y + dy);
    };

    XLineSection.prototype.offsetDimension = function(dimension)
    {
        return new XLineSection(this.point1.x + dimension.getWidth(), this.point1.y + dimension.getHeight(), this.point2.x + dimension.getWidth(), this.point2.y + dimension.getHeight());
    };

    XLineSection.prototype.getIntersectionPoint = function(lineOrLineSection)
    {
        let isParamLineSection = (lineOrLineSection instanceof (XLineSection));

        let line1 = this.getLine();
        let line2 = (isParamLineSection ? lineOrLineSection.getLine() : lineOrLineSection);

        let point = line1.getIntersectionPoint(line2);

        if ((point != null) && this.getBoundingRect().containsPoint(point) && (!isParamLineSection || lineOrLineSection.getBoundingRect().containsPoint(point)))
        {
            return point;
        }

        return null;
    };

    XLineSection.prototype.getMidPoint = function()
    {
        let x = (this.point1.x + this.point2.x) / 2;
        let y = (this.point1.y + this.point2.y) / 2;

        return new XPoint(x, y);
    };

    XLineSection.prototype.getPointFromX = function(x)
    {
        let p = this.getLine().getPointFromX(x);
        if (this.getBoundingRect().containsPoint(p))
        {
            return p;
        }

        return null;
    };

    XLineSection.prototype.getPointFromY = function(y)
    {
        let p = this.getLine().getPointFromY(y);
        if (this.getBoundingRect().containsPoint(p))
        {
            return p;
        }

        return null;
    };

    XLineSection.prototype.getHeading = function()
    {
        return xMapUtils.getHeading(this.point1, this.point2);
    };

    XLineSection.prototype.clip = function(rect)
    {
        let containsP1 = rect.containsPoint(this.point1);
        let containsP2 = rect.containsPoint(this.point2);

        if (containsP1 && containsP2)
        {
            return this;
        }

        let ips = [];
        ips.push(rect.getLeftSection().getIntersectionPoint(this));
        ips.push(rect.getRightSection().getIntersectionPoint(this));
        ips.push(rect.getBottomSection().getIntersectionPoint(this));
        ips.push(rect.getTopSection().getIntersectionPoint(this));

        let ip1 = null;
        let ip2 = null;

        for (var i = 0; (ip1 == null) && (i < ips.length); i++)
        {
            ip1 = ips[i];
        }

        for (; (ip2 == null) && (i < ips.length); i++)
        {
            ip2 = ips[i];
        }

        if (containsP1)
        {
            if (!ip1.equals(this.point1))
            {
                return XLineSection.fromPoints(this.point1, ip1);
            }

            if ((ip2 != null) && (!ip2.equals(this.point1)))
            {
                return XLineSection.fromPoints(this.point1, ip2);
            }

            return null;
        }

        if (containsP2)
        {
            if (!ip1.equals(this.point2))
            {
                return XLineSection.fromPoints(ip1, this.point2);
            }

            if ((ip2 != null) && (!ip2.equals(this.point2)))
            {
                return XLineSection.fromPoints(ip2, this.point2);
            }

            return null;
        }

        if ((ip1 == null) || (ip2 == null) || ip1.equals(ip2))
        {
            return null;
        }

        let section = XLineSection.fromPoints(ip1, ip2);
        if (section.getHeading() == this.getHeading())
        {
            return section;
        }

        return XLineSection.fromPoints(ip2, ip1);
    };

    // -----------------------------------------------------------------------------------------------------------------

    function XDrawParams()
    {
        this.strokeStyle = "#FFFFFF";
        this.lineWidth = 1;
        this.sphereDuplication = xConst.sphereDuplication.ALPHA;
    }

    XDrawParams.prototype.getLogString = function()
    {
        return "XDrawParams(strokeStyle: " + this.strokeStyle + ", lineWidth: " + this.lineWidth + ", sphereDuplication: " + this.sphereDuplication + ")";
    };

    XDrawParams.prototype.setStrokeStyle = function(strokeStyle)
    {
        this.strokeStyle = strokeStyle;

        return this;
    };

    XDrawParams.prototype.setLineWidth = function(lineWidth)
    {
        this.lineWidth = lineWidth;

        return this;
    };

    XDrawParams.prototype.setSphereDuplication = function(sphereDuplication)
    {
        this.sphereDuplication = sphereDuplication;

        return this;
    };

    // -----------------------------------------------------------------------------------------------------------------
}

let script = document.createElement("script");
script.type = "application/javascript";
let globalText = "" + global;
globalText = globalText.substring(globalText.indexOf('{') + 1, globalText.lastIndexOf('}'));
script.textContent = globalText + ";(" + wrapper + ")(\"" + GM_info.script.version + "\");";
document.body.appendChild(script);
document.body.removeChild(script);