LOTRO Wiki - Shortest Path Between Map Points

Calculates and draws an overlay of the shortest path between points on a map page in the LOTRO Wiki (e.g. for doing explorer deeds)

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         LOTRO Wiki - Shortest Path Between Map Points
// @namespace    http://tampermonkey.net/
// @version      2024-09-16u3
// @description  Calculates and draws an overlay of the shortest path between points on a map page in the LOTRO Wiki (e.g. for doing explorer deeds)
// @author       Morde
// @match        https://lotro-wiki.com/wiki/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=lotro-wiki.com
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    "use strict";
    // Wait until the map has loaded
    var interval = setInterval(() => {
        if (document.getElementById("mw-content-text")) return;

        // We have to check like this because the map page can be navigated to via a frontend router
        // (e.g. when clicking a coordinate link), so the URL can change without a page load actually happening
        var expectedNumPoints = window.location.href
        .matchAll(/(\d+\.\d+(?:S|N),\d+\.\d+(?:W|E))/g)
        .toArray().length;

        if (!expectedNumPoints) return;

        var container = document.querySelector("#content > div");

        if (!container) return;

        var points = Array.from(
            document.querySelectorAll("#content > div > img[style]")
        ).map((ele) => ({
            x:
            (Number(ele.style.left.slice(0, -1)) / 100) *
            Number(container.attributes.width.value),
            y:
            (Number(ele.style.top.slice(0, -1)) / 100) *
            Number(container.attributes.height.value),
        }));

        console.log(expectedNumPoints);

        if (points.length != expectedNumPoints) return;

        clearInterval(interval);

        var weights = {};

        for (var i = 0; i < points.length; i++) {
            for (var j = 0; j < points.length; j++) {
                if (i == j) continue;

                if (!weights[i]) weights[i] = {};

                weights[i][j] =
                    ((points[i].x - points[j].x) ** 2 +
                     (points[i].y - points[j].y) ** 2) **
                    0.5;
            }
        }

        console.table(weights);

        // Find a tentative permutation with the lowest sum of weight transitions
        var candidate = getBestCandidate(weights);

        drawBetweenPoints(candidate.chain, points, container);
    }, 100);

    function getBestCandidate(weights) {
        var numWeights = Object.keys(weights).length;

        var results = [];

        var start = new Date();

        for (var i = 0; i < numWeights; i++) results.push(getCandidate(weights, i));

        // filter out duplicates
        results = Object.values(
            results.reduce((acc, x) => {
                acc[x.chain.join(",")] = x;
                return acc;
            }, {})
        );

        results.sort((a, b) => a.distance - b.distance);

        console.table(results);

        var bestChain = results[0].chain;
        var bestDist = results[0].distance;

        console.log("Improving...");

        results = results.slice(0, Math.ceil(results.length / 2)); // check the more promising-looking half

        results.forEach(({ chain, distance }) => {
            var origDist = distance;
            var { chain, distance } = improveCandidate(
                chain,
                distance,
                weights,
                true
            );

            if (distance < bestDist) {
                console.log({ origDist, distance });
                bestChain = chain;
                bestDist = distance;
            }
        });

        var { chain, distance } = finalizeCandidate(bestChain, bestDist, weights);
        bestChain = chain;
        bestDist = distance;

        var elapsed = new Date() - start;
        console.log(`Took ${elapsed / 1000}s`);

        console.log(bestDist);

        return { chain: bestChain, distance: bestDist };
    }

    function getCandidate(weights, current) {
        var chain = [current];
        var used = new Set(chain);
        var numWeights = Object.keys(weights).length;
        var distance = 0;

        while (chain.length < numWeights) {
            var entries = Object.entries(weights[current]).map(([key, value]) => [
                Number(key),
                value,
            ]);
            var options = entries.filter(([key, value]) => !used.has(key));

            var [key, value] = options.reduce(
                ([bestKey, bestValue], [key, value]) => {
                    return value < bestValue ? [key, value] : [bestKey, bestValue];
                },
                [-1, Number.POSITIVE_INFINITY]
            );

            if (key < 0) break;

            used.add(key);
            chain.push(key);
            distance += value;

            current = key;
        }

        return improveCandidate(chain, distance, weights);
    }

    function improveCandidate(chain, distance, weights, double = false) {
        var improved = true;

        while (improved) {
            improved = false;

            var bestChain = chain;
            var bestDist = distance;

            for (var i0 = 0; i0 < chain.length; i0++) {
                // Try all possible variations where chain[i0] is moved to other parts of the chain
                for (var j0 = 0; j0 <= chain.length; j0++) {
                    if (i0 == j0) continue;

                    var testChain0 = chain.slice();
                    var value = testChain0[i0];
                    var offset = i0 < j0 ? -1 : 0;

                    testChain0.splice(i0, 1);
                    testChain0.splice(j0 + offset, 0, value);

                    if (!double) {
                        var testDist = getDistance(testChain0, weights);
                        if (testDist < bestDist) {
                            bestChain = testChain0;
                            bestDist = testDist;
                            improved = true;
                        }
                    } else {
                        // Try moving another part of the chain at the same time
                        for (var i1 = 0; i1 < chain.length; i1++) {
                            // Try all possible variations where chain[i1] is moved to other parts of the chain
                            for (var j1 = 0; j1 <= chain.length; j1++) {
                                var testChain1 = testChain0.slice();
                                if (i1 != j1) {
                                    value = testChain1[i1];
                                    offset = i1 < j1 ? -1 : 0;

                                    testChain1.splice(i1, 1);
                                    testChain1.splice(j1 + offset, 0, value);
                                }

                                var testDist = getDistance(testChain1, weights);
                                if (testDist < bestDist) {
                                    bestChain = testChain1;
                                    bestDist = testDist;
                                    improved = true;
                                }
                            }
                        }
                    }
                }
            }

            // Save change
            if (improved) {
                chain = bestChain;
                distance = bestDist;
            }
        }

        return { chain, distance };
    }

    function finalizeCandidate(chain, distance, weights) {
        var improved = true;

        while (improved) {
            improved = false;

            var bestChain = chain;
            var bestDist = distance;

            // Try to find a subset reversal that shortens the chain
            for (var i = 0; i < chain.length && !improved; i++) {
                for (var j = i + 1; j < chain.length && !improved; j++) {
                    var testChain = chain.slice();

                    var _i = i;
                    var _j = j;

                    // reverse between i and j
                    while (_i < _j) {
                        var value = testChain[_i];
                        testChain[_i] = testChain[_j];
                        testChain[_j] = value;
                        _i++;
                        _j--;
                    }

                    var testDist = getDistance(testChain, weights);

                    if (testDist < bestDist) {
                        bestChain = testChain;
                        bestDist = testDist;
                        improved = true;
                    }
                }
            }

            // Save change
            if (improved) {
                chain = bestChain;
                distance = bestDist;
            }
        }

        return { chain, distance };
    }

    function getDistance(chain, weights) {
        var result = 0;
        var prev = chain[0];
        for (var i = 1; i < chain.length; i++) {
            var next = chain[i];
            result += weights[prev][next];
            prev = next;
        }
        return result;
    }

    function drawBetweenPoints(indeces, points, container) {
        var prev = indeces[0];

        var imgs = Array.from(
            document.querySelectorAll("#content > div > img[style]")
        );
        console.log(indeces.map((x) => imgs[x]));

        for (var i = 1; i < indeces.length; i++) {
            var next = indeces[i];

            var prevPoint = points[prev];
            var nextPoint = points[next];

            drawFrom(prevPoint, nextPoint, container);

            prev = next;
        }
    }

    function drawFrom(pointA, pointB, container) {
        // Create a div of width=dist(pointA, pointB) at the midpoint between the points, rotated to be the angle between them
        var dist = ((pointA.x - pointB.x) ** 2 + (pointA.y - pointB.y) ** 2) ** 0.5;
        var midX = (pointA.x + pointB.x) / 2 - dist / 2;
        var midY = (pointA.y + pointB.y) / 2;
        var angle =
            (Math.atan2(pointA.y - pointB.y, pointA.x - pointB.x) * 180) / Math.PI;

        var div = document.createElement("div");
        div.style.position = "absolute";
        div.style.height = "3px";
        div.style.left = midX + "px";
        div.style.top = midY + "px";
        div.style.width = dist + "px";
        div.style.rotate = angle + "deg";
        div.style.background = "#0ff";

        container.childNodes[0].after(div);
    }
})();