ingress-intel-total-conversion/total-conversion-build.user.js
2013-02-11 18:20:01 +01:00

2724 lines
91 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ==UserScript==
// @id ingress-intel-total-conversion@breunigs
// @name intel map total conversion
// @version 0.5-2013-02-11-174634
// @namespace https://github.com/breunigs/ingress-intel-total-conversion
// @updateURL https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/dist/total-conversion-build.user.js
// @downloadURL https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/dist/total-conversion-build.user.js
// @description total conversion for the ingress intel map.
// @include http://www.ingress.com/intel*
// @match http://www.ingress.com/intel*
// ==/UserScript==
// REPLACE ORIG SITE ///////////////////////////////////////////////////
if(document.getElementsByTagName('html')[0].getAttribute('itemscope') != null)
throw('Ingress Intel Website is down, not a userscript issue.');
// disable vanilla JS
window.onload = function() {};
// rescue user data from original page
var scr = document.getElementsByTagName('script');
for(var x in scr) {
var s = scr[x];
if(s.src) continue;
if(s.type !== 'text/javascript') continue;
var d = s.innerHTML.split('\n');
break;
}
if(!d) {
// page doesnt have a script tag with player information.
if(document.getElementById('header_email')) {
// however, we are logged in.
setTimeout('location.reload();', 10*1000);
throw('Page doesnt have player data, but you are logged in. Reloading in 10s.');
}
// FIXME: handle nia takedown in progress
throw('Couldnt retrieve player data. Are you logged in?');
}
for(var i = 0; i < d.length; i++) {
if(!d[i].match('var PLAYER = ')) continue;
eval(d[i].match(/^var /, 'window.'));
break;
}
// player information is now available in a hash like this:
// window.PLAYER = {"ap": "123", "energy": 123, "available_invites": 123, "nickname": "somenick", "team": "ALIENS||RESISTANCE"};
// remove complete page. We only wanted the user-data and the pages
// security context so we can access the API easily. Setup as much as
// possible without requiring scripts.
document.getElementsByTagName('head')[0].innerHTML = ''
//~ + '<link rel="stylesheet" type="text/css" href="http://0.0.0.0:8000/style.css"/>'
+ '<title>Ingress Intel Map</title>'
+ '<link rel="stylesheet" type="text/css" href="http://breunigs.github.com/ingress-intel-total-conversion/dist/style.css?2013-02-11-174634"/>'
+ '<link rel="stylesheet" type="text/css" href="http://cdn.leafletjs.com/leaflet-0.5/leaflet.css"/>'
+ '<link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Coda"/>';
document.getElementsByTagName('body')[0].innerHTML = ''
+ '<div id="map">Loading, please wait</div>'
+ '<div id="chatcontrols" style="display:none">'
+ ' <a><span class="toggle expand"></span></a><a>automated</a><a>public</a><a class="active">faction</a>'
+ '</div>'
+ '<div id="chat" style="display:none">'
+ ' <div id="chatfaction"></div>'
+ ' <div id="chatpublic"></div>'
+ ' <div id="chatautomated"></div>'
+ '</div>'
+ '<form id="chatinput" style="display:none"><time></time><span>tell faction:</span><input type="text"/></form>'
+ '<a id="sidebartoggle"><span class="toggle close"></span></a>'
+ '<div id="scrollwrapper">' // enable scrolling for small screens
+ ' <div id="sidebar" style="display: none">'
+ ' <div id="playerstat">t</div>'
+ ' <div id="gamestat">&nbsp;loading global control stats</div>'
+ ' <input id="geosearch" placeholder="Search location…" type="text"/>'
+ ' <div id="portaldetails"></div>'
+ ' <input id="redeem" placeholder="Redeem code…" type="text"/>'
+ ' <div id="toolbox"><a onmouseover="setPermaLink(this)">permalink</a></div>'
+ ' <div id="spacer"></div>'
+ ' </div>'
+ '</div>'
+ '<div id="updatestatus"></div>';
// putting everything in a wrapper function that in turn is placed in a
// script tag on the website allows us to execute in the sites context
// instead of in the Greasemonkey/Extension/etc. context.
function wrapper() {
// LEAFLET PREFER CANVAS ///////////////////////////////////////////////
// Set to true if Leaflet should draw things using Canvas instead of SVG
// Disabled for now because it has several bugs: flickering, constant
// CPU usage and it continuously fires the moveend event.
L_PREFER_CANVAS = false;
// CONFIG OPTIONS ////////////////////////////////////////////////////
var REFRESH = 30; // refresh view every 30s (base time)
var ZOOM_LEVEL_ADJ = 5; // add 5 seconds per zoom level
var REFRESH_GAME_SCORE = 5*60; // refresh game score every 5 minutes
var MAX_IDLE_TIME = 4; // stop updating map after 4min idling
var PRECACHE_PLAYER_NAMES_ZOOM = 17; // zoom level to start pre-resolving player names
var HIDDEN_SCROLLBAR_ASSUMED_WIDTH = 20;
var SIDEBAR_WIDTH = 300;
// chat messages are requested for the visible viewport. On high zoom
// levels this gets pretty pointless, so request messages in at least a
// X km radius.
var CHAT_MIN_RANGE = 6;
// this controls how far data is being drawn outside the viewport. Set
// it 0 to only draw entities that intersect the current view. A value
// of one will render an area twice the size of the viewport (or some-
// thing like that, Leaflet doc isnt too specific). Setting it too low
// makes the missing data on move/zoom out more obvious. Setting it too
// high causes too many items to be drawn, making drag&drop sluggish.
var VIEWPORT_PAD_RATIO = 0.3;
// how many items to request each query
var CHAT_PUBLIC_ITEMS = 200
var CHAT_FACTION_ITEMS = 50
// Leaflet will get very slow for MANY items. Its better to display
// only some instead of crashing the browser.
var MAX_DRAWN_PORTALS = 1000;
var MAX_DRAWN_LINKS = 400;
var MAX_DRAWN_FIELDS = 200;
var COLOR_SELECTED_PORTAL = '#f00';
var COLORS = ['#FFCE00', '#0088FF', '#03FE03']; // none, res, enl
var COLORS_LVL = ['#000', '#FECE5A', '#FFA630', '#FF7315', '#E40000', '#FD2992', '#EB26CD', '#C124E0', '#9627F4'];
var COLORS_MOD = {VERY_RARE: '#F78AF6', RARE: '#AD8AFF', COMMON: '#84FBBD'};
// circles around a selected portal that show from where you can hack
// it and how far the portal reaches (i.e. how far links may be made
// from this portal)
var ACCESS_INDICATOR_COLOR = 'orange';
var RANGE_INDICATOR_COLOR = 'red';
// INGRESS CONSTANTS /////////////////////////////////////////////////
// http://decodeingress.me/2012/11/18/ingress-portal-levels-and-link-range/
var RESO_NRG = [0, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 6000];
var MAX_XM_PER_LEVEL = [0, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000];
var MIN_AP_FOR_LEVEL = [0, 10000, 30000, 70000, 150000, 300000, 600000, 1200000];
var HACK_RANGE = 40; // in meters, max. distance from portal to be able to access it
var OCTANTS = ['E', 'NE', 'N', 'NW', 'W', 'SW', 'S', 'SE'];
var DEFAULT_PORTAL_IMG = 'http://commondatastorage.googleapis.com/ingress/img/default-portal-image.png';
var DESTROY_RESONATOR = 75; //AP for destroying portal
var DESTROY_LINK = 187; //AP for destroying link
var DESTROY_FIELD = 750; //AP for destroying field
// OTHER MORE-OR-LESS CONSTANTS //////////////////////////////////////
var NOMINATIM = 'http://nominatim.openstreetmap.org/search?format=json&limit=1&q=';
var DEG2RAD = Math.PI / 180;
var TEAM_NONE = 0, TEAM_RES = 1, TEAM_ENL = 2;
var TEAM_TO_CSS = ['none', 'res', 'enl'];
var TYPE_UNKNOWN = 0, TYPE_PORTAL = 1, TYPE_LINK = 2, TYPE_FIELD = 3, TYPE_PLAYER = 4, TYPE_CHAT = 5, TYPE_RESONATOR = 6;
// make PLAYER variable available in site context
var PLAYER = window.PLAYER;
var CHAT_SHRINKED = 60;
// Minimum zoom level resonator will display
var RESONATOR_DISPLAY_ZOOM_LEVEL = 17;
// Constants for resonator positioning
var SLOT_TO_LAT = [0, Math.sqrt(2)/2, 1, Math.sqrt(2)/2, 0, -Math.sqrt(2)/2, -1, -Math.sqrt(2)/2];
var SLOT_TO_LNG = [1, Math.sqrt(2)/2, 0, -Math.sqrt(2)/2, -1, -Math.sqrt(2)/2, 0, Math.sqrt(2)/2];
var EARTH_RADIUS=6378137;
// STORAGE ///////////////////////////////////////////////////////////
// global variables used for storage. Most likely READ ONLY. Proper
// way would be to encapsulate them in an anonymous function and write
// getters/setters, but if you are careful enough, this works.
var refreshTimeout;
var urlPortal = null;
window.playersToResolve = [];
window.playersInResolving = [];
window.selectedPortal = null;
window.portalRangeIndicator = null;
window.portalAccessIndicator = null;
window.mapRunsUserAction = false;
var portalsLayers, linksLayer, fieldsLayer;
// contain references to all entities shown on the map. These are
// automatically kept in sync with the items on *sLayer, so never ever
// write to them.
window.portals = {};
window.links = {};
window.fields = {};
window.resonators = {};
// plugin framework. Plugins may load earlier than iitc, so dont
// overwrite data
if(typeof window.plugin !== 'function') window.plugin = function() {};
// PLUGIN HOOKS ////////////////////////////////////////////////////////
// Plugins may listen to any number of events by specifying the name of
// the event to listen to and handing a function that should be exe-
// cuted when an event occurs. Callbacks will receive additional data
// the event created as their first parameter. The value is always a
// hash that contains more details.
//
// For example, this line will listen for portals to be added and print
// the data generated by the event to the console:
// window.addHook('portalAdded', function(data) { console.log(data) });
//
// Boot hook: booting is handled differently because IITC may not yet
// be available. Have a look at the plugins in plugins/. All
// code before “// PLUGIN START” and after “// PLUGIN END” os
// required to successfully boot the plugin.
//
// Heres more specific information about each event:
// portalAdded: called when a portal has been received and is about to
// be added to its layer group. Note that this does NOT
// mean it is already visible or will be, shortly after.
// If a portal is added to a hidden layer it may never be
// shown at all. Injection point is in
// code/map_data.js#renderPortal near the end. Will hand
// the Leaflet CircleMarker for the portal in "portal" var.
window._hooks = {}
window.VALID_HOOKS = ['portalAdded'];
window.runHooks = function(event, data) {
if(VALID_HOOKS.indexOf(event) === -1) throw('Unknown event type: ' + event);
if(!_hooks[event]) return;
$.each(_hooks[event], function(ind, callback) {
callback(data);
});
}
window.addHook = function(event, callback) {
if(VALID_HOOKS.indexOf(event) === -1) throw('Unknown event type: ' + event);
if(typeof callback !== 'function') throw('Callback must be a function.');
if(!_hooks[event])
_hooks[event] = [callback];
else
_hooks[event].push(callback);
}
// MAP DATA //////////////////////////////////////////////////////////
// these functions handle how and which entities are displayed on the
// map. They also keep them up to date, unless interrupted by user
// action.
// requests map data for current viewport. For details on how this
// works, refer to the description in “MAP DATA REQUEST CALCULATORS”
window.requestData = function() {
console.log('refreshing data');
requests.abort();
cleanUp();
var magic = convertCenterLat(map.getCenter().lat);
var R = calculateR(magic);
var bounds = map.getBounds();
// convert to point values
topRight = convertLatLngToPoint(bounds.getNorthEast(), magic, R);
bottomLeft = convertLatLngToPoint(bounds.getSouthWest() , magic, R);
// how many quadrants intersect the current view?
quadsX = Math.abs(bottomLeft.x - topRight.x);
quadsY = Math.abs(bottomLeft.y - topRight.y);
// will group requests by second-last quad-key quadrant
tiles = {};
// walk in x-direction, starts right goes left
for(var i = 0; i <= quadsX; i++) {
var x = Math.abs(topRight.x - i);
var qk = pointToQuadKey(x, topRight.y);
var bnds = convertPointToLatLng(x, topRight.y, magic, R);
if(!tiles[qk.slice(0, -1)]) tiles[qk.slice(0, -1)] = [];
tiles[qk.slice(0, -1)].push(generateBoundsParams(qk, bnds));
// walk in y-direction, starts top, goes down
for(var j = 1; j <= quadsY; j++) {
var qk = pointToQuadKey(x, topRight.y + j);
var bnds = convertPointToLatLng(x, topRight.y + j, magic, R);
if(!tiles[qk.slice(0, -1)]) tiles[qk.slice(0, -1)] = [];
tiles[qk.slice(0, -1)].push(generateBoundsParams(qk, bnds));
}
}
// finally send ajax requests
$.each(tiles, function(ind, tls) {
data = { minLevelOfDetail: -1 };
data.boundsParamsList = tls;
window.requests.add(window.postAjax('getThinnedEntitiesV2', data, window.handleDataResponse));
});
}
// works on map data response and ensures entities are drawn/updated.
window.handleDataResponse = function(data, textStatus, jqXHR) {
// remove from active ajax queries list
if(!data || !data.result) {
window.failedRequestCount++;
console.warn(data);
return;
}
var portalUpdateAvailable = false;
var portalInUrlAvailable = false;
var m = data.result.map;
// defer rendering of portals because there is no z-index in SVG.
// this means that whats rendered last ends up on top. While the
// portals can be brought to front, this costs extra time. They need
// to be in the foreground, or they cannot be clicked. See
// https://github.com/Leaflet/Leaflet/issues/185
var ppp = [];
var p2f = {};
$.each(m, function(qk, val) {
$.each(val.deletedGameEntityGuids, function(ind, guid) {
if(getTypeByGuid(guid) === TYPE_FIELD && window.fields[guid] !== undefined) {
$.each(window.fields[guid].options.vertices, function(ind, vertex) {
if(window.portals[vertex.guid] === undefined) return true;
fieldArray = window.portals[vertex.guid].options.portalV2.linkedFields;
fieldArray.splice($.inArray(guid, fieldArray), 1);
});
}
window.removeByGuid(guid);
});
$.each(val.gameEntities, function(ind, ent) {
// ent = [GUID, id(?), details]
// format for links: { controllingTeam, creator, edge }
// format for portals: { controllingTeam, turret }
if(ent[2].turret !== undefined) {
if(selectedPortal == ent[0]) portalUpdateAvailable = true;
if(urlPortal && ent[0] == urlPortal) portalInUrlAvailable = true;
var latlng = [ent[2].locationE6.latE6/1E6, ent[2].locationE6.lngE6/1E6];
if(!window.getPaddedBounds().contains(latlng)
&& selectedPortal != ent[0]
&& urlPortal != ent[0]
) return;
ppp.push(ent); // delay portal render
} else if(ent[2].edge !== undefined) {
renderLink(ent);
} else if(ent[2].capturedRegion !== undefined) {
$.each(ent[2].capturedRegion, function(ind, vertex) {
if(p2f[vertex.guid] === undefined)
p2f[vertex.guid] = new Array();
p2f[vertex.guid].push(ent[0]);
});
renderField(ent);
} else {
throw('Unknown entity: ' + JSON.stringify(ent));
}
});
});
$.each(ppp, function(ind, portal) {
if(portal[2].portalV2['linkedFields'] === undefined) {
portal[2].portalV2['linkedFields'] = [];
}
if(p2f[portal[0]] !== undefined) {
$.merge(p2f[portal[0]], portal[2].portalV2['linkedFields']);
portal[2].portalV2['linkedFields'] = uniqueArray(p2f[portal[0]]);
}
});
$.each(ppp, function(ind, portal) { renderPortal(portal); });
if(portals[selectedPortal]) {
try {
portals[selectedPortal].bringToFront();
} catch(e) { /* portal is now visible, catch Leaflet error */ }
}
if(portalInUrlAvailable) {
renderPortalDetails(urlPortal);
urlPortal = null; // select it only once
}
if(portalUpdateAvailable) renderPortalDetails(selectedPortal);
resolvePlayerNames();
}
// removes entities that are still handled by Leaflet, although they
// do not intersect the current viewport.
window.cleanUp = function() {
var cnt = [0,0,0];
var b = getPaddedBounds();
var minlvl = getMinPortalLevel();
for(var i = 0; i < portalsLayers.length; i++) {
// i is also the portal level
portalsLayers[i].eachLayer(function(item) {
var itemGuid = item.options.guid;
// check if 'item' is a portal
if(getTypeByGuid(itemGuid) != TYPE_PORTAL) return true;
// portal must be in bounds and have a high enough level. Also dont
// remove if it is selected.
if(itemGuid == window.selectedPortal ||
(b.contains(item.getLatLng()) && i >= minlvl)) return true;
cnt[0]++;
portalsLayers[i].removeLayer(item);
});
}
linksLayer.eachLayer(function(link) {
if(b.intersects(link.getBounds())) return;
cnt[1]++;
linksLayer.removeLayer(link);
});
fieldsLayer.eachLayer(function(field) {
if(b.intersects(field.getBounds())) return;
cnt[2]++;
fieldsLayer.removeLayer(field);
});
console.log('removed out-of-bounds: '+cnt[0]+' portals, '+cnt[1]+' links, '+cnt[2]+' fields');
}
// removes given entity from map
window.removeByGuid = function(guid) {
switch(getTypeByGuid(guid)) {
case TYPE_PORTAL:
if(!window.portals[guid]) return;
var p = window.portals[guid];
for(var i = 0; i < portalsLayers.length; i++)
portalsLayers[i].removeLayer(p);
break;
case TYPE_LINK:
if(!window.links[guid]) return;
linksLayer.removeLayer(window.links[guid]);
break;
case TYPE_FIELD:
if(!window.fields[guid]) return;
fieldsLayer.removeLayer(window.fields[guid]);
break;
case TYPE_RESONATOR:
if(!window.resonators[guid]) return;
var r = window.resonators[guid];
for(var i = 1; i < portalsLayers.length; i++)
portalsLayers[i].removeLayer(r);
break;
default:
console.warn('unknown GUID type: ' + guid);
//window.debug.printStackTrace();
}
}
// renders a portal on the map from the given entity
window.renderPortal = function(ent) {
if(Object.keys(portals).length >= MAX_DRAWN_PORTALS && ent[0] != selectedPortal)
return removeByGuid(ent[0]);
// hide low level portals on low zooms
var portalLevel = getPortalLevel(ent[2]);
if(portalLevel < getMinPortalLevel() && ent[0] != selectedPortal)
return removeByGuid(ent[0]);
var team = getTeam(ent[2]);
// do nothing if portal did not change
var layerGroup = portalsLayers[parseInt(portalLevel)];
var old = findEntityInLeaflet(layerGroup, window.portals, ent[0]);
if(old) {
var oo = old.options;
var u = oo.team !== team;
u = u || oo.level !== portalLevel;
// nothing for the portal changed, so dont update. Let resonators
// manage themselves if they want to be updated.
if(!u) return renderResonators(ent);
removeByGuid(ent[0]);
}
// there were changes, remove old portal
removeByGuid(ent[0]);
var latlng = [ent[2].locationE6.latE6/1E6, ent[2].locationE6.lngE6/1E6];
// pre-loads player names for high zoom levels
loadPlayerNamesForPortal(ent[2]);
var lvWeight = Math.max(2, portalLevel / 1.5);
var lvRadius = Math.max(portalLevel + 3, 5);
var p = L.circleMarker(latlng, {
radius: lvRadius,
color: ent[0] == selectedPortal ? COLOR_SELECTED_PORTAL : COLORS[team],
opacity: 1,
weight: lvWeight,
fillColor: COLORS[team],
fillOpacity: 0.5,
clickable: true,
level: portalLevel,
team: team,
details: ent[2],
guid: ent[0]});
p.on('remove', function() {
var portalGuid = this.options.guid
// remove attached resonators, skip if
// all resonators have already removed by zooming
if(isResonatorsShow()) {
for(var i = 0; i <= 7; i++)
removeByGuid(portalResonatorGuid(portalGuid,i));
}
delete window.portals[portalGuid];
if(window.selectedPortal === portalGuid) {
window.unselectOldPortal();
window.map.removeLayer(window.portalAccessIndicator);
window.portalAccessIndicator = null;
}
});
p.on('add', function() {
// enable for debugging
if(window.portals[this.options.guid]) throw('duplicate portal detected');
window.portals[this.options.guid] = this;
// handles the case where a selected portal gets removed from the
// map by hiding all portals with said level
if(window.selectedPortal != this.options.guid)
window.portalResetColor(this);
});
p.on('click', function() { window.renderPortalDetails(ent[0]); });
p.on('dblclick', function() {
window.renderPortalDetails(ent[0]);
window.map.setView(latlng, 17);
});
window.renderResonators(ent);
window.runHooks('portalAdded', {portal: p});
// portalLevel contains a float, need to round down
p.addTo(layerGroup);
}
window.renderResonators = function(ent) {
var portalLevel = getPortalLevel(ent[2]);
if(portalLevel < getMinPortalLevel() && ent[0] != selectedPortal) return;
if(!isResonatorsShow()) return;
for(var i=0; i < ent[2].resonatorArray.resonators.length; i++) {
var rdata = ent[2].resonatorArray.resonators[i];
if(rdata == null) continue;
if(window.resonators[portalResonatorGuid(ent[0],i)]) continue;
// offset in meters
var dn = rdata.distanceToPortal*SLOT_TO_LAT[rdata.slot];
var de = rdata.distanceToPortal*SLOT_TO_LNG[rdata.slot];
// Coordinate offset in radians
var dLat = dn/EARTH_RADIUS;
var dLon = de/(EARTH_RADIUS*Math.cos(Math.PI/180*(ent[2].locationE6.latE6/1E6)));
// OffsetPosition, decimal degrees
var lat0 = ent[2].locationE6.latE6/1E6 + dLat * 180/Math.PI;
var lon0 = ent[2].locationE6.lngE6/1E6 + dLon * 180/Math.PI;
var Rlatlng = [lat0, lon0];
var r = L.circleMarker(Rlatlng, {
radius: 3,
// #AAAAAA outline seems easier to see the fill opacity
color: '#AAAAAA',
opacity: 1,
weight: 1,
fillColor: COLORS_LVL[rdata.level],
fillOpacity: rdata.energyTotal/RESO_NRG[rdata.level],
clickable: false,
level: rdata.level,
details: rdata,
pDetails: ent[2],
guid: portalResonatorGuid(ent[0],i) });
r.on('remove', function() { delete window.resonators[this.options.guid]; });
r.on('add', function() { window.resonators[this.options.guid] = this; });
r.addTo(portalsLayers[parseInt(portalLevel)]);
}
}
// append portal guid with -resonator-[slot] to get guid for resonators
window.portalResonatorGuid = function(portalGuid, slot) {
return portalGuid + '-resonator-' + slot;
}
window.isResonatorsShow = function() {
return map.getZoom() >= RESONATOR_DISPLAY_ZOOM_LEVEL;
}
window.portalResetColor = function(portal) {
portal.setStyle({color: portal.options.fillColor});
}
// renders a link on the map from the given entity
window.renderLink = function(ent) {
if(Object.keys(links).length >= MAX_DRAWN_LINKS)
return removeByGuid(ent[0]);
// assume that links never change. If they do, they will have a
// different ID.
if(findEntityInLeaflet(linksLayer, links, ent[0])) return;
var team = getTeam(ent[2]);
var edge = ent[2].edge;
var latlngs = [
[edge.originPortalLocation.latE6/1E6, edge.originPortalLocation.lngE6/1E6],
[edge.destinationPortalLocation.latE6/1E6, edge.destinationPortalLocation.lngE6/1E6]
];
var poly = L.polyline(latlngs, {
color: COLORS[team],
opacity: 1,
weight:2,
clickable: false,
guid: ent[0],
smoothFactor: 10
});
if(!getPaddedBounds().intersects(poly.getBounds())) return;
poly.on('remove', function() { delete window.links[this.options.guid]; });
poly.on('add', function() {
// enable for debugging
if(window.links[this.options.guid]) throw('duplicate link detected');
window.links[this.options.guid] = this;
this.bringToBack();
});
poly.addTo(linksLayer);
}
// renders a field on the map from a given entity
window.renderField = function(ent) {
if(Object.keys(fields).length >= MAX_DRAWN_FIELDS)
return window.removeByGuid(ent[0]);
// assume that fields never change. If they do, they will have a
// different ID.
if(findEntityInLeaflet(fieldsLayer, fields, ent[0])) return;
var team = getTeam(ent[2]);
var reg = ent[2].capturedRegion;
var latlngs = [
[reg.vertexA.location.latE6/1E6, reg.vertexA.location.lngE6/1E6],
[reg.vertexB.location.latE6/1E6, reg.vertexB.location.lngE6/1E6],
[reg.vertexC.location.latE6/1E6, reg.vertexC.location.lngE6/1E6]
];
var poly = L.polygon(latlngs, {
fillColor: COLORS[team],
fillOpacity: 0.25,
stroke: false,
clickable: false,
smoothFactor: 10,
vertices: ent[2].capturedRegion,
lastUpdate: ent[1],
guid: ent[0]});
if(!getPaddedBounds().intersects(poly.getBounds())) return;
poly.on('remove', function() { delete window.fields[this.options.guid]; });
poly.on('add', function() {
// enable for debugging
if(window.fields[this.options.guid]) console.warn('duplicate field detected');
window.fields[this.options.guid] = this;
this.bringToBack();
});
poly.addTo(fieldsLayer);
}
// looks for the GUID in either the layerGroup or entityHash, depending
// on which is faster. Will either return the Leaflet entity or null, if
// it does not exist.
// For example, to find a field use the function like this:
// field = findEntityInLeaflet(fieldsLayer, fields, 'asdasdasd');
window.findEntityInLeaflet = function(layerGroup, entityHash, guid) {
// fast way
if(map.hasLayer(layerGroup)) return entityHash[guid] || null;
// slow way in case the layer is currently hidden
var ent = null;
layerGroup.eachLayer(function(entity) {
if(entity.options.guid !== guid) return true;
ent = entity;
return false;
});
return ent;
}
// REQUEST HANDLING //////////////////////////////////////////////////
// note: only meant for portal/links/fields request, everything else
// does not count towards “loading”
window.activeRequests = [];
window.failedRequestCount = 0;
window.requests = function() {}
window.requests.add = function(ajax) {
window.activeRequests.push(ajax);
renderUpdateStatus();
}
window.requests.remove = function(ajax) {
window.activeRequests.splice(window.activeRequests.indexOf(ajax), 1);
renderUpdateStatus();
}
window.requests.abort = function() {
$.each(window.activeRequests, function(ind, actReq) {
if(actReq) actReq.abort();
});
window.activeRequests = [];
window.failedRequestCount = 0;
window.chat._requestOldPublicRunning = false;
window.chat._requestNewPublicRunning = false;
window.chat._requestOldFactionRunning = false;
window.chat._requestNewFactionRunning = false;
renderUpdateStatus();
}
// gives user feedback about pending operations. Draws current status
// to website. Updates info in layer chooser.
window.renderUpdateStatus = function() {
var t = '<b>map status:</b> ';
if(mapRunsUserAction)
t += 'paused during interaction';
else if(isIdle())
t += '<span style="color:red">Idle, not updating.</span>';
else if(window.activeRequests.length > 0)
t += window.activeRequests.length + ' requests running.';
else
t += 'Up to date.';
if(renderLimitReached())
t += ' <span style="color:red" class="help" title="Can only render so much before it gets unbearably slow. Not all entities are shown. Zoom in or increase the limit (search for MAX_DRAWN_*).">RENDER LIMIT</span> '
if(window.failedRequestCount > 0)
t += ' <span style="color:red">' + window.failedRequestCount + ' failed</span>.'
t += '<br/>(';
var minlvl = getMinPortalLevel();
if(minlvl === 0)
t += 'loading all portals';
else
t+= 'only loading portals with level '+minlvl+' and up';
t += ')';
var portalSelection = $('.leaflet-control-layers-overlays label');
portalSelection.slice(0, minlvl+1).addClass('disabled').attr('title', 'Zoom in to show those.');
portalSelection.slice(minlvl, 8).removeClass('disabled').attr('title', '');
$('#updatestatus').html(t);
}
// sets the timer for the next auto refresh. Ensures only one timeout
// is queued. May be given 'override' in milliseconds if time should
// not be guessed automatically. Especially useful if a little delay
// is required, for example when zooming.
window.startRefreshTimeout = function(override) {
// may be required to remove 'paused during interaction' message in
// status bar
window.renderUpdateStatus();
if(refreshTimeout) clearTimeout(refreshTimeout);
var t = 0;
if(override) {
t = override;
} else {
t = REFRESH*1000;
var adj = ZOOM_LEVEL_ADJ * (18 - window.map.getZoom());
if(adj > 0) t += adj*1000;
}
var next = new Date(new Date().getTime() + t).toLocaleTimeString();
console.log('planned refresh: ' + next);
refreshTimeout = setTimeout(window.requests._callOnRefreshFunctions, t);
}
window.requests._onRefreshFunctions = [];
window.requests._callOnRefreshFunctions = function() {
startRefreshTimeout();
if(isIdle()) {
console.log('user has been idle for ' + idleTime + ' minutes. Skipping refresh.');
renderUpdateStatus();
return;
}
console.log('refreshing');
$.each(window.requests._onRefreshFunctions, function(ind, f) {
f();
});
}
// add method here to be notified of auto-refreshes
window.requests.addRefreshFunction = function(f) {
window.requests._onRefreshFunctions.push(f);
}
// UTILS + MISC ///////////////////////////////////////////////////////
// retrieves parameter from the URL?query=string.
window.getURLParam = function(param) {
var v = document.URL;
var i = v.indexOf(param);
if(i <= -1) return '';
v = v.substr(i);
i = v.indexOf("&");
if(i >= 0) v = v.substr(0, i);
return v.replace(param+"=","");
}
// read cookie by name.
// http://stackoverflow.com/a/5639455/1684530 by cwolves
var cookies;
window.readCookie = function(name,c,C,i){
if(cookies) return cookies[name];
c = document.cookie.split('; ');
cookies = {};
for(i=c.length-1; i>=0; i--){
C = c[i].split('=');
cookies[C[0]] = unescape(C[1]);
}
return cookies[name];
}
window.writeCookie = function(name, val) {
document.cookie = name + "=" + val + '; expires=Thu, 31 Dec 2020 23:59:59 GMT; path=/';
}
// add thousand separators to given number.
// http://stackoverflow.com/a/1990590/1684530 by Doug Neiner.
window.digits = function(d) {
return (d+"").replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1");
}
// posts AJAX request to Ingress API.
// action: last part of the actual URL, the rpc/dashboard. is
// added automatically
// data: JSON data to post. method will be derived automatically from
// action, but may be overridden. Expects to be given Hash.
// Strings are not supported.
// success: method to call on success. See jQuery API docs for avail-
// able arguments: http://api.jquery.com/jQuery.ajax/
// error: see above. Additionally it is logged if the request failed.
window.postAjax = function(action, data, success, error) {
data = JSON.stringify($.extend({method: 'dashboard.'+action}, data));
var remove = function(data, textStatus, jqXHR) { window.requests.remove(jqXHR); };
var errCnt = function(jqXHR) { window.failedRequestCount++; window.requests.remove(jqXHR); };
return $.ajax({
url: 'rpc/dashboard.'+action,
type: 'POST',
data: data,
dataType: 'json',
success: [remove, success],
error: error ? [errCnt, error] : errCnt,
contentType: 'application/json; charset=utf-8',
beforeSend: function(req) {
req.setRequestHeader('X-CSRFToken', readCookie('csrftoken'));
}
});
}
// converts unix timestamps to HH:mm:ss format if it was today;
// otherwise it returns YYYY-MM-DD
window.unixTimeToString = function(time, full) {
if(!time) return null;
var d = new Date(typeof time === 'string' ? parseInt(time) : time);
var time = d.toLocaleTimeString();
var date = d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate();
if(typeof full !== 'undefined' && full) return date + ' ' + time;
if(d.toDateString() == new Date().toDateString())
return time;
else
return date;
}
window.unixTimeToHHmm = function(time) {
if(!time) return null;
var d = new Date(typeof time === 'string' ? parseInt(time) : time);
var h = '' + d.getHours(); h = h.length === 1 ? '0' + h : h;
var s = '' + d.getMinutes(); s = s.length === 1 ? '0' + s : s;
return h + ':' + s;
}
window.rangeLinkClick = function() {
if(window.portalRangeIndicator)
window.map.fitBounds(window.portalRangeIndicator.getBounds());
}
window.reportPortalIssue = function(info) {
var t = 'Redirecting you to a Google Help Page. Once there, click on “Contact Us” in the upper right corner.\n\nThe text box contains all necessary information. Press CTRL+C to copy it.';
var d = window.portals[window.selectedPortal].options.details;
var info = 'Your Nick: ' + PLAYER.nickname + ' '
+ 'Portal: ' + d.portalV2.descriptiveText.TITLE + ' '
+ 'Location: ' + d.portalV2.descriptiveText.ADDRESS
+' (lat ' + (d.locationE6.latE6/1E6) + '; lng ' + (d.locationE6.lngE6/1E6) + ')';
//codename, approx addr, portalname
if(prompt(t, info) !== null)
location.href = 'https://support.google.com/ingress?hl=en';
}
window._storedPaddedBounds = undefined;
window.getPaddedBounds = function() {
if(_storedPaddedBounds === undefined) {
map.on('zoomstart zoomend movestart moveend', function() {
window._storedPaddedBounds = null;
});
}
if(window._storedPaddedBounds) return window._storedPaddedBounds;
var p = window.map.getBounds().pad(VIEWPORT_PAD_RATIO);
window._storedPaddedBounds = p;
return p;
}
window.renderLimitReached = function() {
if(Object.keys(portals).length >= MAX_DRAWN_PORTALS) return true;
if(Object.keys(links).length >= MAX_DRAWN_LINKS) return true;
if(Object.keys(fields).length >= MAX_DRAWN_FIELDS) return true;
return false;
}
window.getMinPortalLevel = function() {
var z = map.getZoom();
if(z >= 16) return 0;
var conv = ['impossible', 8,7,7,6,6,5,5,4,4,3,3,2,2,1,1];
return conv[z];
}
// returns number of pixels left to scroll down before reaching the
// bottom. Works similar to the native scrollTop function.
window.scrollBottom = function(elm) {
if(typeof elm === 'string') elm = $(elm);
return elm.get(0).scrollHeight - elm.innerHeight() - elm.scrollTop();
}
window.zoomToAndShowPortal = function(guid, latlng) {
map.setView(latlng, 17);
// if the data is available, render it immediately. Otherwise defer
// until it becomes available.
if(window.portals[guid])
renderPortalDetails(guid);
else
urlPortal = guid;
}
// translates guids to entity types
window.getTypeByGuid = function(guid) {
// portals end in “.11” or “.12“, links in “.9", fields in “.b”
// .11 == portals
// .12 == portals
// .9 == links
// .b == fields
// .c == player/creator
// .d == chat messages
//
// others, not used in web:
// .5 == resources (burster/resonator)
// .6 == XM
// .4 == media items, maybe all droppped resources (?)
// resonator guid is [portal guid]-resonator-[slot]
switch(guid.slice(33)) {
case '11':
case '12':
return TYPE_PORTAL;
case '9':
return TYPE_LINK;
case 'b':
return TYPE_FIELD;
case 'c':
return TYPE_PLAYER;
case 'd':
return TYPE_CHAT;
default:
if(guid.slice(-11,-2) == 'resonator') return TYPE_RESONATOR;
return TYPE_UNKNOWN;
}
}
String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1).toLowerCase();
}
// http://stackoverflow.com/a/646643/1684530 by Bergi and CMS
if (typeof String.prototype.startsWith !== 'function') {
String.prototype.startsWith = function (str){
return this.slice(0, str.length) === str;
};
}
window.prettyEnergy = function(nrg) {
return nrg> 1000 ? Math.round(nrg/1000) + 'k': nrg;
}
window.setPermaLink = function(elm) {
var c = map.getCenter();
var lat = Math.round(c.lat*1E6);
var lng = Math.round(c.lng*1E6);
var qry = 'latE6='+lat+'&lngE6='+lng+'&z=' + map.getZoom();
$(elm).attr('href', 'http://www.ingress.com/intel?' + qry);
}
window.uniqueArray = function(arr) {
return $.grep(arr, function(v, i) {
return $.inArray(v, arr) === i;
});
}
// SETUP /////////////////////////////////////////////////////////////
// these functions set up specific areas after the boot function
// created a basic framework. All of these functions should only ever
// be run once.
window.setupLargeImagePreview = function() {
$('#portaldetails').on('click', '.imgpreview img', function() {
var ex = $('#largepreview');
if(ex.length > 0) {
ex.remove();
return;
}
var img = $(this).parent().html();
var w = $(this)[0].naturalWidth/2;
var h = $(this)[0].naturalHeight/2;
var c = $('#portaldetails').attr('class');
$('body').append(
'<div id="largepreview" class="'+c+'" style="margin-left: '+(-SIDEBAR_WIDTH/2-w-2)+'px; margin-top: '+(-h-2)+'px">' + img + '</div>'
);
$('#largepreview').click(function() { $(this).remove() });
$('#largepreview img').attr('title', '');
});
}
window.setupStyles = function() {
$('head').append('<style>' +
[ 'body:after { display: none }',
'#largepreview.enl img { border:2px solid '+COLORS[TEAM_ENL]+'; } ',
'#largepreview.res img { border:2px solid '+COLORS[TEAM_RES]+'; } ',
'#largepreview.none img { border:2px solid '+COLORS[TEAM_NONE]+'; } ',
'#chatcontrols { bottom: '+(CHAT_SHRINKED+24)+'px; }',
'#chat { height: '+CHAT_SHRINKED+'px; } ',
'.leaflet-right { margin-right: '+(SIDEBAR_WIDTH+1)+'px } ',
'#updatestatus { width:'+(SIDEBAR_WIDTH-2*4+1)+'px; } ',
'#sidebar { width:'+(SIDEBAR_WIDTH + HIDDEN_SCROLLBAR_ASSUMED_WIDTH + 1 /*border*/)+'px; } ',
'#sidebartoggle { right:'+SIDEBAR_WIDTH+'px; } ',
'#scrollwrapper { width:'+(SIDEBAR_WIDTH + 2*HIDDEN_SCROLLBAR_ASSUMED_WIDTH)+'px; right:-'+(2*HIDDEN_SCROLLBAR_ASSUMED_WIDTH-2)+'px } ',
'#sidebar input, h2 { width:'+(SIDEBAR_WIDTH - 2*4)+'px !important } ',
'#sidebar > *, #gamestat span, .imgpreview img { width:'+SIDEBAR_WIDTH+'px; }'].join("\n")
+ '</style>');
}
window.setupMap = function() {
$('#map').text('');
var osmOpt = {attribution: 'Map data © OpenStreetMap contributors', maxZoom: 18};
var osm = new L.TileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', osmOpt);
var cmOpt = {attribution: 'Map data © OpenStreetMap contributors, Imagery © CloudMade', maxZoom: 18};
var cmMin = new L.TileLayer('http://{s}.tile.cloudmade.com/654cef5fd49a432ab81267e200ecc502/22677/256/{z}/{x}/{y}.png', cmOpt);
var cmMid = new L.TileLayer('http://{s}.tile.cloudmade.com/654cef5fd49a432ab81267e200ecc502/999/256/{z}/{x}/{y}.png', cmOpt);
var views = [cmMid, cmMin, osm, new L.Google('INGRESS'), new L.Google('ROADMAP'),
new L.Google('SATELLITE'), new L.Google('HYBRID')];
window.map = new L.Map('map', $.extend(getPosition(),
{zoomControl: !(localStorage['iitc.zoom.buttons'] === 'false')}
));
try {
map.addLayer(views[readCookie('ingress.intelmap.type')]);
} catch(e) { map.addLayer(views[0]); }
var addLayers = {};
portalsLayers = [];
for(var i = 0; i <= 8; i++) {
portalsLayers[i] = L.layerGroup([]);
map.addLayer(portalsLayers[i]);
var t = (i === 0 ? 'Unclaimed' : 'Level ' + i) + ' Portals';
addLayers[t] = portalsLayers[i];
}
fieldsLayer = L.layerGroup([]);
map.addLayer(fieldsLayer, true);
addLayers['Fields'] = fieldsLayer;
linksLayer = L.layerGroup([]);
map.addLayer(linksLayer, true);
addLayers['Links'] = linksLayer;
map.addControl(new L.Control.Layers({
'OSM Cloudmade Midnight': views[0],
'OSM Cloudmade Minimal': views[1],
'OSM Mapnik': views[2],
'Google Roads Ingress Style': views[3],
'Google Roads': views[4],
'Google Satellite': views[5],
'Google Hybrid': views[6]
}, addLayers));
map.attributionControl.setPrefix('');
// listen for changes and store them in cookies
map.on('moveend', window.storeMapPosition);
map.on('zoomend', function() {
window.storeMapPosition;
// remove all resonators if zoom out to < RESONATOR_DISPLAY_ZOOM_LEVEL
if(isResonatorsShow()) return;
for(var i = 1; i < portalsLayers.length; i++) {
portalsLayers[i].eachLayer(function(item) {
var itemGuid = item.options.guid;
// check if 'item' is a resonator
if(getTypeByGuid(itemGuid) != TYPE_RESONATOR) return true;
portalsLayers[i].removeLayer(item);
});
}
console.log('Remove all resonators');
});
$("[name='leaflet-base-layers']").change(function () {
writeCookie('ingress.intelmap.type', $(this).parent().index());
});
// map update status handling
map.on('movestart zoomstart', function() { window.mapRunsUserAction = true });
map.on('moveend zoomend', function() { window.mapRunsUserAction = false });
// update map hooks
map.on('movestart zoomstart', window.requests.abort);
map.on('moveend zoomend', function() { window.startRefreshTimeout(500) });
// run once on init
window.requestData();
window.startRefreshTimeout();
window.addResumeFunction(window.requestData);
window.requests.addRefreshFunction(window.requestData);
};
// renders player details into the website. Since the player info is
// included as inline script in the original site, the data is static
// and cannot be updated.
window.setupPlayerStat = function() {
var level;
var ap = parseInt(PLAYER.ap);
for(level = 0; level < MIN_AP_FOR_LEVEL.length; level++) {
if(ap < MIN_AP_FOR_LEVEL[level]) break;
}
var thisLvlAp = MIN_AP_FOR_LEVEL[level-1];
var nextLvlAp = MIN_AP_FOR_LEVEL[level] || ap;
var lvlUpAp = digits(nextLvlAp-ap);
var lvlApProg = Math.round((ap-thisLvlAp)/(nextLvlAp-thisLvlAp)*100);
var xmMax = MAX_XM_PER_LEVEL[level];
var xmRatio = Math.round(PLAYER.energy/xmMax*100);
var cls = PLAYER.team === 'ALIENS' ? 'enl' : 'res';
var t = 'Level:\t\t' + level + '\n'
+ 'XM:\t\t\t' + PLAYER.energy + ' / ' + xmMax + '\n'
+ 'AP:\t\t\t' + digits(ap) + '\n'
+ (level < 8 ? 'level up in:\t' + lvlUpAp + ' AP' : 'Congrats! (neeeeerd)')
+ '\n\Invites:\t\t'+PLAYER.available_invites;
+ '\n\nNote: your player stats can only be updated by a full reload (F5)';
$('#playerstat').html(''
+ '<h2 title="'+t+'">'+level+'&nbsp;'
+ '<span class="'+cls+'">'+PLAYER.nickname+'</span>'
+ '<div>'
+ '<sup>XM: '+xmRatio+'%</sup>'
+ '<sub>' + (level < 8 ? 'level: '+lvlApProg+'%' : 'max level') + '</sub>'
+ '</div>'
+ '</h2>'
);
}
window.setupSidebarToggle = function() {
$('#sidebartoggle').on('click', function() {
var toggle = $('#sidebartoggle');
var sidebar = $('#sidebar');
if(sidebar.is(':visible')) {
sidebar.hide();
$('.leaflet-right').css('margin-right','0');
toggle.html('<span class="toggle open"></span>');
toggle.css('right', '0');
} else {
sidebar.show();
$('.leaflet-right').css('margin-right', SIDEBAR_WIDTH+1+'px');
toggle.html('<span class="toggle close"></span>');
toggle.css('right', SIDEBAR_WIDTH+1+'px');
}
});
}
// BOOTING ///////////////////////////////////////////////////////////
function boot() {
console.log('loading done, booting');
window.setupStyles();
window.setupMap();
window.setupGeosearch();
window.setupRedeem();
window.setupLargeImagePreview();
window.setupSidebarToggle();
window.updateGameScore();
window.setupPlayerStat();
window.chat.setup();
// read here ONCE, so the URL is only evaluated one time after the
// necessary data has been loaded.
urlPortal = getURLParam('pguid');
// load only once
var n = window.PLAYER['nickname'];
window.PLAYER['nickMatcher'] = new RegExp('\\b('+n+')\\b', 'ig');
$('#sidebar').show();
if(window.bootPlugins)
$.each(window.bootPlugins, function(ind, ref) { ref(); });
window.iitcLoaded = true;
}
// this is the minified load.js script that allows us to easily load
// further javascript files async as well as in order.
// https://github.com/chriso/load.js
// Copyright (c) 2010 Chris O'Hara <cohara87@gmail.com>. MIT Licensed
function asyncLoadScript(a){return function(b,c){var d=document.createElement("script");d.type="text/javascript",d.src=a,d.onload=b,d.onerror=c,d.onreadystatechange=function(){var a=this.readyState;if(a==="loaded"||a==="complete")d.onreadystatechange=null,b()},head.insertBefore(d,head.firstChild)}}(function(a){a=a||{};var b={},c,d;c=function(a,d,e){var f=a.halt=!1;a.error=function(a){throw a},a.next=function(c){c&&(f=!1);if(!a.halt&&d&&d.length){var e=d.shift(),g=e.shift();f=!0;try{b[g].apply(a,[e,e.length,g])}catch(h){a.error(h)}}return a};for(var g in b){if(typeof a[g]=="function")continue;(function(e){a[e]=function(){var g=Array.prototype.slice.call(arguments);if(e==="onError"){if(d)return b.onError.apply(a,[g,g.length]),a;var h={};return b.onError.apply(h,[g,g.length]),c(h,null,"onError")}return g.unshift(e),d?(a.then=a[e],d.push(g),f?a:a.next()):c({},[g],e)}})(g)}return e&&(a.then=a[e]),a.call=function(b,c){c.unshift(b),d.unshift(c),a.next(!0)},a.next()},d=a.addMethod=function(d){var e=Array.prototype.slice.call(arguments),f=e.pop();for(var g=0,h=e.length;g<h;g++)typeof e[g]=="string"&&(b[e[g]]=f);--h||(b["then"+d.substr(0,1).toUpperCase()+d.substr(1)]=f),c(a)},d("chain",function(a){var b=this,c=function(){if(!b.halt){if(!a.length)return b.next(!0);try{null!=a.shift().call(b,c,b.error)&&c()}catch(d){b.error(d)}}};c()}),d("run",function(a,b){var c=this,d=function(){c.halt||--b||c.next(!0)},e=function(a){c.error(a)};for(var f=0,g=b;!c.halt&&f<g;f++)null!=a[f].call(c,d,e)&&d()}),d("defer",function(a){var b=this;setTimeout(function(){b.next(!0)},a.shift())}),d("onError",function(a,b){var c=this;this.error=function(d){c.halt=!0;for(var e=0;e<b;e++)a[e].call(c,d)}})})(this);var head=document.getElementsByTagName("head")[0]||document.documentElement;addMethod("load",function(a,b){for(var c=[],d=0;d<b;d++)(function(b){c.push(asyncLoadScript(a[b]))})(d);this.call("run",c)})
// modified version of https://github.com/shramov/leaflet-plugins. Also
// contains the default Ingress map style.
var LLGMAPS = 'http://breunigs.github.com/ingress-intel-total-conversion/dist/leaflet_google.js';
var JQUERY = 'https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js';
var LEAFLET = 'http://cdn.leafletjs.com/leaflet-0.5/leaflet.js';
var AUTOLINK = 'http://breunigs.github.com/ingress-intel-total-conversion/dist/autolink.js';
// after all scripts have loaded, boot the actual app
load(JQUERY, LEAFLET, AUTOLINK).then(LLGMAPS).onError(function (err) {
alert('Could not all resources, the script likely wont work.\n\nIf this happend the first time for you, its probably a temporary issue. Just wait a bit and try again.\n\nIf you installed the script for the first time and this happens:\n try disabling NoScript if you have it installed\n press CTRL+SHIFT+K in Firefox or CTRL+SHIFT+I in Chrome/Opera and reload the page. Additional info may be available in the console.\n Open an issue at https://github.com/breunigs/ingress-intel-total-conversion/issues');
}).thenRun(boot);
window.chat = function() {};
window.chat._lastNicksForAutocomplete = [[], []];
window.chat.addNickForAutocomplete = function(nick, isFaction) {
var r = chat._lastNicksForAutocomplete[isFaction ? 0 : 1];
if(r.indexOf(nick) !== -1) return;
r.push(nick);
if(r.length >= 15)
r.shift();
}
window.chat.handleTabCompletion = function() {
var el = $('#chatinput input');
var curPos = el.get(0).selectionStart;
var text = el.val();
var word = text.slice(0, curPos).replace(/.*\b([a-z0-9-_])/, '$1').toLowerCase();
var list = window.chat._lastNicksForAutocomplete;
list = list[1].concat(list[0]);
var nick = null;
for(var i = 0; i < list.length; i++) {
if(!list[i].toLowerCase().startsWith(word)) continue;
if(nick && nick !== list[i]) {
console.log('More than one nick matches, aborting. ('+list[i]+' vs '+nick+')');
return;
}
nick = list[i];
}
if(!nick) {
console.log('No matches for ' + word);
return;
}
var posStart = curPos - word.length;
var newText = text.substring(0, posStart);
newText += nick + (posStart === 0 ? ': ' : ' ');
newText += text.substring(curPos);
el.val(newText);
}
//
// timestamp and clear management
//
window.chat._oldFactionTimestamp = -1;
window.chat._newFactionTimestamp = -1;
window.chat._oldPublicTimestamp = -1;
window.chat._newPublicTimestamp = -1;
window.chat.getOldestTimestamp = function(public) {
return chat['_old'+(public ? 'Public' : 'Faction')+'Timestamp'];
}
window.chat.getNewestTimestamp = function(public) {
return chat['_new'+(public ? 'Public' : 'Faction')+'Timestamp'];
}
window.chat.clearIfRequired = function(elm) {
if(!elm.data('needsClearing')) return;
elm.data('ignoreNextScroll', true).data('needsClearing', false).html('');
}
window.chat._oldBBox = null;
window.chat.genPostData = function(public, getOlderMsgs) {
if(typeof public !== 'boolean') throw('Need to know if public or faction chat.');
chat._localRangeCircle.setLatLng(map.getCenter());
var b = map.getBounds().extend(chat._localRangeCircle.getBounds());
var ne = b.getNorthEast();
var sw = b.getSouthWest();
// round bounds in order to ignore rounding errors
var bbs = $.map([ne.lat, ne.lng, sw.lat, sw.lng], function(x) { return Math.round(x*1E4) }).join();
if(chat._oldBBox && chat._oldBBox !== bbs) {
$('#chat > div').data('needsClearing', true);
console.log('Bounding Box changed, chat will be cleared (old: '+chat._oldBBox+' ; new: '+bbs+' )');
// need to reset these flags now because clearing will only occur
// after the request is finished i.e. there would be one almost
// useless request.
chat._displayedFactionGuids = [];
chat._displayedPublicGuids = [];
chat._displayedPlayerActionTime = {};
chat._oldFactionTimestamp = -1;
chat._newFactionTimestamp = -1;
chat._oldPublicTimestamp = -1;
chat._newPublicTimestamp = -1;
}
chat._oldBBox = bbs;
var ne = b.getNorthEast();
var sw = b.getSouthWest();
var data = {
desiredNumItems: public ? CHAT_PUBLIC_ITEMS : CHAT_FACTION_ITEMS,
minLatE6: Math.round(sw.lat*1E6),
minLngE6: Math.round(sw.lng*1E6),
maxLatE6: Math.round(ne.lat*1E6),
maxLngE6: Math.round(ne.lng*1E6),
minTimestampMs: -1,
maxTimestampMs: -1,
factionOnly: !public
}
if(getOlderMsgs) {
// ask for older chat when scrolling up
data = $.extend(data, {maxTimestampMs: chat.getOldestTimestamp(public)});
} else {
// ask for newer chat
var min = chat.getNewestTimestamp(public);
// the inital request will have both timestamp values set to -1,
// thus we receive the newest desiredNumItems. After that, we will
// only receive messages with a timestamp greater or equal to min
// above.
// After resuming from idle, there might be more new messages than
// desiredNumItems. So on the first request, we are not really up to
// date. We will eventually catch up, as long as there are less new
// messages than desiredNumItems per each refresh cycle.
// A proper solution would be to query until no more new results are
// returned. Another way would be to set desiredNumItems to a very
// large number so we really get all new messages since the last
// request. Setting desiredNumItems to -1 does unfortunately not
// work.
// Currently this edge case is not handled. Lets see if this is a
// problem in crowded areas.
$.extend(data, {minTimestampMs: min});
}
return data;
}
//
// requesting faction
//
window.chat._requestOldFactionRunning = false;
window.chat.requestOldFaction = function(isRetry) {
if(chat._requestOldFactionRunning) return;
if(isIdle()) return renderUpdateStatus();
chat._requestOldFactionRunning = true;
var d = chat.genPostData(false, true);
var r = window.postAjax(
'getPaginatedPlextsV2',
d,
chat.handleOldFaction,
isRetry
? function() { window.chat._requestOldFactionRunning = false; }
: function() { window.chat.requestOldFaction(true) }
);
requests.add(r);
}
window.chat._requestNewFactionRunning = false;
window.chat.requestNewFaction = function(isRetry) {
if(chat._requestNewFactionRunning) return;
if(window.isIdle()) return renderUpdateStatus();
chat._requestNewFactionRunning = true;
var d = chat.genPostData(false, false);
var r = window.postAjax(
'getPaginatedPlextsV2',
d,
chat.handleNewFaction,
isRetry
? function() { window.chat._requestNewFactionRunning = false; }
: function() { window.chat.requestNewFaction(true) }
);
requests.add(r);
}
//
// handle faction
//
window.chat.handleOldFaction = function(data, textStatus, jqXHR) {
chat._requestOldFactionRunning = false;
chat.handleFaction(data, textStatus, jqXHR, true);
}
window.chat.handleNewFaction = function(data, textStatus, jqXHR) {
chat._requestNewFactionRunning = false;
chat.handleFaction(data, textStatus, jqXHR, false);
}
window.chat._displayedFactionGuids = [];
window.chat.handleFaction = function(data, textStatus, jqXHR, isOldMsgs) {
if(!data || !data.result) {
window.failedRequestCount++;
return console.warn('faction chat error. Waiting for next auto-refresh.');
}
var c = $('#chatfaction');
chat.clearIfRequired(c);
if(data.result.length === 0) return;
chat._newFactionTimestamp = data.result[0][1];
chat._oldFactionTimestamp = data.result[data.result.length-1][1];
var scrollBefore = scrollBottom(c);
chat.renderPlayerMsgsTo(true, data, isOldMsgs, chat._displayedFactionGuids);
chat.keepScrollPosition(c, scrollBefore, isOldMsgs);
if(data.result.length >= CHAT_FACTION_ITEMS) chat.needMoreMessages();
}
//
// requesting public
//
window.chat._requestOldPublicRunning = false;
window.chat.requestOldPublic = function(isRetry) {
if(chat._requestOldPublicRunning) return;
if(isIdle()) return renderUpdateStatus();
chat._requestOldPublicRunning = true;
var d = chat.genPostData(true, true);
var r = window.postAjax(
'getPaginatedPlextsV2',
d,
chat.handleOldPublic,
isRetry
? function() { window.chat._requestOldPublicRunning = false; }
: function() { window.chat.requestOldPublic(true) }
);
requests.add(r);
}
window.chat._requestNewPublicRunning = false;
window.chat.requestNewPublic = function(isRetry) {
if(chat._requestNewPublicRunning) return;
if(window.isIdle()) return renderUpdateStatus();
chat._requestNewPublicRunning = true;
var d = chat.genPostData(true, false);
var r = window.postAjax(
'getPaginatedPlextsV2',
d,
chat.handleNewPublic,
isRetry
? function() { window.chat._requestNewPublicRunning = false; }
: function() { window.chat.requestNewPublic(true) }
);
requests.add(r);
}
//
// handle public
//
window.chat.handleOldPublic = function(data, textStatus, jqXHR) {
chat._requestOldPublicRunning = false;
chat.handlePublic(data, textStatus, jqXHR, true);
}
window.chat.handleNewPublic = function(data, textStatus, jqXHR) {
chat._requestNewPublicRunning = false;
chat.handlePublic(data, textStatus, jqXHR, false);
}
window.chat._displayedPublicGuids = [];
window.chat._displayedPlayerActionTime = {};
window.chat.handlePublic = function(data, textStatus, jqXHR, isOldMsgs) {
if(!data || !data.result) {
window.failedRequestCount++;
return console.warn('public chat error. Waiting for next auto-refresh.');
}
var ca = $('#chatautomated');
var cp = $('#chatpublic');
chat.clearIfRequired(ca);
chat.clearIfRequired(cp);
if(data.result.length === 0) return;
chat._newPublicTimestamp = data.result[0][1];
chat._oldPublicTimestamp = data.result[data.result.length-1][1];
var scrollBefore = scrollBottom(ca);
chat.handlePublicAutomated(data);
chat.keepScrollPosition(ca, scrollBefore, isOldMsgs);
var scrollBefore = scrollBottom(cp);
chat.renderPlayerMsgsTo(false, data, isOldMsgs, chat._displayedPublicGuids);
chat.keepScrollPosition(cp, scrollBefore, isOldMsgs);
if(data.result.length >= CHAT_PUBLIC_ITEMS) chat.needMoreMessages();
}
window.chat.handlePublicAutomated = function(data) {
$.each(data.result, function(ind, json) { // newest first!
var time = json[1];
// ignore player messages
var t = json[2].plext.plextType;
if(t !== 'SYSTEM_BROADCAST' && t !== 'SYSTEM_NARROWCAST') return true;
var tmpmsg = '', nick = null, pguid, team;
// each automated message is composed of many text chunks. loop
// over them to gather all necessary data.
$.each(json[2].plext.markup, function(ind, part) {
switch(part[0]) {
case 'PLAYER':
pguid = part[1].guid;
var lastAction = window.chat._displayedPlayerActionTime[pguid];
// ignore older messages about player
if(lastAction && lastAction[0] > time) return false;
nick = part[1].plain;
team = part[1].team === 'ALIENS' ? TEAM_ENL : TEAM_RES;
window.setPlayerName(pguid, nick); // free nick name resolves
if(ind > 0) tmpmsg += nick; // dont repeat nick directly
break;
case 'TEXT':
tmpmsg += part[1].plain;
break;
case 'PORTAL':
var latlng = [part[1].latE6/1E6, part[1].lngE6/1E6];
var js = 'window.zoomToAndShowPortal(\''+part[1].guid+'\', ['+latlng[0]+', '+latlng[1]+'])';
tmpmsg += '<a onclick="'+js+'" title="'+part[1].address+'" class="help">'+part[1].name+'</a>';
break;
}
});
// nick will only be set if we dont have any info about that
// player yet.
if(nick) {
tmpmsg = chat.renderMsg(tmpmsg, nick, time, team);
window.chat._displayedPlayerActionTime[pguid] = [time, tmpmsg];
};
});
if(chat.getActive() === 'automated')
window.chat.renderAutomatedMsgsTo();
}
window.chat.renderAutomatedMsgsTo = function() {
var x = window.chat._displayedPlayerActionTime;
// we dont care about the GUIDs anymore
var vals = $.map(x, function(v, k) { return [v]; });
// sort them old to new
vals = vals.sort(function(a, b) { return a[0]-b[0]; });
var prevTime = null;
var msgs = $.map(vals, function(v) {
var nowTime = new Date(v[0]).toLocaleDateString();
if(prevTime && prevTime !== nowTime)
var val = chat.renderDivider(nowTime) + v[1];
else
var val = v[1];
prevTime = nowTime;
return val;
}).join('\n');
$('#chatautomated').html(msgs);
}
//
// common
//
window.chat.renderPlayerMsgsTo = function(isFaction, data, isOldMsgs, dupCheckArr) {
var msgs = '';
var prevTime = null;
$.each(data.result.reverse(), function(ind, json) { // oldest first!
if(json[2].plext.plextType !== 'PLAYER_GENERATED') return true;
// avoid duplicates
if(dupCheckArr.indexOf(json[0]) !== -1) return true;
dupCheckArr.push(json[0]);
var time = json[1];
var team = json[2].plext.team === 'ALIENS' ? TEAM_ENL : TEAM_RES;
var msg, nick, pguid;
$.each(json[2].plext.markup, function(ind, markup) {
if(markup[0] === 'SENDER') {
nick = markup[1].plain.slice(0, -2); // cut “: ” at end
pguid = markup[1].guid;
window.setPlayerName(pguid, nick); // free nick name resolves
if(!isOldMsgs) window.chat.addNickForAutocomplete(nick, isFaction);
}
if(markup[0] === 'TEXT') {
msg = markup[1].plain.autoLink();
msg = msg.replace(window.PLAYER['nickMatcher'], '<em>$1</em>');
}
if(!isFaction && markup[0] === 'SECURE') {
nick = null;
return false; // aka break
}
});
if(!nick) return true; // aka next
var nowTime = new Date(time).toLocaleDateString();
if(prevTime && prevTime !== nowTime)
msgs += chat.renderDivider(nowTime);
msgs += chat.renderMsg(msg, nick, time, team);
prevTime = nowTime;
});
var addTo = isFaction ? $('#chatfaction') : $('#chatpublic');
// if there is a change of day between two requests, handle the
// divider insertion here.
if(isOldMsgs) {
var ts = addTo.find('time:first').data('timestamp');
var nextTime = new Date(ts).toLocaleDateString();
if(prevTime && prevTime !== nextTime && ts)
msgs += chat.renderDivider(nextTime);
}
if(isOldMsgs)
addTo.prepend(msgs);
else
addTo.append(msgs);
}
window.chat.renderDivider = function(text) {
return '<summary>─ '+text+' ────────────────────────────────────────────────────────────────────────────</summary>';
}
window.chat.renderMsg = function(msg, nick, time, team) {
var ta = unixTimeToHHmm(time);
var tb = unixTimeToString(time, true);
// help cursor via “#chat time”
var t = '<time title="'+tb+'" data-timestamp="'+time+'">'+ta+'</time>';
var s = 'style="color:'+COLORS[team]+'"';
var title = nick.length >= 8 ? 'title="'+nick+'" class="help"' : '';
return '<p>'+t+'<span class="invisibleseparator"> &lt;</span><mark '+s+'>'+nick+'</mark><span class="invisibleseparator">&gt; </span><span>'+msg+'</span></p>';
}
window.chat.getActive = function() {
return $('#chatcontrols .active').text();
}
window.chat.toggle = function() {
var c = $('#chat, #chatcontrols');
if(c.hasClass('expand')) {
$('#chatcontrols a:first').html('<span class="toggle expand"></span>');
c.removeClass('expand');
var div = $('#chat > div:visible');
div.data('ignoreNextScroll', true);
div.scrollTop(99999999); // scroll to bottom
$('.leaflet-control').css('margin-left', '13px');
} else {
$('#chatcontrols a:first').html('<span class="toggle shrink"></span>');
c.addClass('expand');
$('.leaflet-control').css('margin-left', '720px');
chat.needMoreMessages();
}
}
window.chat.request = function() {
console.log('refreshing chat');
chat.requestNewFaction();
chat.requestNewPublic();
}
// checks if there are enough messages in the selected chat tab and
// loads more if not.
window.chat.needMoreMessages = function() {
var activeChat = $('#chat > :visible');
if(scrollBottom(activeChat) !== 0 || activeChat.scrollTop() !== 0) return;
console.log('no scrollbar in active chat, requesting more msgs');
if($('#chatcontrols a:last.active').length)
chat.requestOldFaction();
else
chat.requestOldPublic();
}
window.chat.chooser = function(event) {
var t = $(event.target);
var tt = t.text();
var span = $('#chatinput span');
$('#chatcontrols .active').removeClass('active');
t.addClass('active');
$('#chat > div').hide();
var elm;
switch(tt) {
case 'faction':
span.css('color', '');
span.text('tell faction:');
elm = $('#chatfaction');
break;
case 'public':
span.css('cssText', 'color: red !important');
span.text('broadcast:');
elm = $('#chatpublic');
break;
case 'automated':
span.css('cssText', 'color: #bbb !important');
span.text('tell Jarvis:');
chat.renderAutomatedMsgsTo();
elm = $('#chatautomated');
break;
}
elm.show();
if(elm.data('needsScrollTop')) {
elm.data('ignoreNextScroll', true);
elm.scrollTop(elm.data('needsScrollTop'));
elm.data('needsScrollTop', null);
}
chat.needMoreMessages();
}
// contains the logic to keep the correct scroll position.
window.chat.keepScrollPosition = function(box, scrollBefore, isOldMsgs) {
// If scrolled down completely, keep it that way so new messages can
// be seen easily. If scrolled up, only need to fix scroll position
// when old messages are added. New messages added at the bottom dont
// change the view and enabling this would make the chat scroll down
// for every added message, even if the user wants to read old stuff.
if(box.is(':hidden') && !isOldMsgs) {
box.data('needsScrollTop', 99999999);
return;
}
if(scrollBefore === 0 || isOldMsgs) {
box.data('ignoreNextScroll', true);
box.scrollTop(box.scrollTop() + (scrollBottom(box)-scrollBefore));
}
}
//
// setup
//
window.chat.setup = function() {
window.chat._localRangeCircle = L.circle(map.getCenter(), CHAT_MIN_RANGE*1000);
$('#chatcontrols, #chat, #chatinput').show();
$('#chatcontrols a:first').click(window.chat.toggle);
$('#chatcontrols a:not(:first)').click(window.chat.chooser);
$('#chatinput').click(function() {
$('#chatinput input').focus();
});
window.chat.setupTime();
window.chat.setupPosting();
$('#chatfaction').scroll(function() {
var t = $(this);
if(t.data('ignoreNextScroll')) return t.data('ignoreNextScroll', false);
if(t.scrollTop() < 200) chat.requestOldFaction();
if(scrollBottom(t) === 0) chat.requestNewFaction();
});
$('#chatpublic, #chatautomated').scroll(function() {
var t = $(this);
if(t.data('ignoreNextScroll')) return t.data('ignoreNextScroll', false);
if(t.scrollTop() < 200) chat.requestOldPublic();
if(scrollBottom(t) === 0) chat.requestNewPublic();
});
chat.request();
window.addResumeFunction(chat.request);
window.requests.addRefreshFunction(chat.request);
var cls = PLAYER.team === 'ALIENS' ? 'enl' : 'res';
$('#chatinput span').addClass(cls)
}
window.chat.setupTime = function() {
var inputTime = $('#chatinput time');
var updateTime = function() {
if(window.isIdle()) return;
var d = new Date();
var h = d.getHours() + ''; if(h.length === 1) h = '0' + h;
var m = d.getMinutes() + ''; if(m.length === 1) m = '0' + m;
inputTime.text(h+':'+m);
// update ON the minute (1ms after)
setTimeout(updateTime, (60 - d.getSeconds()) * 1000 + 1);
};
updateTime();
window.addResumeFunction(updateTime);
}
//
// posting
//
window.chat.setupPosting = function() {
$('#chatinput input').keydown(function(event) {
try{
var kc = (event.keyCode ? event.keyCode : event.which);
if(kc === 13) { // enter
chat.postMsg();
event.preventDefault();
} else if (kc === 9) { // tab
event.preventDefault();
window.chat.handleTabCompletion();
}
} catch(error) {
console.log(error);
debug.printStackTrace();
}
});
$('#chatinput').submit(function(event) {
chat.postMsg();
event.preventDefault();
});
}
window.chat.postMsg = function() {
var c = chat.getActive();
if(c === 'automated') return alert('Jarvis: A strange game. The only winning move is not to play. How about a nice game of chess?');
var msg = $.trim($('#chatinput input').val());
if(!msg || msg === '') return;
var public = c === 'public';
var latlng = map.getCenter();
var data = {message: msg,
latE6: Math.round(latlng.lat*1E6),
lngE6: Math.round(latlng.lng*1E6),
factionOnly: !public};
window.postAjax('sendPlext', data,
function() { if(public) chat.requestNewPublic(); else chat.requestNewFaction(); },
function() {
alert('Your message could not be delivered. You can copy&' +
'paste it here and try again if you want:\n\n'+msg);
}
);
$('#chatinput input').val('');
}
// PORTAL DETAILS DISPLAY ////////////////////////////////////////////
// hand any of these functions the details-hash of a portal, and they
// will return pretty, displayable HTML or parts thereof.
// returns displayable text+link about portal range
window.getRangeText = function(d) {
var range = getPortalRange(d);
return ['range',
+ '<a onclick="window.rangeLinkClick()">'
+ (range > 1000
? Math.round(range/1000) + 'km'
: Math.round(range) + 'm')
+ '</a>'];
}
// generates description text from details for portal
window.getPortalDescriptionFromDetails = function(details) {
var descObj = details.portalV2.descriptiveText;
// FIXME: also get real description?
var desc = descObj.TITLE + '\n' + descObj.ADDRESS;
if(descObj.ATTRIBUTION)
desc += '\nby '+descObj.ATTRIBUTION+' ('+descObj.ATTRIBUTION_LINK+')';
return desc;
}
// given portal details, returns html code to display mod details.
window.getModDetails = function(d) {
var mods = [];
var modsTitle = [];
var modsColor = [];
$.each(d.portalV2.linkedModArray, function(ind, mod) {
if(!mod) {
mods.push('');
modsTitle.push('');
modsColor.push('#000');
} else if(mod.type === 'RES_SHIELD') {
var title = mod.rarity.capitalize() + ' ' + mod.displayName + '\n';
title += 'Installed by: '+ getPlayerName(mod.installingUser);
title += '\nStats:';
for (var key in mod.stats) {
if (!mod.stats.hasOwnProperty(key)) continue;
title += '\n+' + mod.stats[key] + ' ' + key.capitalize();
}
mods.push(mod.rarity.capitalize().replace('_', ' ') + ' ' + mod.displayName);
modsTitle.push(title);
modsColor.push(COLORS_MOD[mod.rarity]);
} else {
mods.push(mod.type);
modsTitle.push('Unknown mod. No further details available.');
modsColor.push('#FFF');
}
});
var t = '<span title="'+modsTitle[0]+'" style="color:'+modsColor[0]+'">'+mods[0]+'</span>'
+ '<span title="'+modsTitle[1]+'" style="color:'+modsColor[1]+'">'+mods[1]+'</span>'
+ '<span title="'+modsTitle[2]+'" style="color:'+modsColor[2]+'">'+mods[2]+'</span>'
+ '<span title="'+modsTitle[3]+'" style="color:'+modsColor[3]+'">'+mods[3]+'</span>'
return t;
}
window.getEnergyText = function(d) {
var currentNrg = getCurrentPortalEnergy(d);
var totalNrg = getTotalPortalEnergy(d);
var inf = currentNrg + ' / ' + totalNrg;
var fill = prettyEnergy(currentNrg) + ' / ' + prettyEnergy(totalNrg)
return ['energy', '<tt title="'+inf+'">' + fill + '</tt>'];
}
window.getAvgResoDistText = function(d) {
var avgDist = Math.round(10*getAvgResoDist(d))/10;
return ['⌀ res dist', avgDist + 'm'];
}
window.getResonatorDetails = function(d) {
var resoDetails = '';
// 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 columns, they
// NW E need to be ordered like this: N, NW, W, SW, NE,
// W SE E, SE, S, i.e. 2 3 4 5 1 0 7 6
// SW S
$.each([2, 3, 4, 5, 1, 0, 7, 6], function(ind, slot) {
var isLeft = slot >= 2 && slot <= 5;
var reso = d.resonatorArray.resonators[slot];
if(!reso) {
resoDetails += renderResonatorDetails(slot, 0, 0, null, null, isLeft);
return true;
}
var l = parseInt(reso.level);
var v = parseInt(reso.energyTotal);
var nick = window.getPlayerName(reso.ownerGuid);
var dist = reso.distanceToPortal;
// if array order and slot order drift apart, at least the octant
// naming will still be correct.
slot = parseInt(reso.slot);
resoDetails += renderResonatorDetails(slot, l, v, dist, nick, isLeft);
});
return resoDetails;
}
// helper function that renders the HTML for a given resonator. Does
// not work with raw details-hash. Needs digested infos instead:
// slot: which slot this resonator occupies. Starts with 0 (east) and
// rotates clockwise. So, last one is 7 (southeast).
window.renderResonatorDetails = function(slot, level, nrg, dist, nick, isLeft) {
if(level === 0) {
var meter = '<span class="meter" title="octant:\t' + OCTANTS[slot] + '"></span>';
} else {
var max = RESO_NRG[level];
var fillGrade = nrg/max*100;
var inf = 'energy:\t\t' + nrg + ' / ' + max + ' (' + Math.round(fillGrade) + '%)\n'
+ 'level:\t\t' + level + '\n'
+ 'distance:\t' + dist + 'm\n'
+ 'owner:\t\t' + nick + '\n'
+ 'octant:\t' + OCTANTS[slot];
var style = 'width:'+fillGrade+'%; background:'+COLORS_LVL[level]+';';
var color = (level < 3 ? "#9900FF" : "#FFFFFF");
var lbar = '<span class="meter-level" style="color: ' + color + ';"> ' + level + ' </span>';
var fill = '<span style="'+style+'"></span>';
var meter = '<span class="meter meter-rel" title="'+inf+'">' + fill + lbar + '</span>';
}
var cls = isLeft ? 'left' : 'right';
var text = '<span class="meter-text '+cls+'">'+(nick||'')+'</span>';
return (isLeft ? text+meter : meter+text) + '<br/>';
}
// calculate AP gain from destroying portal
// so far it counts only resonators + links
window.getDestroyAP = function(d) {
var resoCount = 0;
$.each(d.resonatorArray.resonators, function(ind, reso) {
if(!reso) return true;
resoCount += 1;
});
var linkCount = d.portalV2.linkedEdges ? d.portalV2.linkedEdges.length : 0;
var fieldCount = d.portalV2.linkedFields ? d.portalV2.linkedFields.length : 0;
var resoAp = resoCount * DESTROY_RESONATOR;
var linkAp = linkCount * DESTROY_LINK;
var fieldAp = fieldCount * DESTROY_FIELD;
var sum = resoAp + linkAp + fieldAp;
function tt(text) {
var t = 'Destroy:\n';
t += resoCount + '×\tResonators\t= ' + digits(resoAp) + '\n';
t += linkCount + '×\tLinks\t\t= ' + digits(linkAp) + '\n';
t += fieldCount + '×\tFields\t\t= ' + digits(fieldAp) + '\n';
t += 'Sum: ' + digits(sum) + ' AP';
return '<tt title="'+t+'">' + digits(text) + '</tt>';
}
return [tt('AP Gain'), tt(sum)];
}
// GAME STATUS ///////////////////////////////////////////////////////
// MindUnit display
window.updateGameScore = function(data) {
if(!data) {
window.postAjax('getGameScore', {}, window.updateGameScore);
return;
}
var r = parseInt(data.result.resistanceScore), e = parseInt(data.result.alienScore);
var s = r+e;
var rp = r/s*100, ep = e/s*100;
r = digits(r), e = digits(e);
var rs = '<span class="res" style="width:'+rp+'%;">'+Math.round(rp)+'%&nbsp;</span>';
var es = '<span class="enl" style="width:'+ep+'%;">&nbsp;'+Math.round(ep)+'%</span>';
$('#gamestat').html(rs+es).one('click', function() { window.updateGameScore() });
// help cursor via “#gamestat span”
$('#gamestat').attr('title', 'Resistance:\t\t'+r+' MindUnits\nEnlightenment:\t'+e+' MindUnits');
window.setTimeout('window.updateGameScore', REFRESH_GAME_SCORE*1000);
}
// MAP DATA REQUEST CALCULATORS //////////////////////////////////////
// Ingress Intel splits up requests for map data (portals, links,
// fields) into tiles. To get data for the current viewport (i.e. what
// is currently visible) it first calculates which tiles intersect.
// For all those tiles, it then calculates the lat/lng bounds of that
// tile and a quadkey. Both the bounds and the quadkey are “somewhat”
// required to get complete data. No idea how the projection between
// lat/lng and tiles works.
// What follows now are functions that allow conversion between tiles
// and lat/lng as well as calculating the quad key. The variable names
// may be misleading.
// The minified source for this code was in gen_dashboard.js after the
// “// input 89” line (alternatively: the class was called “Xe”).
window.convertCenterLat = function(centerLat) {
return Math.round(256 * 0.9999 * Math.abs(1 / Math.cos(centerLat * DEG2RAD)));
}
window.calculateR = function(convCenterLat) {
return 1 << window.map.getZoom() - (convCenterLat / 256 - 1);
}
window.convertLatLngToPoint = function(latlng, magic, R) {
var x = (magic/2 + latlng.lng * magic / 360)*R;
var l = Math.sin(latlng.lat * DEG2RAD);
var y = (magic/2 + 0.5*Math.log((1+l)/(1-l)) * -(magic / (2*Math.PI)))*R;
return {x: Math.floor(x/magic), y: Math.floor(y/magic)};
}
window.convertPointToLatLng = function(x, y, magic, R) {
var e = {};
e.sw = {
// orig function put together from all over the place
// lat: (2 * Math.atan(Math.exp((((y + 1) * magic / R) - (magic/ 2)) / (-1*(magic / (2 * Math.PI))))) - Math.PI / 2) / (Math.PI / 180),
// shortened version by your favorite algebra program.
lat: (360*Math.atan(Math.exp(Math.PI - 2*Math.PI*(y+1)/R)))/Math.PI - 90,
lng: 360*x/R-180
};
e.ne = {
//lat: (2 * Math.atan(Math.exp(((y * magic / R) - (magic/ 2)) / (-1*(magic / (2 * Math.PI))))) - Math.PI / 2) / (Math.PI / 180),
lat: (360*Math.atan(Math.exp(Math.PI - 2*Math.PI*y/R)))/Math.PI - 90,
lng: 360*(x+1)/R-180
};
return e;
}
// calculates the quad key for a given point. The point is not(!) in
// lat/lng format.
window.pointToQuadKey = function(x, y) {
var quadkey = [];
for(var c = window.map.getZoom(); c > 0; c--) {
// +-------+ quadrants are probably ordered like this
// | 0 | 1 |
// |---|---|
// | 2 | 3 |
// |---|---|
var quadrant = 0;
var e = 1 << c - 1;
(x & e) != 0 && quadrant++; // push right
(y & e) != 0 && (quadrant++, quadrant++); // push down
quadkey.push(quadrant);
}
return quadkey.join("");
}
// given quadkey and bounds, returns the format as required by the
// Ingress API to request map data.
window.generateBoundsParams = function(quadkey, bounds) {
return {
id: quadkey,
qk: quadkey,
minLatE6: Math.round(bounds.sw.lat * 1E6),
minLngE6: Math.round(bounds.sw.lng * 1E6),
maxLatE6: Math.round(bounds.ne.lat * 1E6),
maxLngE6: Math.round(bounds.ne.lng * 1E6)
};
}
// ENTITY DETAILS TOOLS //////////////////////////////////////////////
// hand any of these functions the details-hash of an entity (i.e.
// portal, link, field) and they will return useful data.
// given the entity detail data, returns the team the entity belongs
// to. Uses TEAM_* enum values.
window.getTeam = function(details) {
var team = TEAM_NONE;
if(details.controllingTeam.team === 'ALIENS') team = TEAM_ENL;
if(details.controllingTeam.team === 'RESISTANCE') team = TEAM_RES;
return team;
}
// IDLE HANDLING /////////////////////////////////////////////////////
window.idleTime = 0; // in minutes
setInterval('window.idleTime += 1', 60*1000);
var idleReset = function () {
// update immediately when the user comes back
if(isIdle()) {
window.idleTime = 0;
$.each(window._onResumeFunctions, function(ind, f) {
f();
});
}
window.idleTime = 0;
};
$('body').mousemove(idleReset).keypress(idleReset);
window.isIdle = function() {
return window.idleTime >= MAX_IDLE_TIME;
}
window._onResumeFunctions = [];
// add your function here if you want to be notified when the user
// resumes from being idle
window.addResumeFunction = function(f) {
window._onResumeFunctions.push(f);
}
// LOCATION HANDLING /////////////////////////////////////////////////
// i.e. setting initial position and storing new position after moving
// retrieves current position from map and stores it cookies
window.storeMapPosition = function() {
var m = window.map.getCenter();
writeCookie('ingress.intelmap.lat', m['lat']);
writeCookie('ingress.intelmap.lng', m['lng']);
writeCookie('ingress.intelmap.zoom', window.map.getZoom());
}
// either retrieves the last shown position from a cookie, from the
// URL or if neither is present, via Geolocation. If that fails, it
// returns a map that shows the whole world.
window.getPosition = function() {
if(getURLParam('latE6') && getURLParam('lngE6')) {
console.log("mappos: reading URL params");
var lat = parseInt(getURLParam('latE6'))/1E6 || 0.0;
var lng = parseInt(getURLParam('lngE6'))/1E6 || 0.0;
// google seems to zoom in far more than leaflet
var z = parseInt(getURLParam('z'))+1 || 17;
return {center: new L.LatLng(lat, lng), zoom: z > 18 ? 18 : z};
}
if(readCookie('ingress.intelmap.lat') && readCookie('ingress.intelmap.lng')) {
console.log("mappos: reading cookies");
var lat = parseFloat(readCookie('ingress.intelmap.lat')) || 0.0;
var lng = parseFloat(readCookie('ingress.intelmap.lng')) || 0.0;
var z = parseInt(readCookie('ingress.intelmap.zoom')) || 17;
return {center: new L.LatLng(lat, lng), zoom: z > 18 ? 18 : z};
}
setTimeout("window.map.locate({setView : true, maxZoom: 13});", 50);
return {center: new L.LatLng(0.0, 0.0), zoom: 1};
}
// PORTAL DETAILS MAIN ///////////////////////////////////////////////
// main code block that renders the portal details in the sidebar and
// methods that highlight the portal in the map view.
window.renderPortalDetails = function(guid) {
var d = window.portals[guid].options.details;
if(!d) {
unselectOldPortal();
urlPortal = guid;
return;
}
var update = selectPortal(guid);
// collect some random data thats not worth to put in an own method
var links = {incoming: 0, outgoing: 0};
if(d.portalV2.linkedEdges) $.each(d.portalV2.linkedEdges, function(ind, link) {
links[link.isOrigin ? 'outgoing' : 'incoming']++;
});
function linkExpl(t) { return '<tt title="↳ incoming links\n↴ outgoing links\n• is meant to be the portal.">'+t+'</tt>'; }
var linksText = [linkExpl('links'), linkExpl(' ↳ ' + links.incoming+'&nbsp;&nbsp;•&nbsp;&nbsp;'+links.outgoing+' ↴')];
var player = d.captured && d.captured.capturingPlayerId
? getPlayerName(d.captured.capturingPlayerId)
: null;
var playerText = player ? ['owner', player] : null;
var time = d.captured ? unixTimeToString(d.captured.capturedTime) : null;
var sinceText = time ? ['since', time] : null;
var linkedFields = ['fields', d.portalV2.linkedFields.length];
// collect and html-ify random data
var randDetails = [playerText, sinceText, getRangeText(d), getEnergyText(d), linksText, getAvgResoDistText(d), linkedFields, getDestroyAP(d)];
randDetails = randDetails.map(function(detail) {
if(!detail) return '';
detail = '<aside>'+detail[0]+'<span>'+detail[1]+'</span></aside>';
return detail;
}).join('\n');
// replacing causes flicker, so if the selected portal does not
// change, only update the data points that are likely to change.
if(update) {
console.log('Updating portal details');
$('#level').text(Math.floor(getPortalLevel(d)));
$('.mods').html(getModDetails(d));
$('#randdetails').html(randDetails);
$('#resodetails').html(getResonatorDetails(d));
$('#portaldetails').attr('class', TEAM_TO_CSS[getTeam(d)]);
} else {
console.log('exchanging portal details');
setPortalIndicators(d);
var img = d.imageByUrl && d.imageByUrl.imageUrl ? d.imageByUrl.imageUrl : DEFAULT_PORTAL_IMG;
var lat = d.locationE6.latE6;
var lng = d.locationE6.lngE6;
var perma = 'http://ingress.com/intel?latE6='+lat+'&lngE6='+lng+'&z=17&pguid='+guid;
$('#portaldetails')
.attr('class', TEAM_TO_CSS[getTeam(d)])
.html(''
+ '<h3>'+d.portalV2.descriptiveText.TITLE+'</h3>'
// help cursor via “.imgpreview img”
+ '<div class="imgpreview"><img src="'+img+'" title="'+getPortalDescriptionFromDetails(d)+'\n\nClick to show full image."/></div>'
+ '<span id="level">'+Math.floor(getPortalLevel(d))+'</span>'
+ '<div class="mods">'+getModDetails(d)+'</div>'
+ '<div id="randdetails">'+randDetails+'</div>'
+ '<div id="resodetails">'+getResonatorDetails(d)+'</div>'
+ '<div class="linkdetails">'
+ '<aside><a href="'+perma+'">portal link</a></aside>'
+ '<aside><a onclick="window.reportPortalIssue()">report issue</a></aside>'
+ '</div>'
);
}
// try to resolve names that were required for above functions, but
// werent available yet.
resolvePlayerNames();
}
// draws link-range and hack-range circles around the portal with the
// given details.
window.setPortalIndicators = function(d) {
if(portalRangeIndicator) map.removeLayer(portalRangeIndicator);
var range = getPortalRange(d);
var coord = [d.locationE6.latE6/1E6, d.locationE6.lngE6/1E6];
portalRangeIndicator = (range > 0
? L.circle(coord, range, { fill: false, color: RANGE_INDICATOR_COLOR, weight: 3, clickable: false })
: L.circle(coord, range, { fill: false, stroke: false, clickable: false })
).addTo(map);
if(!portalAccessIndicator)
portalAccessIndicator = L.circle(coord, HACK_RANGE,
{ fill: false, color: ACCESS_INDICATOR_COLOR, weight: 2, clickable: false }
).addTo(map);
else
portalAccessIndicator.setLatLng(coord);
}
// highlights portal with given GUID. Automatically clears highlights
// on old selection. Returns false if the selected portal changed.
// Returns true if its still the same portal that just needs an
// update.
window.selectPortal = function(guid) {
var update = selectedPortal === guid;
var oldPortal = portals[selectedPortal];
if(!update && oldPortal) portalResetColor(oldPortal);
selectedPortal = guid;
if(portals[guid])
portals[guid].bringToFront().setStyle({color: COLOR_SELECTED_PORTAL});
return update;
}
window.unselectOldPortal = function() {
var oldPortal = portals[selectedPortal];
if(oldPortal)
oldPortal.setStyle({color: oldPortal.options.fillColor});
selectedPortal = null;
$('#portaldetails').html('');
}
// REDEEMING /////////////////////////////////////////////////////////
window.handleRedeemResponse = function(data, textStatus, jqXHR) {
if (data.error) {
var error = '';
if (data.error === 'ALREADY_REDEEMED') {
error = 'The passcode has already been redeemed.';
} else if (data.error === 'ALREADY_REDEEMED_BY_PLAYER') {
error = 'You have already redeemed this passcode.';
} else if (data.error === 'INVALID_PASSCODE') {
error = 'This passcode is invalid.';
} else {
error = 'The passcode cannot be redeemed.';
}
alert("Error: " + data.error + "\n" + error);
} else if (data.result) {
var res_level = 0, res_count = 0;
var xmp_level = 0, xmp_count = 0;
var shield_rarity = '', shield_count = 0;
// This assumes that each passcode gives only one type of resonator/XMP/shield.
// This may break at some point, depending on changes to passcode functionality.
for (var i in data.result.inventoryAward) {
var acquired = data.result.inventoryAward[i][2];
if (acquired.modResource) {
if (acquired.modResource.resourceType === 'RES_SHIELD') {
shield_rarity = acquired.modResource.rarity.split('_').map(function (i) {return i[0]}).join('');
shield_count++;
}
} else if (acquired.resourceWithLevels) {
if (acquired.resourceWithLevels.resourceType === 'EMITTER_A') {
res_level = acquired.resourceWithLevels.level;
res_count++;
} else if (acquired.resourceWithLevels.resourceType === 'EMP_BURSTER') {
xmp_level = acquired.resourceWithLevels.level;
xmp_count++;
}
}
}
alert("Passcode redeemed!\n" + [data.result.apAward + 'AP', data.result.xmAward + 'XM', res_count + 'xL' + res_level + ' RES', xmp_count + 'xL' + xmp_level + ' XMP', shield_count + 'x' + shield_rarity + ' SHIELD'].join('/'));
}
}
window.setupRedeem = function() {
$("#redeem").keypress(function(e) {
if((e.keyCode ? e.keyCode : e.which) != 13) return;
var data = {passcode: $(this).val()};
window.postAjax('redeemReward', data, window.handleRedeemResponse,
function() { alert('HTTP request failed. Try again?'); });
});
}
// PLAYER NAMES //////////////////////////////////////////////////////
// Player names are cached in local storage forever. There is no GUI
// element from within the total conversion to clean them, but you
// can run localStorage.clean() to reset it.
// retrieves player name by GUID. If the name is not yet available, it
// will be added to a global list of GUIDs that need to be resolved.
// The resolve method is not called automatically.
window.getPlayerName = function(guid) {
if(localStorage[guid]) return localStorage[guid];
// only add to queue if it isnt already
if(playersToResolve.indexOf(guid) === -1 && playersInResolving.indexOf(guid) === -1) {
console.log('resolving player guid=' + guid);
playersToResolve.push(guid);
}
return '{'+guid.slice(0, 12)+'}';
}
// resolves all player GUIDs that have been added to the list. Reruns
// renderPortalDetails when finished, so that then-unresolved names
// get replaced by their correct versions.
window.resolvePlayerNames = function() {
if(window.playersToResolve.length === 0) return;
var p = window.playersToResolve;
var d = {guids: p};
playersInResolving = window.playersInResolving.concat(p);
playersToResolve = [];
postAjax('getPlayersByGuids', d, function(dat) {
$.each(dat.result, function(ind, player) {
window.setPlayerName(player.guid, player.nickname);
// remove from array
window.playersInResolving.splice(window.playersInResolving.indexOf(player.guid), 1);
});
if(window.selectedPortal)
window.renderPortalDetails(window.selectedPortal);
},
function() {
// append failed resolves to the list again
console.warn('resolving player guids failed: ' + p.join(', '));
window.playersToResolve.concat(p);
});
}
window.setPlayerName = function(guid, nick) {
localStorage[guid] = nick;
}
window.loadPlayerNamesForPortal = function(portal_details) {
if(map.getZoom() < PRECACHE_PLAYER_NAMES_ZOOM) return;
var e = portal_details;
if(e.captured && e.captured.capturingPlayerId)
getPlayerName(e.captured.capturingPlayerId);
if(!e.resonatorArray || !e.resonatorArray.resonators) return;
$.each(e.resonatorArray.resonators, function(ind, reso) {
if(reso) getPlayerName(reso.ownerGuid);
});
}
// DEBUGGING TOOLS ///////////////////////////////////////////////////
// meant to be used from browser debugger tools and the like.
window.debug = function() {}
window.debug.renderDetails = function() {
console.log('portals: ' + Object.keys(portals).length);
console.log('links: ' + Object.keys(links).length);
console.log('fields: ' + Object.keys(fields).length);
}
window.debug.printStackTrace = function() {
var e = new Error('dummy');
console.log(e.stack);
}
window.debug.clearPortals = function() {
for(var i = 0; i < portalsLayers.length; i++)
portalsLayers[i].clearLayers();
}
window.debug.clearLinks = function() {
linksLayer.clearLayers();
}
window.debug.clearFields = function() {
fieldsLayer.clearLayers();
}
window.debug.getFields = function() {
return fields;
}
window.debug.forceSync = function() {
localStorage.clear();
window.playersToResolve = [];
window.playersInResolving = [];
debug.clearFields();
debug.clearLinks();
debug.clearPortals();
updateGameScore();
requestData();
}
// GEOSEARCH /////////////////////////////////////////////////////////
window.setupGeosearch = function() {
$('#geosearch').keypress(function(e) {
if((e.keyCode ? e.keyCode : e.which) != 13) return;
$.getJSON(NOMINATIM + encodeURIComponent($(this).val()), function(data) {
if(!data || !data[0]) return;
var b = data[0].boundingbox;
if(!b) return;
var southWest = new L.LatLng(b[0], b[2]),
northEast = new L.LatLng(b[1], b[3]),
bounds = new L.LatLngBounds(southWest, northEast);
window.map.fitBounds(bounds);
});
e.preventDefault();
});
}
// PORTAL DETAILS TOOLS //////////////////////////////////////////////
// hand any of these functions the details-hash of a portal, and they
// will return useful, but raw data.
// returns a float. Displayed portal level is always rounded down from
// that value.
window.getPortalLevel = function(d) {
var lvl = 0;
var hasReso = false;
$.each(d.resonatorArray.resonators, function(ind, reso) {
if(!reso) return true;
lvl += parseInt(reso.level);
hasReso = true;
});
return hasReso ? Math.max(1, lvl/8) : 0;
}
window.getTotalPortalEnergy = function(d) {
var nrg = 0;
$.each(d.resonatorArray.resonators, function(ind, reso) {
if(!reso) return true;
var level = parseInt(reso.level);
var max = RESO_NRG[level];
nrg += max;
});
return nrg;
}
// For backwards compatibility
window.getPortalEnergy = window.getTotalPortalEnergy;
window.getCurrentPortalEnergy = function(d) {
var nrg = 0;
$.each(d.resonatorArray.resonators, function(ind, reso) {
if(!reso) return true;
nrg += parseInt(reso.energyTotal);
});
return nrg;
}
window.getPortalRange = function(d) {
// formula by the great gals and guys at
// http://decodeingress.me/2012/11/18/ingress-portal-levels-and-link-range/
var lvl = 0;
var resoMissing = false;
$.each(d.resonatorArray.resonators, function(ind, reso) {
if(!reso) {
resoMissing = true;
return false;
}
lvl += parseInt(reso.level);
});
if(resoMissing) return 0;
return 160*Math.pow(getPortalLevel(d), 4);
}
window.getAvgResoDist = function(d) {
var sum = 0, resos = 0;
$.each(d.resonatorArray.resonators, function(ind, reso) {
if(!reso) return true;
sum += parseInt(reso.distanceToPortal);
resos++;
});
return sum/resos;
}
} // end of wrapper
// inject code into site context
var script = document.createElement('script');
script.appendChild(document.createTextNode('('+ wrapper +')();'));
(document.body || document.head || document.documentElement).appendChild(script);