// ==UserScript==
// @author DanielOnDiordna
// @name IITC plugin: Highlight Resonator Owners
// @category Highlighter
// @version 1.0.0.20231019.224200
// @updateURL https://softspot.nl/ingress/plugins/iitc-plugin-highlight-resonator-owners.meta.js
// @downloadURL https://softspot.nl/ingress/plugins/iitc-plugin-highlight-resonator-owners.user.js
// @description [danielondiordna-1.0.0.20231019.224200] Highlight portals with resonators by owner
// @id iitc-plugin-highlight-resonator-owners@danielondiordna
// @namespace https://softspot.nl/ingress/
// @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.highlightresonatorowners = function() {};
var self = window.plugin.highlightresonatorowners;
self.id = 'highlightresonatorowners';
self.title = 'Highlight Resonator Owners';
self.version = '1.0.0.20231019.224200';
self.author = 'DanielOnDiordna';
self.changelog = `
Changelog:
version 1.0.0.20231019.224200
- version upgrade due to a change in the wrapper, added changelog
- reversed the changelog order to show last changes at the top
- reformatted javascript code ES6 backticks
- replaced all var with let declarations
- added machina faction
version 0.0.5.20210724.002500
- prevent double plugin setup on hook iitcLoaded
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.20210328.002900
- disabled interaction on resonator text labels
version 0.0.4.20210123.174900
- integrated Spectrum Colorpicker 1.8.1 plugin code, no need anymore for the separate plugin
- updated plugin wrapper and userscript header formatting to match IITC-CE coding
version 0.0.3.20200908.231600
- added short names option
- added limit to scan portals by level and faction
- added a statusbar with total and loading indicator
- changed the orange highlighter function from all to any resonator by level
- fixed some issues
version 0.0.2.20200906.224900
- color picker for owner name/level (colorpicker add-on or drawtools plugin required)
- highlight portal if missing resonator by level for a selected player
- option to show/hide owner names
- option to show/hide resonator levels
version 0.0.1.20200906.182700
- first version
`;
self.namespace = 'window.plugin.' + self.id + '.';
self.pluginname = 'plugin-' + self.id;
self.localstoragesettings = self.pluginname + '-settings';
self.highlightername = 'Resonator Owners';
self.portallabelslayer = {};
self.portallabelslayergroup = null;
self.settings = {};
self.settings.refreshinterval = 5; // hours
self.settings.resonatorowner = '';
self.settings.shownames = true;
self.settings.shortnames = false;
self.settings.showlevels = true;
self.settings.resonatorownercolor = '#ff00f7';
self.settings.resolevel = [0,false,false,false,false,false,false,false,true]; // 0, level 1-8
self.settings.portallevel = [0,true,true,true,true,true,true,true,true]; // 0, level 1-8
self.settings.scanenl = true;
self.settings.scanres = true;
self.settings.scanmac = true;
self.restartrunning = false;
self.nextrunning = false;
self.portals = {};
self.resonatorowners = {};
self.latlngindex = {};
self.updatelist = {};
self.requestlist = {};
self.requestid = '';
// need to initialise the 'spectrum' color picker
self.colorpickeroptions = {
flat: false,
showInput: true,
showButtons: true,
showPalette: true,
showSelectionPalette: true,
allowEmpty: false,
palette: [
['#004000','#008000','#00C000'],
['#00FF00','#80FF80','#C0FFC0'],
['#000040','#000080','#0000C0'],
['#4040FF','#8080FF','#C0C0FF'],
['#6A3400','#964A00','#C05F00'],
['#E27000','#FF8309','#FFC287'],
['#a24ac3','#514ac3','#4aa8c3','#51c34a'],
['#c1c34a','#c38a4a','#c34a4a','#c34a6f'],
['#000000','#666666','#bbbbbb','#ffffff']
]};
self.restoresettings = function() {
if (typeof localStorage[self.localstoragesettings] === 'string' && localStorage[self.localstoragesettings] !== '') {
try {
var settings = JSON.parse(localStorage[self.localstoragesettings]);
if (typeof settings === 'object' && settings instanceof Object && !(settings instanceof Array)) {
for (const i in settings) {
if (typeof settings[i] === typeof self.settings[i]) self.settings[i] = settings[i];
}
}
} catch(e) {
return false;
}
}
};
self.storesettings = function() {
localStorage[self.localstoragesettings] = JSON.stringify(self.settings);
};
self.highlight_portal = function(portal) {
let guid = portal.options.guid;
var noresonatorsforowner = (self.settings.resonatorowner != '' && self.resonatorowners[self.settings.resonatorowner] !== undefined);
var noresonatorslevelforowner = false;
if (noresonatorsforowner && self.portals[guid]) {
var ownerlevels = {};
for (let cnt = 0; cnt < self.portals[guid].resonators.length; cnt++) {
if (self.portals[guid].resonators[cnt].owner === self.settings.resonatorowner) {
noresonatorsforowner = false;
ownerlevels[self.portals[guid].resonators[cnt].level]++;
}
}
//console.log(self.title,ownerlevels);
for (let level = 1; level < self.settings.portallevel.length; level++) {
if (self.settings.resolevel[level] && ownerlevels[level] == undefined) {
noresonatorslevelforowner = true;
}
}
}
//console.log(self.title + ' highlight_portal',guid,noresonatorsforowner,noresonatorslevelforowner,portal.options.team,portal.options.level);
let params = {opacity: 1.0, fillOpacity: 0.5};
if (portal.options.team === window.TEAM_NONE ||
!self.settings.scanenl && portal.options.team == window.TEAM_ENL ||
!self.settings.scanres && portal.options.team == window.TEAM_RES ||
!self.settings.scanmac && portal.options.team == window.TEAM_MAC ||
!self.settings.portallevel[portal.options.level]) {
params.fillColor = window.COLORS[portal.options.team];
params.fillOpacity = 0.5;
} else if (self.requestlist[guid]) {
params.fillColor = window.COLORS[portal.options.team];
params.fillOpacity = 0.3;
} else if (!self.portals[guid]) {
params.fillColor = window.COLORS[portal.options.team];
params.fillOpacity = 0.1;
} else if (noresonatorsforowner) {
params.fillColor = 'red';
params.fillOpacity = 1;
} else if (noresonatorslevelforowner) {
params.fillColor = 'orange';
params.fillOpacity = 1;
} else {
params.fillColor = window.COLORS[portal.options.team];
params.fillOpacity = 0.5;
}
portal.setStyle(params);
};
self.highlightportals = {
highlight:
function(data) {
if (!data || !(data instanceof Object)) return;
if (data.guid && window.portals[data.guid]) data.portal = window.portals[data.guid];
if (!data || !data.portal || !data.portal.options || !data.portal.options.guid) return;
if (self.highlighteractive()) {
self.highlight_portal(data.portal);
}
},
setSelected:
function(active) {
if (!active) {
//hide
window.map.removeLayer(self.portallabelslayergroup);
self.stop();
} else {
self.updateHighlighter();
window.map.addLayer(self.portallabelslayergroup);
$(`.${self.pluginname}-portallabel`).css('pointer-events','none');
self.restart();
}
}
};
self.highlighteractive = function() {
return (window._current_highlighter === self.highlightername);
};
self.updateHighlighter = function() {
if (!self.highlighteractive()) return;
for (let guid in self.portals) {
if (window.portals[guid]) {
self.highlight_portal(window.portals[guid]);
}
}
};
self.addLabel = function(guid,latLng) {
self.removeLabel(guid);
if (!self.highlighteractive()) return;
if (window.getMapZoomTileParameters(window.getDataZoomForMapZoom(window.map.getZoom())).level !== 0) return;
let portal = window.portals[guid];
if (window.teamStringToId(portal.options.data.team) == window.TEAM_NONE) return; // skip unclaimed
if (!self.settings.scanenl && window.teamStringToId(portal.options.data.team) == window.TEAM_ENL) return;
if (!self.settings.scanres && window.teamStringToId(portal.options.data.team) == window.TEAM_RES) return;
if (!self.settings.scanmac && window.teamStringToId(portal.options.data.team) == window.TEAM_MAC) return;
if (!self.settings.portallevel[portal.options.data.level]) return;
// octant=slot: 0=E, 1=NE, 2=N, 3=NW, 4=W, 5=SW, 6=S, SE=7
// resos in the display should be ordered like this:
// N NE Since the view is displayed in rows, they
// NW E need to be ordered like this: N NE NW E W SE SW S
// W SE i.e. 2 1 3 0 4 7 5 6
// SW S
// note: as of 2014-05-23 update, this is not true for portals with empty slots!
// only if all 8 resonators are deployed, we know which is in which slot
var resonatortext = ['','','','','','','',''];
if (self.settings.showlevels || self.settings.shownames) resonatortext = ['?','?','?','?','?','?','?','?'];
var resonatorstyle = ['','','','','','','',''];
if (self.portals[guid]) {
if (self.portals[guid].resonators.length == 8) {
// fully deployed - we can make assumptions about deployment slots
$.each([2, 1, 3, 0, 4, 7, 5, 6], function(cnt, slot) {
if (self.settings.resonatorowner && self.portals[guid].resonators[slot].owner === self.settings.resonatorowner) resonatorstyle[cnt] = ' color:' + self.settings.resonatorownercolor + ';';
var level = self.portals[guid].resonators[slot].level;
var owner = self.portals[guid].resonators[slot].owner;
if (self.settings.shortnames) owner = owner.substring(0,3);
if ((cnt+1) % 2 == 0) {
resonatortext[cnt] = (self.settings.showlevels?level:'') + ' ' + (self.settings.shownames?owner:''); // even, right side
} else {
resonatortext[cnt] = (self.settings.shownames?owner:'') + ' ' + (self.settings.showlevels?level:''); // odd, left side
}
});
} else {
// partially deployed portal - we can no longer find out which resonator is in which slot
for (let cnt = 0; cnt < 8; cnt++) {
if (cnt < self.portals[guid].resonators.length) {
var level = self.portals[guid].resonators[cnt].level;
var owner = self.portals[guid].resonators[cnt].owner;
if (self.settings.resonatorowner && owner === self.settings.resonatorowner) resonatorstyle[cnt] = ' color:' + self.settings.resonatorownercolor + ';';
if (self.settings.shortnames) owner = owner.substring(0,3);
if ((cnt+1) % 2 == 0) {
resonatortext[cnt] = (self.settings.showlevels?level:'') + ' ' + (self.settings.shownames?owner:''); // even, right side
} else {
resonatortext[cnt] = (self.settings.shownames?owner:'') + ' ' + (self.settings.showlevels?level:''); // odd, left side
}
} else {
resonatortext[cnt] = (self.settings.showlevels || self.settings.shownames?'-':'');
}
}
}
}
let labelwidth = 300;
let labelheight = 40;
let portaltext = `
${resonatortext[0]}
${resonatortext[1]}
${resonatortext[2]}
${resonatortext[3]}
${resonatortext[4]}
${resonatortext[5]}
${resonatortext[6]}
${resonatortext[7]}
`;
self.portallabelslayer[guid] = window.L.marker(latLng, {
icon: window.L.divIcon({
className: `${self.pluginname}-portallabel`,
iconAnchor: [labelwidth/2 + 3,labelheight/2],
iconSize: [labelwidth,labelheight],
html: portaltext
}),
guid: guid
});
self.portallabelslayer[guid].addTo(self.portallabelslayergroup);
$(`.${self.pluginname}-portallabel`).css('pointer-events','none');
};
self.removeLabel = function(guid) {
let existinglayer = self.portallabelslayer[guid];
if (existinglayer) {
self.portallabelslayergroup.removeLayer(existinglayer);
delete self.portallabelslayer[guid];
}
};
self.updatePortalLabel = function(data) {
let portal = window.portals[data.guid];
if (!portal) return;
let latLng = portal.getLatLng();
self.addLabel(data.guid, latLng);
};
self.updatePortalLabels = function() {
$.each(self.portals, function(guid) {
self.updatePortalLabel({guid: guid});
});
};
self.portalonvisiblelayer = function(portal) {
if (portal.options.data.team === 'E' && !window.overlayStatus['Enlightened']) return false;
if (portal.options.data.team === 'R' && !window.overlayStatus['Resistance']) return false;
if (portal.options.data.team === 'M' && !(window.overlayStatus['__MACHINA__'] || window.overlayStatus['U̶͚̓̍N̴̖̈K̠͔̍͑̂͜N̞̥͋̀̉Ȯ̶̹͕̀W̶̢͚͑̚͝Ṉ̨̟̒̅'])) return false;
let unclaimedlayername = 'Unclaimed Portals';
let found = window.setupMap.toString().match(/i === 0 \? \'([^']+)\'/);
if (found) unclaimedlayername = found[1] + ' Portals';
if (portal.options.data.team === 'N' && !window.overlayStatus[unclaimedlayername]) return false;
if (!window.overlayStatus['Level ' + portal.options.data.level + ' Portals']) return false;
return true;
};
self.storedetails = function(data) {
if (!(data instanceof Object)) return;
if (data.success === false) return data.guid; // retry
// check if returned data is from requested portal
delete(self.requestlist[data.guid]);
delete(self.updatelist[data.guid]);
let portal = {};
portal.team = data.details.team;
portal.level = (portal.team === window.TEAM_NONE?0:data.details.level); // set level to 0 if portal is unclaimed
portal.owner = data.details.owner;
portal.title = data.details.title;
portal.resonators = data.details.resonators;
portal.checked = new Date().getTime();
// store unique resonatorowners
let totalresonatorowners = Object.keys(self.resonatorowners).length;
for (let cnt = 0; cnt < portal.resonators.length; cnt++) {
self.resonatorowners[portal.resonators[cnt].owner] = window.teamStringToId(portal.team);
}
if (Object.keys(self.resonatorowners).length != totalresonatorowners) {
self.updateplayerselectlist();
}
self.portals[data.guid] = portal;
let latlngid = data.details.latE6 + ',' + data.details.lngE6;
self.latlngindex[latlngid] = data.guid;
//console.log(self.title + " storedetails 4",data);
// if (!portal || !(portal instanceof Object)) return;
// if (portal.options && portal.options.guid && window.portals[portal.options.guid]) portal = window.portals[portal.options.guid];
// if (!portal || !portal.options || !portal.options.guid) return;
self.highlight_portal(window.portals[data.guid]); //{ portal : { options : window.portals[guid].options } });
//console.log(self.title + " storedetails 5");
self.updatePortalLabel(data);
//console.log(self.title + " storedetails 6");
return data.guid;
};
self.stop = function() {
//console.log(self.title + " STOP");
// clear list
window.clearTimeout(self.timerid);
self.requestlist = {};
self.requestid = '';
self.nextrunning = false;
self.updatestatusbar();
};
self.restart = function() {
if (!self.highlighteractive()) return;
if (window.mapDataRequest.status.short !== 'done' || window.getMapZoomTileParameters(window.getDataZoomForMapZoom(window.map.getZoom())).level !== 0) { // zoom to all portals
self.stop();
return;
}
if (self.restartrunning) return;
self.restartrunning = true;
//console.log(self.title + " RESTART");
// create list of portals to request:
let visiblebounds = window.map.getBounds();
self.requestlist = {};
let currenttime = new Date().getTime();
for (let guid in window.portals) {
let portal = window.portals[guid];
// portal must be within visible bounds
// and portal must on a visible layer
if (visiblebounds.contains(portal.getLatLng()) && self.portalonvisiblelayer(portal)) {
// portal must be new
// or changed faction
// or mentioned in comms (on updatelist)
// or older then refreshinterval (hours)
if (self.portals[guid] === undefined ||
self.portals[guid]['team'] !== portal.options.data.team ||
self.updatelist[guid] ||
(currenttime - self.portals[guid]['checked']) > self.settings.refreshinterval * 60*60*1000) {
if (!self.settings.scanenl && window.teamStringToId(portal.options.data.team) == window.TEAM_ENL) continue;
if (!self.settings.scanres && window.teamStringToId(portal.options.data.team) == window.TEAM_RES) continue;
if (!self.settings.scanmac && window.teamStringToId(portal.options.data.team) == window.TEAM_MAC) continue;
if (!self.settings.portallevel[portal.options.data.level]) continue;
self.requestlist[guid] = portal;
//break; // REMOVE THIS LINE WHEN READY
}
}
}
self.next();
self.restartrunning = false;
};
self.next = function() {
if (!self.highlighteractive() || window.mapDataRequest.status.short !== 'done') {
self.stop();
return;
}
if (self.nextrunning) return;
self.nextrunning = true;
//console.log(self.title + " NEXT");
// request next portal, if any
if (Object.keys(self.requestlist).length === 0) {
self.stop();
self.nextrunning = false;
return;
}
// next key
self.updatestatusbar();
let guid = Object.keys(self.requestlist)[0];
self.requestid = guid;
let portal = window.portals[guid];
if (!portal || !portal.options) {
// skip portal if not in range
delete(self.requestlist[guid]);
self.timerid = window.setTimeout(function() {
self.nextrunning = false;
self.next();
});
return;
}
self.timerid = window.setTimeout(function() {
self.nextrunning = false;
let portal = self.requestlist[self.requestid];
if (!portal) {
self.next();
return;
}
window.portalDetail.request(self.requestid);
});
};
self.publicchat = function(data) {
if (!self.highlighteractive()) return;
//console.log(self.title + " PUBLICCHAT");
let forceupdate = false;
$.each(data.result, function(ind, json) {
let plrname,latE6,lngE6,title,skipmessage = false;
let portalstatuschanged = false;
$.each(json[2].plext.markup, function(ind, markup) {
switch(markup[0]) {
case 'TEXT':
// only register certain messages
if (markup[1].plain.indexOf(' captured ') >= 0 || markup[1].plain.indexOf(' destroyed ') >= 0 || markup[1].plain.indexOf(' deployed ') >= 0) {
portalstatuschanged = true;
} else {
skipmessage = true;
return;
}
break;
case 'PLAYER':
plrname = markup[1].plain;
break;
case 'PORTAL':
latE6 = markup[1].latE6;
lngE6 = markup[1].lngE6;
title = title ? title : markup[1].name;
//address = address ? address : markup[1].address;
break;
}
});
if (!latE6 || !lngE6 || skipmessage) return;
let latlngid = latE6+','+lngE6;
let time = json[1];
let guid = self.latlngindex[latlngid];
if (self.portals[guid]) { // portal is stored, check if details need to be updated
let checked = self.portals[guid]['checked'];
if (portalstatuschanged && time > checked) {
self.updatelist[guid] = time;
forceupdate = true;
}
}
});
if (forceupdate) window.setTimeout(self.restart);
};
self.onPortalSelected = function() {
//console.log(self.title + " onPortalSelected");
};
self.showPortalSelected = function() {
//console.log(self.title + " showPortalSelected");
};
self.portalAdded = function(data) {
data.portal.on('add', function() {
self.addLabel(this.options.guid,this.getLatLng());
});
data.portal.on('remove', function() {
self.removeLabel(this.options.guid);
});
};
self.zoomend = function() {
if (!self.highlighteractive) return;
if (window.getMapZoomTileParameters(window.getDataZoomForMapZoom(window.map.getZoom())).level !== 0) { // zoom to all portals
map.removeLayer(self.portallabelslayergroup);
} else {
map.addLayer(self.portallabelslayergroup);
$('.' + self.pluginname + '-portallabel').css('pointer-events','none');
}
};
self.updatestatusbar = function() {
$('#' + self.id + '_statusbar').replaceWith(self.statusbarhtml());
};
self.statusbarhtml = function() {
let portalstotal = Object.keys(self.portals).length;
let requestlisttotal = Object.keys(self.requestlist).length;
return 'Portals loaded: ' + portalstotal + (requestlisttotal > 0?' Loading: ' + requestlisttotal:'') + '';
};
self.updateplayerselectlist = function() {
let newlist = self.playerselectlist($('#' + self.id + '_selectplayer option:selected').val());
if (newlist !== $('#' + self.id + '_selectplayer').html()) $('#' + self.id + '_selectplayer').replaceWith(newlist);
};
self.playerselectlist = function(selectedname) {
//console.log(self.title + ' updateplayerselectlist ' + selectedname);
let players = Object.keys(self.resonatorowners).sort(function(a,b) { return (a.toLowerCase() < b.toLowerCase()?-1:(a.toLowerCase() > b.toLowerCase()?1:0)); });
if (!selectedname) selectedname = self.settings.resonatorowner;
let player_team = window.teamStringToId(window.PLAYER.team);
let selectednamefound = false;
let list = [];
list.push('');
for (let index in players) {
let plrname = players[index];
// style="color: ' + self.getPlayerColor(plrname) + '"
list.push('');
if (plrname === selectedname) selectednamefound = true;
}
return '';
};
self.menu = function() {
let lvl = 0;
let reso_lvlcheckboxes = '';
let portal_lvlcheckboxes = '';
let reso_countselectors = '';
for (lvl = 1; lvl <= 8; lvl++) {
portal_lvlcheckboxes += '';
reso_lvlcheckboxes += '';
reso_countselectors += '';
}
let container = document.createElement('div');
container.innerHTML = `
Auto load resonator details
Limit to these portal levels:
${portal_lvlcheckboxes}
Limit to portals of these factions:
${self.statusbarhtml()}
For this selected player:
${self.playerselectlist(self.settings.resonatorowner)}
Text color
Require resonators of this level:
${reso_lvlcheckboxes}
Require this number of resonators:
${reso_countselectors}
Highlight RED: 0 resonators
Highlight ORANGE: missing required resonators