// ==UserScript==
// @author DanielOnDiordna
// @name Portals list add-on
// @category Addon
// @version 1.1.0.20230321.232000
// @updateURL https://raw.githubusercontent.com/IITC-CE/Community-plugins/master/dist/DanielOnDiordna/portals-list-addon.meta.js
// @downloadURL https://raw.githubusercontent.com/IITC-CE/Community-plugins/master/dist/DanielOnDiordna/portals-list-addon.user.js
// @description [danielondiordna-1.1.0.20230321.232000] Add-on to only display portals for visible/enabled layers, a fix for Unclaimed/Placeholder Portals, added level filters and load portal details.
// @id portals-list-addon@DanielOnDiordna
// @namespace https://softspot.nl/ingress/
// @depends portals-list@teo96
// @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() {};
// use own namespace for plugin
window.plugin.portalslistAddon = function() {};
var self = window.plugin.portalslistAddon;
self.id = 'portalslistAddon';
self.title = 'Portals list add-on';
self.version = '1.1.0.20230321.232000';
self.author = 'DanielOnDiordna';
self.changelog = `
Changelog:
version 1.1.0.20230321.232000
- added Machina support when loading details
- added updating faction totals when loading details
- added updating history totals when loading details for Portals list version 0.4.0
- added updating level selectors when loading details
- improved continue loading details on failures
- fixed the footnotes dialog
version 1.0.0.20221010.154100
- reversed the changelog order
- fixed the neutral portals layer detection
- added error checking when replacing strings
- added error checking when running the eval
- fixed load details button by replacing display hidden with display none
- fixed support for stock plugin Portals list version 0.4.0
- moved the footnotes into a dialog
version 0.0.7.20210724.002500
- prevent double plugin setup on hook iitcLoaded
version 0.0.7.20210711.210800
- fixed row number for loaded portal details
- fixed problem that would filter away all neutral portals
- neutral portals now show no shields, resonators, mods and owne
- details for neutral portals are not loaded anymore
version 0.0.6.20210621.234600
- added level filter checkboxes
- changed filtering for visible layers
version 0.0.5.20210421.190200
- minor fix for IITC CE where runHooks iitcLoaded is executed before addHook is defined in this plugin
version 0.0.5.20210204.231200
- changed title from 'show list of portals add-on' to 'Portals list add-on' to match IITC-CE plugin name
- disabled skipping ghost portals in main plugin
- fixed sort functions to support ghost portals
- portal table displayed with fixed title and made scrollable
- updated plugin wrapper and userscript header formatting to match IITC-CE coding
version 0.0.4.20200308.232600
- replaced a lot of code to speed up the table updates
- added a stop loading button
- limit loading details to new details only
version 0.0.3.2020125.003100
- added loading of more portal details columns like shields, resonators, mods, owner
version 0.0.2.2020109.001300
- added a fix for portals with an undefined title, level and health
version 0.0.1.20191114.115600
- first release
`;
self.namespace = 'window.plugin.' + self.id + '.';
self.pluginname = 'plugin-' + self.id;
self.unclaimedlayername = 'Unclaimed/Placeholder Portals'; // Value will be retreived from window.setupMap
self.enllayername = 'Enlightened';
self.reslayername = 'Resistance';
self.levellayername = 'Level '; // Value will be retreived from window.setupMap
self.portallayername = ' Portals'; // Value will be retreived from window.setupMap
self.machinaname = 'U̶͚̓̍N̴̖̈K̠͔̍͑̂͜N̞̥͋̀̉Ȯ̶̹͕̀W̶̢͚͑̚͝Ṉ̨̟̒̅ ';
self.portaldetails = {};
self.requestlist = {};
self.requestid = undefined;
self.requestmaxretries = 3;
self.ownercolor = 'black';
self.shortmodnames =
{
'Heat Sink' :'H',
'Portal Shield' :'S',
'Link Amp' :'L',
'Turret' :'T',
'Multi-hack' :'M',
'Aegis Shield' :'A',
'Force Amp' :'F',
'SoftBank Ultra Link' :'U',
'Ito En Transmuter (+)':'I+',
'Ito En Transmuter (-)':'I-'
};
self.shortrarities =
{
'COMMON' : 'c',
'RARE' : 'r',
'VERY_RARE' : 'v'
};
self.filterlevel = [];
self.countlevel = [];
self.filterreversed = false;
self.gettotalshielding = function(guid,htmlformatting) {
if (!self.portaldetails[guid]) return (htmlformatting?(window.portals[guid] && (self.isMachinaPortal(window.portals[guid]) || window.portals[guid].options.team != window.TEAM_NONE) ? 'unknown' : ''):-1);
let details = self.portaldetails[guid];
let linkInfo = window.getPortalLinks(guid);
let linkCount = linkInfo.in.length + linkInfo.out.length;
let mitigationDetails = window.getPortalMitigationDetails(details,linkCount);
let totalshielding = mitigationDetails.shields + mitigationDetails.links;
return Math.round(totalshielding);
};
self.getresonatorstring = function(guid,htmlformatting,highlightowner) {
if (!self.portaldetails[guid]) return (self.requestlist[guid]?'(loading)':(window.portals[guid] && (self.isMachinaPortal(window.portals[guid]) || window.portals[guid].options.team != window.TEAM_NONE) ? '(unknown)' : '--------') );
if (!highlightowner) highlightowner = window.PLAYER.nickname;
let resonators = self.portaldetails[guid].resonators;
let portalowner = self.getportalowner(guid);
resonators = resonators.sort(function(b,a) {return (a.level + a.owner > b.level + b.owner) ? 1 : ((b.level + b.owner > a.level + a.owner) ? -1 : 0);}); // sort by resonator level resonator[owner,level]
let resolist = [];
for (let cnt=0; cnt<8; cnt++) {
let resonator = '-';
if (cnt < resonators.length && resonators[cnt]) {
//let nrg = parseInt(resonators[cnt].energy);
resonator = parseInt(resonators[cnt].level); // level
let resonatorowner = resonators[cnt].owner;
if (htmlformatting) {
if (resonatorowner === highlightowner) {
resonator = '' + resonator + ''; // highlight reso's of current player
} else if (resonatorowner !== portalowner) {
resonator = '' + resonator + ''; // underline reso's of other people then the portal owner
}
resonator = '' + resonator + '';
}
}
resolist.push(resonator);
}
return resolist.join('');
};
self.getmodstring = function(guid,htmlformatting,highlightowner) {
if (!self.portaldetails[guid]) return (self.requestlist[guid]?'(loading)':(window.portals[guid] && (self.isMachinaPortal(window.portals[guid]) || window.portals[guid].options.team != window.TEAM_NONE) ? '(unknown)' : ''));
if (!highlightowner) highlightowner = window.PLAYER.nickname;
let mods = self.portaldetails[guid].mods;
if (mods.length === 0) return '(empty)';
let portalowner = self.getportalowner(guid);
let modslist = [];
for (let cnt=0; cnt<4; cnt++) {
let mod = '';
if (cnt < mods.length && mods[cnt]) {
mod = self.shortmodnames[mods[cnt].name];
let modowner = mods[cnt].owner; // owner
if (mod === 'H' || mod === 'S' || mod === 'M') mod = self.shortrarities[mods[cnt].rarity] + mod;
if (htmlformatting) {
if (modowner === highlightowner) {
mod = '' + mod + ''; // highlight mods of current player
} else if (modowner !== portalowner) {
mod = '' + mod + ''; // underline mods of other people then the portal owner
}
mod = '' + mod + '';
}
}
modslist.push(mod);
}
return modslist.join(' ');
};
self.getportalowner = function(guid) {
if (guid in window.portals) {
if (self.isMachinaPortal(window.portals[guid])) return self.machinaname;
if (window.portals[guid].options.team == window.TEAM_NONE) return '-';
}
return self.portaldetails[guid]?.owner || window.portals[guid]?.options?.data.owner || 'unknown';
};
self.getportalownerhtml = function(guid) {
if (guid in self.requestlist) return (guid in self.portaldetails ? '(updating)' : '(loading)');
if (guid in window.portals && self.isMachinaPortal(window.portals[guid])) return self.machinaname;
if (!(guid in self.portaldetails)) return (window.portals[guid] && window.portals[guid].options.team != window.TEAM_NONE ? '(unknown)' : '-');
let owner = self.portaldetails[guid].owner || '-'; // use - if unknown
if (owner === window.PLAYER.nickname) owner = `${owner}`;
return owner;
};
self.isMachinaPortal = function(portal) {
if (!portal?.options) return false;
if (portal.options.team == window.TEAM_ENL || portal.options.team == window.TEAM_RES) return false;
if (portal.options.data.resCount > 0) return true;
if (portal.options.data.resCount === 0) return false;
// only if resCount is undefined, check all links for matching origin or destination match
// Be aware: a placeholder portal is allways drawn before the link is drawn, so most of the time there is no link yet to check for a match, unless the portal has multiple links
let portallatlng = portal.getLatLng();
let machinalinkplaceholderfound = false;
for (let id in window.links) {
let link = window.links[id];
if (portal.options.guid == link.options.data.oGuid || portal.options.guid == link.options.data.dGuid) {
machinalinkplaceholderfound = true;
break;
}
}
return machinalinkplaceholderfound;
};
self.loaddetails = function() {
self.requestlist = {};
// parse guid list of visible portal rows
let skipped = {};
let neutral = {};
for (const guid of [...document.querySelectorAll('#portalslist TR[guid]')].map((el)=>{return el.getAttribute('guid');})) {
if (guid in self.portaldetails) {
skipped[guid] = self.requestmaxretries;
} else if (!(guid in window.portals) || guid in window.portals && (self.isMachinaPortal(window.portals[guid]) || window.portals[guid]?.options.team == window.TEAM_ENL || window.portals[guid]?.options.team == window.TEAM_RES)) {
// add only if not already loaded, and not in portals, or in portals and not team_none
self.requestlist[guid] = self.requestmaxretries;
self.updatePortalsListRow(guid);
} else {
neutral[guid] = self.requestmaxretries;
}
}
if (Object.keys(self.requestlist).length == 0 && Object.keys(neutral).length == 0 && Object.keys(skipped).length > 0) {
if (confirm('All details already loaded. Do you want to reload all details again?')) {
neutral = {};
for (let guid in skipped) {
if (!(guid in window.portals) || guid in window.portals && (self.isMachinaPortal(window.portals[guid]) || window.portals[guid]?.options.team == window.TEAM_ENL || window.portals[guid]?.options.team == window.TEAM_RES)) {
// add only if not already loaded, and not in portals, or in portals and not team_none
self.requestlist[guid] = self.requestmaxretries;
self.updatePortalsListRow(guid);
} else {
neutral[guid] = self.requestmaxretries;
}
}
}
}
self.requestlist = {...self.requestlist,...neutral}; // handle all neutral last
for (let guid in neutral) {
self.updatePortalsListRow(guid);
}
self.requestid = undefined;
setTimeout(self.loadnext);
};
self.stoploaddetails = function() {
clearTimeout(self.loadfailtimerid);
self.loadfailtimerid = 0;
// restore portal list
for (const guid in self.requestlist) {
delete(self.requestlist[guid]);
self.updatePortalsListRow(guid);
}
self.requestlist = {};
self.requestid = undefined;
};
self.loadfail = function() {
self.requestlist[self.requestid]--;
if (self.requestlist[self.requestid] > 0) {
console.log(self.title + " - Load portal details failed (retry " + (self.requestmaxretries - self.requestlist[self.requestid]) + "/" + self.requestmaxretries + ")",self.requestid);
// place element at the end of the object, try again later:
let retriesleft = self.requestlist[self.requestid];
delete(self.requestlist[self.requestid]);
self.requestlist[self.requestid] = retriesleft;
self.requestid = undefined;
setTimeout(self.loadnext);
return;
}
console.log(self.title + " - Load portal details failed (after " + self.requestmaxretries + " retries)",self.requestid);
self.stopbuttonarea.style.display = 'none';
self.loadbutton.style.display = 'inline';
self.stoploaddetails();
window.dialog({
html: "Load portal details failed for guid:
\n" + self.requestid + "
\n
\nYou can press 'Load details' to try again.",
title: self.title,
id: "portalslist-loadfail"
});
};
self.loadnext = function() {
if (self.requestid) return; // busy
if (Object.keys(self.requestlist).length == 0) {
self.loaddetailsarea.textContent = '';
self.loadbutton.style.display = 'inline';
self.stopbuttonarea.style.display = 'none';
return;
}
self.loaddetailsarea.textContent = ' (' + Object.keys(self.requestlist).length + ')';
self.requestid = Object.keys(self.requestlist)[0];
if (self.loadfailtimerid) {
clearTimeout(self.loadfailtimerid);
self.loadfailtimerid = 0;
}
self.loadfailtimerid = setTimeout(function() { self.loadfailtimerid = 0; self.loadfail(); },1000);
window.portalDetail.request(self.requestid);
};
self.storedetails = function(data) {
if (!(data instanceof Object) || !('guid' in data) || !('details' in data)) return;
if (data.guid == self.requestid && self.loadfailtimerid) {
clearTimeout(self.loadfailtimerid);
self.loadfailtimerid = 0;
}
self.portaldetails[data.guid] = data.details; // plain storage of all details
delete(self.requestlist[data.guid]); // delete before executing getPortalObj
self.updatePortalsListRow(data.guid);
if (data.guid == self.requestid) { // only load next if this guid was the last request
self.requestid = undefined;
setTimeout(self.loadnext);
}
};
self.updatePortalsListRow = function(guid) {
// update table row
let listindex = window.plugin.portalslist.listPortals.findIndex((el)=>{return el.portal.options.guid == guid});
if (listindex >= 0) {
// update list item
window.plugin.portalslist.listPortals[listindex] = self.getPortalObj(guid);
if (window.plugin.portalslist.listPortals[listindex].row.classList.contains(window.TEAM_TO_CSS[window.TEAM_NONE]) && self.isMachinaPortal(window.portals[guid])) {
window.plugin.portalslist.listPortals[listindex].row.classList.replace(window.TEAM_TO_CSS[window.TEAM_NONE],self.id + '-mac');
}
let tablerow = document.querySelector('#portalslist TR[guid="' + guid + '"]');
if (tablerow) {
// update this row
let row = window.plugin.portalslist.listPortals[listindex].row;
row.cells[0].textContent = tablerow.rowIndex;
tablerow.replaceWith(row);
}
// update header counts
self.updateCountsRow();
// update filterlevel, countlevel
self.updateLevelRow();
}
};
self.initialize = function() {
let neutraldisplayed = window.isLayerGroupDisplayed(self.unclaimedlayername);
let enldisplayed = window.isLayerGroupDisplayed(self.enllayername);
let resdisplayed = window.isLayerGroupDisplayed(self.reslayername);
if (window.plugin.portalslist.portalTable.toString().match('reversed')) {
if (neutraldisplayed && resdisplayed && enldisplayed) {
window.plugin.portalslist.filter = 0;
self.filterreversed = false;
} else if (neutraldisplayed && !resdisplayed && !enldisplayed) {
window.plugin.portalslist.filter = 1;
self.filterreversed = false;
} else if (!neutraldisplayed && resdisplayed && enldisplayed) {
window.plugin.portalslist.filter = 1;
self.filterreversed = true;
} else if (!neutraldisplayed && resdisplayed && !enldisplayed) {
window.plugin.portalslist.filter = 2;
self.filterreversed = false;
} else if (neutraldisplayed && !resdisplayed && enldisplayed) {
window.plugin.portalslist.filter = 2;
self.filterreversed = true;
} else if (!neutraldisplayed && !resdisplayed && enldisplayed) {
window.plugin.portalslist.filter = 3;
self.filterreversed = false;
} else if (neutraldisplayed && resdisplayed && !enldisplayed) {
window.plugin.portalslist.filter = 3;
self.filterreversed = true;
}
} else {
// method before 0.4.0 had negative and positive filter values:
if (neutraldisplayed && resdisplayed && enldisplayed) {
window.plugin.portalslist.filter = 0;
} else if (neutraldisplayed && !resdisplayed && !enldisplayed) {
window.plugin.portalslist.filter = 1;
} else if (!neutraldisplayed && resdisplayed && enldisplayed) {
window.plugin.portalslist.filter = -1;
} else if (!neutraldisplayed && resdisplayed && !enldisplayed) {
window.plugin.portalslist.filter = 2;
} else if (neutraldisplayed && !resdisplayed && enldisplayed) {
window.plugin.portalslist.filter = -2;
} else if (!neutraldisplayed && !resdisplayed && enldisplayed) {
window.plugin.portalslist.filter = 3;
} else if (neutraldisplayed && resdisplayed && !enldisplayed) {
window.plugin.portalslist.filter = -3;
}
}
self.filterlevel = [];
self.countlevel = [];
self.filterlevel[0] = neutraldisplayed;
for (let level = 1; level <= 8; level++) {
self.filterlevel[level] = window.isLayerGroupDisplayed(self.levellayername + level + self.portallayername);
self.countlevel[level] = 0;
}
};
self.setup = function() {
if ('pluginloaded' in self) {
console.log('IITC plugin already loaded: ' + self.title + ' version ' + self.version);
return;
} else {
self.pluginloaded = true;
}
if (!window.plugin.portalslist) {
console.log('IITC plugin ERROR: ' + self.title + ' version ' + self.version + ' - requires plugin portalslist');
return;
}
// get the actual layer names for Unclaimed and Level portals
let overlaylayers = window.layerChooser._layers;
if (!(overlaylayers instanceof Array)) { // IITC 0.26
overlaylayers = Object.keys(window.layerChooser._layers).map((el)=>{return window.layerChooser._layers[el]});
}
self.unclaimedlayername = overlaylayers.filter((el)=>{return el.overlay})[0].name; // the first overlay layer is always for Unclaimed/Placeholder Portals
let layermatches = overlaylayers.filter((el)=>{return el.overlay})[1].name.match(/^(.*)1(.*)$/);
if (layermatches) { // the second overlay layer is always for Level 1 Portals
self.levellayername = layermatches[1];
self.portallayername = layermatches[2];
}
if (window.TEAM_NAMES) self.enllayername = window.TEAM_NAMES[window.TEAM_ENL];
if (window.TEAM_NAMES) self.reslayername = window.TEAM_NAMES[window.TEAM_RES];
let getPortalsString = window.plugin.portalslist.getPortals.toString();
// disable skipping ghost portals
if (getPortalsString == (getPortalsString = getPortalsString.replace(/(if \()(!portal\.options\.data\.title\))/,'$1false && $2'))) { // older version
if (getPortalsString == (getPortalsString = getPortalsString.replace(/(if \()(!\('title' in portal\.options\.data\)\))/,'$1false && $2'))) { // newer versions
// oldest version did not have this filter
// console.log(self.title + ' - ERROR: replace if title failed');
}
}
// add portal guid as an attribute, to be used for replacing data in the table
if (getPortalsString == (getPortalsString = getPortalsString.replace('obj.row = row;','row.setAttribute("guid", portal.options.guid);\n obj.row = row;'))) {
console.log(self.title + ' - ERROR: replace obj.row failed');
}
if (getPortalsString == (getPortalsString = getPortalsString.replace(/(.*switch)/,' ' + self.namespace + 'countlevel[portal.options.level]++;\n$1'))) {
console.log(self.title + ' - ERROR: replace switch failed');
}
getPortalsString = getPortalsString.replaceAll('data.history.','data?.history?.'); // fix Missions 0.4.0 error
try {
eval('window.plugin.portalslist.getPortals = ' + getPortalsString + ';');
} catch(e) {
console.log(self.title + ' - ERROR: eval getPortals failed',e,getPortalsString);
}
let getPortalLinkString = window.plugin.portalslist.getPortalLink.toString();
// fix undefined titles and make the title smaller
if (getPortalLinkString == (getPortalLinkString = getPortalLinkString.replace('link.textContent = portal.options.data.title;','link.textContent = (portal.options.data.title ? portal.options.data.title : "[undefined]");\n if (link.textContent.length > 30) {\n link.title = link.textContent;\n link.textContent = link.textContent.substring(0,27) + \'...\';\n }\n'))) {
console.log(self.title + ' - ERROR: replace link.textContent failed');
}
// show leading spaces in a title
if (getPortalLinkString == (getPortalLinkString = getPortalLinkString.replace('("a");','("a");\n link.style.whiteSpace = "pre";'))) {
console.log(self.title + ' - ERROR: replace a failed');
}
try {
eval('window.plugin.portalslist.getPortalLink = ' + getPortalLinkString + ';');
} catch(e) {
console.log(self.title + ' - ERROR: eval getPortalLink failed',e,getPortalLinkString);
}
// Fix columns value and format functions to support ghost portals:
for (let cnt = 0; cnt < window.plugin.portalslist.fields.length; cnt++) {
if (window.plugin.portalslist.fields[cnt].title == "Portal Name") {
window.plugin.portalslist.fields[cnt].value = function(portal) { return (portal.options.data.title && portal.options.data.title != "" ? portal.options.data.title : "[undefined]"); };
} else if (window.plugin.portalslist.fields[cnt].title == "Level") {
window.plugin.portalslist.fields[cnt].value = function(portal) { return (portal.options.data.level ? portal.options.data.level : -1); };
let formatString = window.plugin.portalslist.fields[cnt].format.toString().replace('{','{\n value = (value == -1?"?":value);');
try {
eval('window.plugin.portalslist.fields[' + cnt + '].format = ' + formatString + ';');
} catch(e) {
console.log(self.title + ' - ERROR: eval portalslist.fields[' + cnt + '].format failed',e,formatString);
}
} else if (window.plugin.portalslist.fields[cnt].title == "Health") {
window.plugin.portalslist.fields[cnt].value = function(portal) { return (portal.options.data.health ? portal.options.data.health : -1); };
let formatString = window.plugin.portalslist.fields[cnt].format.toString().replace('{','{\n value = (value == -1?"?":value);');
try {
eval('window.plugin.portalslist.fields[' + cnt + '].format = ' + formatString + ';');
} catch(e) {
console.log(self.title + ' - ERROR: eval portalslist.fields[' + cnt + '].format failed',e,formatString);
}
} else if (window.plugin.portalslist.fields[cnt].title == "AP") {
window.plugin.portalslist.fields[cnt].sortValue = function(value, portal) { return (Number.isNaN(value.enemyAp) ? -1 : value.enemyAp); }
let formatString = window.plugin.portalslist.fields[cnt].format.toString().replace('{','{\n for (let i in value) {\n value[i] = (Number.isNaN(value[i])?"?":value[i]);\n };');
try {
eval('window.plugin.portalslist.fields[' + cnt + '].format = ' + formatString + ';');
} catch(e) {
console.log(self.title + ' - ERROR: eval portalslist.fields[' + cnt + '].format failed',e,formatString);
}
}
}
// make dialog wider to fit extra columns and extra buttons
let displayPLString = window.plugin.portalslist.displayPL.toString();
displayPLString = displayPLString.replace(/(, false\))/,', ' + self.namespace + 'filterreversed)'); // for version 0.4.0
if (displayPLString == (displayPLString = displayPLString.replace(/(.+getPortals.+)/,' ' + self.namespace + 'initialize();\n\n$1'))) {
console.log(self.title + ' - ERROR: replace getPortals failed');
}
if (displayPLString == (displayPLString = displayPLString.replace(/(Nothing to show!)/,'$1 Refresh'))) {
console.log(self.title + ' - ERROR: replace nothing to show failed');
}
if (displayPLString == (displayPLString = displayPLString.replace('width: 700','width: 960'))) {
console.log(self.title + ' - ERROR: replace width failed');
}
if (!displayPLString.match('macP') && displayPLString == (displayPLString = displayPLString.replace(/(neuP = 0;)/,'$1\n window.plugin.portalslist.macP = 0;'))) {
console.log(self.title + ' - ERROR: add macP failed');
}
if (displayPLString == (displayPLString = displayPLString.replace(/(list =)/,`for (let item of window.plugin.portalslist.listPortals) {
if (item.portal.options.team == window.TEAM_NONE && item.row.classList.contains(window.TEAM_TO_CSS[window.TEAM_NONE]) && ${self.namespace}isMachinaPortal(item.portal)) {
item.row.classList.replace(window.TEAM_TO_CSS[window.TEAM_NONE],'${self.id}-mac');
window.plugin.portalslist.neuP--;
window.plugin.portalslist.macP++;
}
}
$1`))) {
console.log(self.title + ' - ERROR: insert machine class failed');
}
try {
eval('window.plugin.portalslist.displayPL = ' + displayPLString + ';');
} catch(e) {
console.log(self.title + ' - ERROR: eval getPortalLink failed',e,displayPLString);
}
// modification max-width: 1000px
$('