'
- + '';
-
-// putting everything in a wrapper function that in turn is placed in a
-// script tag on the website allows us to execute in the site’s 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 isn’t 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. It’s 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 don’t
-// 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.
-//
-// Here’s 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 what’s 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 don’t
- // 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 don’t 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 = 'map status: ';
- if(mapRunsUserAction)
- t += 'paused during interaction';
- else if(isIdle())
- t += 'Idle, not updating.';
- else if(window.activeRequests.length > 0)
- t += window.activeRequests.length + ' requests running.';
- else
- t += 'Up to date.';
-
- if(renderLimitReached())
- t += ' RENDER LIMIT '
-
- if(window.failedRequestCount > 0)
- t += ' ' + window.failedRequestCount + ' failed.'
-
- t += ' (';
- 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(
- '