// ==UserScript== // @author Tarsi210 // @id draw-best-link-star@Tarsi210 // @name Draw Best Link Star // @category Info // @version 0.0.8 // @namespace https://www.nathanpralle.com // @description Build the best link star in an area. // @updateURL https://raw.githubusercontent.com/IITC-CE/Community-plugins/master/dist/Tarsi210/draw-best-link-star.meta.js // @downloadURL https://raw.githubusercontent.com/IITC-CE/Community-plugins/master/dist/Tarsi210/draw-best-link-star.user.js // @depends draw-tools@breunigs // @include https://intel.ingress.com/* // @match https://intel.ingress.com/* // @grant none // ==/UserScript== function wrapper(plugin_info) { // ensure plugin framework is there, even if iitc is not yet loaded if(typeof window.plugin !== 'function') window.plugin = function() {}; // PLUGIN START //////////////////////////////////////////////////////// // use own namespace for plugin window.plugin.drawBestLinkStar = function() {}; window.plugin.drawBestLinkStar.setupCallback = function() { $('#toolbox').append(' Draw Best Link Star'); }; function createModal() { const modal = document.createElement('div'); modal.id = 'linkStarModal'; modal.style.display = 'none'; modal.style.position = 'fixed'; modal.style.zIndex = '1000'; modal.style.left = '10%'; modal.style.top = '10%'; modal.style.backgroundColor = '#fff'; modal.style.padding = '20px'; modal.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)'; modal.style.borderRadius = '10px'; const modalContent = `
Auto-Select Best Portal
`; modal.innerHTML = modalContent; document.body.appendChild(modal); } function createResultsModal() { const resultsmodal = document.createElement('div'); resultsmodal.id = 'linkStarResultsModal'; resultsmodal.style.display = 'none'; resultsmodal.style.position = 'fixed'; resultsmodal.style.zIndex = '1000'; resultsmodal.style.left = '10%'; resultsmodal.style.top = '10%'; resultsmodal.style.backgroundColor = '#fff'; resultsmodal.style.padding = '20px'; resultsmodal.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)'; resultsmodal.style.borderRadius = '10px'; const modalContent = `

`; resultsmodal.innerHTML = modalContent; document.body.appendChild(resultsmodal); } window.plugin.drawBestLinkStar.showModal=function(callback) { const modal = document.getElementById('linkStarModal'); const submitButton = document.getElementById('submitButton'); const linkStarModalTargetPortal=document.getElementById('linkStarModalTargetPortal'); submitButton.onclick = () => { const searchRadius = parseInt(document.getElementById('searchRadius').value, 10); const maxLinks = parseInt(document.getElementById('maxPortals').value, 10); const ignoreCrossedLinks = document.getElementById('ignoreCrossedLinks').checked; if (isNaN(searchRadius) || isNaN(maxLinks) || searchRadius <= 0 || maxLinks <= 0) { alert("Invalid input. Please enter positive numbers."); return; } modal.style.display = 'none'; callback({ searchRadius, maxLinks, ignoreCrossedLinks }); }; let bestPortal = getSelectedPortal(); // Use selected portal if available if (bestPortal) { const portalName = bestPortal.options.data.title; const portalUrl = `https://intel.ingress.com/?pll=${bestPortal.getLatLng().lat},${bestPortal.getLatLng().lng}`; const newContent = `${portalName}`; linkStarModalTargetPortal.innerHTML = newContent; } modal.style.display = 'block'; } function showResultsModal(content) { const resultsmodal = document.getElementById('linkStarResultsModal'); const resultscontent = document.getElementById('linkStarResultsModalContent'); const closeButton = document.getElementById('linkStarCloseButton'); resultscontent.innerHTML = content; closeButton.onclick = () => { resultsmodal.style.display = 'none'; }; resultsmodal.style.display = 'block'; } //Find all portals that are visible function getVisiblePortals() { const bounds = map.getBounds(); const visiblePortals = []; for (const guid in window.portals) { const portal = window.portals[guid]; if (bounds.contains(portal.getLatLng())) { visiblePortals.push(portal); } } return visiblePortals; } function getSelectedPortal() { const selectedPortal = window.selectedPortal; if (selectedPortal) { return window.portals[selectedPortal]; } return null; } //Find the best portal to use for the center function findBestPortal(visiblePortals, radius) { let bestPortal = null; let bestScore = 0; let bestMaxDistance = Infinity; visiblePortals.forEach(portal => { const portalLatLng = portal.getLatLng(); const nearbyPortals = []; for (const guid in window.portals) { if (guid !== portal.options.guid) { const otherPortal = window.portals[guid]; const distance = portalLatLng.distanceTo(otherPortal.getLatLng()); if (distance <= radius) { nearbyPortals.push(otherPortal); } } } const maxDistance = Math.max(...nearbyPortals.map(p => portalLatLng.distanceTo(p.getLatLng()))); const score = nearbyPortals.length; if (score > bestScore || (score === bestScore && maxDistance < bestMaxDistance)) { bestPortal = portal; bestScore = score; bestMaxDistance = maxDistance; } }); return bestPortal; } function calculateDistanceMatrix(portals) { const distances = []; for (let i = 0; i < portals.length; i++) { distances[i] = []; for (let j = 0; j < portals.length; j++) { distances[i][j] = portals[i].getLatLng().distanceTo(portals[j].getLatLng()); } } return distances; } function findOptimalPath(distanceMatrix) { const n = distanceMatrix.length; const visited = new Array(n).fill(false); const path = [0]; // Start from the first portal visited[0] = true; for (let i = 1; i < n; i++) { let last = path[path.length - 1]; let nearest = -1; let nearestDistance = Infinity; for (let j = 0; j < n; j++) { if (!visited[j] && distanceMatrix[last][j] < nearestDistance) { nearest = j; nearestDistance = distanceMatrix[last][j]; } } path.push(nearest); visited[nearest] = true; } return path; } function doLinesIntersect(line1, line2) { const [p1, p2] = line1; const [q1, q2] = line2; function ccw(A, B, C) { return (C.lat - A.lat) * (B.lng - A.lng) > (B.lat - A.lat) * (C.lng - A.lng); } return (ccw(p1, q1, q2) !== ccw(p2, q1, q2)) && (ccw(p1, p2, q1) !== ccw(p1, p2, q2)); } function isLinkCrossingAnyExistingLinks(link) { const existingLinks = window.links; for (const guid in existingLinks) { const existingLink = existingLinks[guid]; const latLngs = existingLink.getLatLngs(); const existingLine = [latLngs[0], latLngs[1]]; if (doLinesIntersect(link, existingLine)) { return true; } } return false; } function isPossibleLinkCrossingAnyExistingLinks(portalALatLngs,portalBLatLngs) { const existingLinks = window.links; for (const guid in existingLinks) { const existingLink = existingLinks[guid]; const latLngs = existingLink.getLatLngs(); const existingLine = [latLngs[0],latLngs[1]]; if (doLinesIntersect([portalALatLngs,portalBLatLngs], existingLine)) { return true; } } return false; } function generateCSV(portals,path,bestPortal,distanceMatrix) { let csvContent = "data:text/csv;charset=utf-8,\"Name\",\"Lat\",\"Lng\",\"URL\",\"Distance to Next (meters)\"\n"; path.forEach((index, i) => { const portal = portals[index]; const portalLatLng = portal.getLatLng(); const portalUrl = `https://intel.ingress.com/?pll=${portalLatLng.lat},${portalLatLng.lng}`; let portalName="No Name Loaded"; if(portal.options.data.title){ portalName = portal.options.data.title.replace(/"/g, '""'); // Escape double quotes } const nextIndex = path[(i + 1) % path.length]; const distanceToNext = distanceMatrix[index][nextIndex].toFixed(2); // Distance to the next portal csvContent += `"${portalName}","${portalLatLng.lat}","${portalLatLng.lng}","${portalUrl}","${distanceToNext}"\n`; }); const encodedUri = encodeURI(csvContent); const link = document.createElement("a"); link.setAttribute("href", encodedUri); link.setAttribute("download", `${bestPortal.options.data.title} Link Star Plan.csv`); document.body.appendChild(link); return link; } function calculateTotalDistance(path, distanceMatrix) { let totalDistance = 0; for (let i = 0; i < path.length - 1; i++) { totalDistance += distanceMatrix[path[i]][path[i + 1]]; } totalDistance += distanceMatrix[path[path.length - 1]][path[0]]; // Return to start return totalDistance; } //Build the link star window.plugin.drawBestLinkStar.build = function(config){ if (!window.plugin.drawTools) { alert("DrawTools plugin is required!"); return; } const { searchRadius, maxLinks, ignoreCrossedLinks } = config; const visiblePortals = getVisiblePortals(); if (visiblePortals.length === 0) { alert("No visible portals found. Try changing your zoom level or you didn't loading finish first."); return; } const totalVisiblePortals = visiblePortals.length; let userSelectedPortal = 0; let bestPortal = getSelectedPortal(); // Use selected portal if available if (!bestPortal) { bestPortal = findBestPortal(visiblePortals, searchRadius); } else{ userSelectedPortal=1; } if (!bestPortal) { alert("No suitable portal found (or selected) for creating a link star."); return; } const bestPortalLatLng = bestPortal.getLatLng(); // Collect nearby portals for the best portal const nearbyPortals = []; for (const guid in window.portals) { if (guid !== bestPortal.options.guid) { const portal = window.portals[guid]; const distance = bestPortalLatLng.distanceTo(portal.getLatLng()); const crossesLine = isPossibleLinkCrossingAnyExistingLinks(bestPortalLatLng,portal.getLatLng()); if (distance <= searchRadius && (!crossesLine || ignoreCrossedLinks)){ nearbyPortals.push(portal); } } } // Limit to the maximum number of links nearbyPortals.sort((a, b) => bestPortalLatLng.distanceTo(a.getLatLng()) - bestPortalLatLng.distanceTo(b.getLatLng())); const selectedPortals = nearbyPortals.slice(0, maxLinks); // Use DrawTools to draw the links const drawData = []; selectedPortals.forEach(portal => { const link = { type: 'polyline', latLngs: [ [bestPortalLatLng.lat, bestPortalLatLng.lng], [portal.getLatLng().lat, portal.getLatLng().lng] ], color: '#A020F0' //Optional: Set link color to red }; const linkLatLngs = [ { lat: bestPortalLatLng.lat, lng: bestPortalLatLng.lng }, { lat: portal.getLatLng().lat, lng: portal.getLatLng().lng } ]; if (!isLinkCrossingAnyExistingLinks(linkLatLngs) || ignoreCrossedLinks) { drawData.push(link); } }); // Add links to DrawTools window.plugin.drawTools.drawnItems.clearLayers(); drawData.forEach(link => { const layer = L.polyline(link.latLngs, { color: link.color || '#A020F0' }); window.plugin.drawTools.drawnItems.addLayer(layer); }); // Calculate the optimal path const distanceMatrix = calculateDistanceMatrix(selectedPortals); const optimalPath = findOptimalPath(distanceMatrix); const totalDistance = calculateTotalDistance(optimalPath, distanceMatrix)/1000; const totalMiles = totalDistance * 0.621371; // Generate CSV const csvLink = generateCSV(selectedPortals, optimalPath,bestPortal,distanceMatrix); const portalName = bestPortal.options.data.title; const portalUrl = `https://intel.ingress.com/?pll=${bestPortal.getLatLng().lat},${bestPortal.getLatLng().lng}`; let message = ''; if(userSelectedPortal){ message = `
Choosing portals within ${searchRadius} meters of Selected Portal:
`; } else{ message = `
Best Target Portal with radius of ${searchRadius} meters:
`; } message+=`   ${portalName}
${drawData.length} of possible ${maxLinks} links in the radius (${totalVisiblePortals} visible portals)
Total Travel Distance: ${totalDistance.toFixed(1)} km (${totalMiles.toFixed(1)} miles)
   Download portal visit order (CSV)
`; showResultsModal(message); } //Run setup var setup = function() { window.plugin.drawBestLinkStar.setupCallback(); if (window.plugin.drawTools === undefined) { alert("'Draw Best Link Star' requires 'draw-tools'"); return; } // this plugin also needs to create the draw-tools hook, in case it is initialised before draw-tools window.pluginCreateHook('pluginDrawTools'); // Add a button to trigger the link star creation const button = document.createElement('button'); button.textContent = 'Draw Best Link Star'; button.style.position = 'fixed'; button.style.top = '1%'; button.style.left = '10%'; button.style.zIndex = '9999'; button.onclick = () => window.plugin.drawBestLinkStar.showModal(window.plugin.drawBestLinkStar.build); document.body.appendChild(button); // Initialize modal dialog for input createModal(); createResultsModal(); }; // PLUGIN END ////////////////////////////////////////////////////////// setup.info = plugin_info; //add the script info data to the function as a property if(!window.bootPlugins){ window.bootPlugins = []; } window.bootPlugins.push(setup); // if IITC has already booted, immediately run the 'setup' function if(window.iitcLoaded && typeof setup === 'function') setup(); } // wrapper end // inject code into site context var script = document.createElement('script'); var info = {}; if (typeof GM_info !== 'undefined' && GM_info && GM_info.script){ info.script = { version: GM_info.script.version, name: GM_info.script.name, description: GM_info.script.description }; } script.appendChild(document.createTextNode('('+ wrapper +')('+JSON.stringify(info)+');')); (document.body || document.head || document.documentElement).appendChild(script);