Xorboo / ConsulSegmentsOutliner

// OUTDATED AND NOT WORKING (yet)

// ==UserScript==
// @name        ConsulSegmentsOutliner
// @namespace   consulwar.ru
// @description Basic space segments visual outliner
// @require     https://code.jquery.com/jquery-1.12.4.js
// @version     0.2.1
// @author      Xorboo
// @license     MIT
// @match       *://consulwar.ru/*
// ==/UserScript==

/* RELEASE NOTES
  0.2.1
    . fixed drawing outside of the screen view
    . improved performance a bit
  0.1.1
    + basic outliner
*/


var fillOpacity = "20";   // in hex
var strokeOpacity = "AA"; // in hex
var strokeWidth = 2;
var palette = ["FFFFFF", "FF0000", "008000", "0000FF", "FFA500", "00FF00", "FF00FF", "FFFF00", "00FFFF"];


var canvasExtraSize = {x:2000, y:1000};
var zoomFactor = 1.0;
var currentBounds = null;


$(function() {
    log('============ Start');

    Template.cosmosObjects.onRendered(function() {
        addZoomListener();
        updatePolygons();
    });

    log('============ End');
});


function addZoomListener() {
    var basePlanet = Game.Planets.getBase();
    var basePlanetElement = $(".leaflet-marker-pane div[data-id='" + basePlanet._id + "']");

    var observer = new MutationObserver(function(mutations) {
        updatePolygons();
    });
    observer.observe(basePlanetElement[0], { attributes : true, attributeFilter : ['style', 'class'] });
}


function updatePolygons() {
    log("Updating polygons...");

    updateZoomFactor();
    var planets = getGroupedPlanets();
    drawPlanets(planets);

    log("Done!");
}


function updateZoomFactor() {
    var basePlanet = Game.Planets.getAll().fetch()[0];
    var el = $(".leaflet-marker-pane div[data-id='" + basePlanet._id + "']");

    var zoomClass = "";
    var classes = el.attr('class').split(' ');
    for (var i = classes.length - 1; i >= 0; i--) {
        var testClass = classes[i];
        if (testClass.startsWith("zoom-lt-")) {
            zoomClass = testClass.trim();
            break;
        }
    }

    if (zoomClass == "zoom-lt-5") {
        zoomFactor = 0.25;
    }
    else if (zoomClass == "zoom-lt-7") {
        zoomFactor = 0.5;
    }
    else if (zoomClass == "zoom-lt-9") {
        zoomFactor = 1.0;
    }
    else {
        zoomFactor = 1.8;
    }
    log("Zoom factor: " + zoomFactor);
}

function getGroupedPlanets() {
    var allPlanets = Game.Planets.getAll().fetch();

    // Get planet groups for each segment
    var planetGroups = allPlanets.reduce(function(groups, planet) {
        var planetGroup = planet.hand + "_" + planet.segment;
        if (groups.indexOf(planetGroup) === -1) {
            groups.push(planetGroup);
        }
        return groups;
    }, []);

    // Read all planet transforms
    var regExp = new RegExp("matrix\\(\\d+, \\d+, \\d+, \\d+, (-?\\d+), (-?\\d+)\\)");
    var planetElements = $(".leaflet-marker-pane").find("div.map-planet-marker");
    var planetsDict = {};
    planetElements.each(function(index) {
        var el = $(this);
        var matches = regExp.exec(el.css("transform"));

        planetsDict[el.attr("data-id")] = {
            //planet: planetElement,
            x: parseInt(matches[1]),
            y: parseInt(matches[2])
        };
    });

    // Group planets
    var groups = planetGroups.map(function(group) {
        var groupPlanets = allPlanets.filter(function(planet) {
            var planetGroup = planet.hand + "_" + planet.segment;
            return planetGroup === group;
        });

        var groupElements = groupPlanets.map(function(planet) {
            return planetsDict[planet._id];
        });

        var planet = groupPlanets[0];
        var groupIndex = planet.segment * 2 + planet.hand * 3;

        return {
            planets: groupElements,
            index: groupIndex
        };
    });

    return groups;
}


function drawPlanets(planets) {
    var root = $("#segment-outliner");
    if (!root.length) {
        $(".leaflet-map-pane").prepend(`<svg id="segment-outliner" height="3000px" width="5000px"></svg>`);
        root = $("#segment-outliner");
    }

    currentBounds = null;
    $.each(planets, function(index, group) {
        var points = addExtraPoints(group.planets);
        var convexPolygon = convexHull(points);
        group.finalPolygon = extrudePolygon(convexPolygon);
        updateBounds(group.finalPolygon);
    });

    var plygonsHtml = "";
    $.each(planets, function(index, group) {
        var color = palette[group.index % palette.length];
        plygonsHtml += drawGroup(group.finalPolygon, color, root);
    });

    root.attr("width", currentBounds.maxX - currentBounds.minX);
    root.attr("height", currentBounds.maxY - currentBounds.minY);
    root.css("transform", "translate3d(" + currentBounds.minX + "px, " + currentBounds.minY + "px, 0px)");

    root.html(plygonsHtml);
}


function updateBounds(polygon) {
    $.each(polygon, function(index, point) {
        if (!currentBounds) {
            currentBounds = {
                minX: point.x,
                maxX: point.x,
                minY: point.y,
                maxY: point.y
            };
        }
        currentBounds.minX = Math.min(currentBounds.minX, point.x);
        currentBounds.maxX = Math.max(currentBounds.maxX, point.x);
        currentBounds.minY = Math.min(currentBounds.minY, point.y);
        currentBounds.maxY = Math.max(currentBounds.maxY, point.y);
    });
}

function drawGroup(polygon, color, root) {
    var pointsText = "";
    $.each(polygon, function(index, point) {
        pointsText += (point.x - currentBounds.minX) + "," + (point.y - currentBounds.minY) + " ";
    });
    var fillColor = "#" + color + fillOpacity;
    var strokeColor = "#" + color + strokeOpacity;
    return `<polygon points="` + pointsText + `" style="fill:` + fillColor + `;stroke:` + strokeColor + `;stroke-width:` + strokeWidth + `" />`;
}


function addExtraPoints(points) {
    var shift = 10 * zoomFactor;

    if (points.length === 1) {
        var p = points[0];
        return [
            { x: p.x - shift, y: p.y - shift },
            { x: p.x - shift, y: p.y + shift },
            { x: p.x + shift, y: p.y - shift },
            { x: p.x + shift, y: p.y + shift }
        ];
    }

    if (points.length === 2) {
        var p1 = points[0];
        var p2 = points[1];
        var d = { x: p2.x - p1.x, y: p2.y - p1.y };
        var len = Math.sqrt(d.x * d.x + d.y * d.y);
        var norm = { x: d.x * shift / len, y: -d.y * shift / len };

        return [
            { x: p1.x + norm.x, y: p1.y + norm.y },
            { x: p1.x - norm.x, y: p1.y - norm.y },
            { x: p2.x + norm.x, y: p2.y + norm.y },
            { x: p2.x - norm.x, y: p2.y - norm.y }
        ];
    }

    return points;
}

function extrudePolygon(polygon) {
    // Add extra points
    var normalShift = 25 * zoomFactor;
    var points = [];
    var len = polygon.length;
    for(var i = 0; i < len; i++) {
        var p = polygon[i];

        var prev = polygon[(i - 1 + len) % len];
        var anglePrev = Math.atan2(p.y - prev.y, p.x - prev.x) + Math.PI / 2;
        points.push({
            x: p.x + Math.cos(anglePrev) * normalShift,
            y: p.y + Math.sin(anglePrev) * normalShift
        });

        var next = polygon[(i + 1) % len];
        var angleNext = Math.atan2(p.y - next.y, p.x - next.x) - Math.PI / 2;
        points.push({
            x: p.x + Math.cos(angleNext) * normalShift,
            y: p.y + Math.sin(angleNext) * normalShift
        });
    }

    // Extrude from the center
    var p0 = points[0];
    var b = { minX: p0.x, minY: p0.y, maxX: p0.x, maxY: p0.y };

    $.each(points, function(index, p) {
        b.minX = Math.min(b.minX, p.x);
        b.maxX = Math.max(b.maxX, p.x);
        b.minY = Math.min(b.minY, p.y);
        b.maxY = Math.max(b.maxY, p.y);
    });
    var c = { x: (b.maxX + b.minX) / 2, y: (b.maxY + b.minY) / 2 };

    var shift = 15 * zoomFactor;
    $.each(points, function(index, p) {
        var d = { x: p.x - c.x, y : p.y - c.y };
        var len = Math.sqrt(d.x * d.x + d.y * d.y);
        p.x += d.x * shift / len;
        p.y += d.y * shift / len;
    });

    return points;
}


function convexHull(points) {
    points.sort(function (a, b) {
        return a.x != b.x ? a.x - b.x : a.y - b.y;
    });

    var n = points.length;
    var hull = [];

    for (var i = 0; i < 2 * n; i++) {
        var j = i < n ? i : 2 * n - 1 - i;
        while (hull.length >= 2 && removeMiddle(hull[hull.length - 2], hull[hull.length - 1], points[j]))
            hull.pop();
        hull.push(points[j]);
    }

    hull.pop();
    return hull;
}

function removeMiddle(a, b, c) {
    var cross = (a.x - b.x) * (c.y - b.y) - (a.y - b.y) * (c.x - b.x);
    var dot = (a.x - b.x) * (c.x - b.x) + (a.y - b.y) * (c.y - b.y);
    return cross < 0 || cross === 0 && dot <= 0;
}


// LOGGING
function log(text) {
    console.log('ConsulSegmentsOutliner: ' + text);
}

function logObj(obj, obj_name='[Object]') {
    log('\'' + obj_name + '\' data:');
    console.dir(obj);
}