// ==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 $('