diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..6a70ed57
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+iitc-debug.user.js
diff --git a/NEWS.md b/NEWS.md
index 38f289be..69e63666 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,46 @@
-CHANGES in 0.4
-==============
+CHANGES IN 0.5 / 0.51
+=====================
+
+- Feature: draw resonators on map on high zoom levels (by Xelio)
+- Feature: show AP if portal is taken down (by Pirozek)
+- Feature: collapsible sidebar (by cmrn)
+- Feature: fields connected to portal (by phoenixsong6)
+- Feature: Permalink feature
+- Feature: chat now more copy&paste friendly (by scrool)
+- Feature: display max. energy for portal (by scrool)
+- Feature: auto-reload if page states your account is not enabled for
+ Ingress
+- Feature: You are now alerted if some of the resources fail to load
+- Change: portal level should now stand out better against more
+ backgrouds (by jonatkins)
+- Change: increased hack range from 35m to 40m
+- Change: Sidebar now semi-transparent, just like chat (by cmrn)
+- Change: portals are now sizes according to their level (by OshiHidra)
+- Change: resonators are now more aligned to their octant (thanks
+ Worros for helping confirming the slot-to-octant matching)
+- Change: release versions are now put in `dist/`. This should avoid
+ update issues in the future.
+- Bugfix: entities would be drawn again if they were hidden while using
+ the map
+- Bugfix: Python 3+ now required for building to fix encoding issues
+- Bugfix: portal mod rendering of unclaimed portals wrong
+- Bugfix: chat/sidebar arrows have gap or overlap (by mledoze)
+- Bugfix: entities invisible after hiding some of them
+- Bugfix: some portals in automated view were not clickable
+- Bugfix: resonators + nicks were sometimes misaligned
+- Bugfix: portal mod boxes sometimes misaligned
+- Plugin: guess-player-levels now also shows guessed level in tooltip
+
+
+Additional fixes in 0.51:
+- Bugfix: sidebar not visible in Chrome
+- Bugfix: layer chooser not usable if sidebar collapsed
+- Bugfix: range link not working
+
+
+
+CHANGES IN 0.4
+--------------
- Feature: display resonator charge percentage in tooltip (by Xelio)
- Feature: display resonator level in reso bar (by JasonMillward)
diff --git a/README.md b/README.md
index 3fcfabbb..c6b6c2a5 100644
--- a/README.md
+++ b/README.md
@@ -37,9 +37,9 @@ Features
Install
-------
-Current version is 0.4. See [NEWS.md](https://github.com/breunigs/ingress-intel-total-conversion/blob/gh-pages/NEWS.md) for details.
+Current version is 0.51. See [NEWS.md](https://github.com/breunigs/ingress-intel-total-conversion/blob/gh-pages/NEWS.md) for details.
-[**INSTALL**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/total-conversion-build.user.js)
+[**INSTALL**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/dist/total-conversion-build.user.js)
**Firefox:** Install [Greasemonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/) or [Scriptish](https://addons.mozilla.org/en-US/firefox/addon/scriptish/). Click install link. Install. Reload page.
@@ -60,7 +60,7 @@ Current version is 0.4. See [NEWS.md](https://github.com/breunigs/ingress-intel-
-[**INSTALL**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/total-conversion-build.user.js)
+[**INSTALL**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/dist/total-conversion-build.user.js)
Contributing
@@ -81,6 +81,7 @@ Contributors
[JasonMillward](https://github.com/JasonMillward),
[mledoze](https://github.com/mledoze),
[OshiHidra](https://github.com/OshiHidra),
+[phoenixsong6](https://github.com/phoenixsong6),
[Pirozek](https://github.com/Pirozek),
[Scrool](https://github.com/Scrool),
[sorgo](https://github.com/sorgo),
@@ -91,11 +92,13 @@ Contributors
Hacking
-------
-Execute `./build.py` to effectively concatenate `main.js` with all the files in `code/`. It generates the user script which may be installed into your browser. Do not modify `total-conversion-build.user.js` manually, because it is automatically generated. Please don’t include it in patches either, because it makes merging harder. Instead, modify the files in `code/` and have that file built for you.
+Execute `./build.py` to effectively concatenate `main.js` with all the files in `code/`. It generates the user script which may be installed into your browser. Do not modify `ttic-debug.user.js` manually, because it is automatically generated. Instead, modify the files in `code/` and have that file built for you. The files in `dist/` are for release only and should not be touched by you.
`style.css` contains most styles required for the user-script. The extra ones can be found in `code/boot.js#window.setupStyles`. Only CSS rules that depend on config variables should be defined there.
-`leaflet_google.js` contains some code to display Google Maps imagery with Leaflet, which is a slightly modified version [of this gist](https://gist.github.com/4504864). The code likely was originally written by Pavel Shramov.
+`external/leaflet_google.js` contains some code to display Google Maps imagery with Leaflet, which is a slightly modified version [of this gist](https://gist.github.com/4504864). The code likely was originally written by Pavel Shramov.
+
+`external/autolink.js` is the same file as distributed by Bryan Woods.
My dev setup is like this:
diff --git a/autobuild.sh b/autobuild.sh
index 1730aadd..398cbb12 100755
--- a/autobuild.sh
+++ b/autobuild.sh
@@ -2,7 +2,7 @@
./build.py
FORMAT=$(echo "\033[1;33m%w%f\033[0m written")
-while inotifywait -qre close_write --exclude "total-conversion-build.user.js|.git*" --format "$FORMAT" .
+while inotifywait -qre close_write --exclude "iitc-debug.user.js|.git*" --format "$FORMAT" .
do
./build.py
done
diff --git a/build.py b/build.py
index 611c86e3..2752f8e9 100755
--- a/build.py
+++ b/build.py
@@ -14,7 +14,7 @@ m = m.split('@@INJECTHERE@@')
m.insert(1, c)
t = '\n\n'.join(m)
-with open('total-conversion-build.user.js', 'w', encoding='utf8') as f:
+with open('iitc-debug.user.js', 'w', encoding='utf8') as f:
f.write(t)
# vim: ai si ts=4 sw=4 sts=4 et
diff --git a/code/boot.js b/code/boot.js
index dbd9b265..57e6ebe9 100644
--- a/code/boot.js
+++ b/code/boot.js
@@ -27,7 +27,8 @@ window.setupLargeImagePreview = function() {
window.setupStyles = function() {
$('head').append('');
+}
+
+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(''
+ + '
'+level+' '
+ + ''+PLAYER.nickname+''
+ + '
'
+ + 'XM: '+xmRatio+'%'
+ + '' + (level < 8 ? 'level: '+lvlApProg+'%' : 'max level') + ''
+ + '
'
+ + ''
+ );
+}
+
+window.setupSidebarToggle = function() {
+ $('#sidebartoggle').on('click', function() {
+ var toggle = $('#sidebartoggle');
+ var sidebar = $('#scrollwrapper');
+ if(sidebar.is(':visible')) {
+ sidebar.hide().css('z-index', 1);
+ $('.leaflet-right').css('margin-right','0');
+ toggle.html('');
+ toggle.css('right', '0');
+ } else {
+ sidebar.css('z-index', 1001).show();
+ $('.leaflet-right').css('margin-right', SIDEBAR_WIDTH+1+'px');
+ toggle.html('');
+ 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 . 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= 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. Let’s 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; // don’t 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 += ''+part[1].name+'';
+ break;
+ }
+ });
+
+ // nick will only be set if we don’t 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 don’t 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'], '$1');
+ }
+
+ 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 '─ '+text+' ────────────────────────────────────────────────────────────────────────────';
+}
+
+
+window.chat.renderMsg = function(msg, nick, time, team) {
+ var ta = unixTimeToHHmm(time);
+ var tb = unixTimeToString(time, true);
+ // help cursor via “#chat time”
+ var t = '';
+ var s = 'style="color:'+COLORS[team]+'"';
+ var title = nick.length >= 8 ? 'title="'+nick+'" class="help"' : '';
+ return ''+t+' <'+nick+'> '+msg+'
';
+}
+
+
+
+window.chat.getActive = function() {
+ return $('#chatcontrols .active').text();
+}
+
+
+window.chat.toggle = function() {
+ var c = $('#chat, #chatcontrols');
+ if(c.hasClass('expand')) {
+ $('#chatcontrols a:first').html('');
+ 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('');
+ 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 don’t
+ // 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',
+ ''
+ + (range > 1000
+ ? Math.round(range/1000) + ' km'
+ : Math.round(range) + ' m')
+ + ''];
+}
+
+// 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 = ''+mods[0]+''
+ + ''+mods[1]+''
+ + ''+mods[2]+''
+ + ''+mods[3]+''
+
+ 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', '' + fill + ''];
+}
+
+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 = '';
+ } 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 = ' ' + level + ' ';
+
+ var fill = '';
+
+ var meter = '' + fill + lbar + '';
+ }
+ var cls = isLeft ? 'left' : 'right';
+ var text = ''+(nick||'')+'';
+ return (isLeft ? text+meter : meter+text) + '
';
+}
+
+// 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 '' + digits(text) + '';
+ }
+
+ 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 = ''+Math.round(rp)+'% ';
+ var es = ' '+Math.round(ep)+'%';
+ $('#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 that’s 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 ''+t+''; }
+ var linksText = [linkExpl('links'), linkExpl(' ↳ ' + links.incoming+' • '+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 = '';
+ 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(''
+ + ''+d.portalV2.descriptiveText.TITLE+'
'
+ // help cursor via “.imgpreview img”
+ + ''
+ + ''+Math.floor(getPortalLevel(d))+''
+ + ''+getModDetails(d)+'
'
+ + ''+randDetails+'
'
+ + ''+getResonatorDetails(d)+'
'
+ + ''
+ );
+ }
+
+ // try to resolve names that were required for above functions, but
+ // weren’t 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 it’s 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 isn’t 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);
diff --git a/main.js b/main.js
index 31bd434e..96f0a63c 100644
--- a/main.js
+++ b/main.js
@@ -1,10 +1,10 @@
// ==UserScript==
// @id ingress-intel-total-conversion@breunigs
// @name intel map total conversion
-// @version 0.4-@@BUILDDATE@@
+// @version 0.51-@@BUILDDATE@@
// @namespace https://github.com/breunigs/ingress-intel-total-conversion
-// @updateURL https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/total-conversion-build.user.js
-// @downloadURL https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/total-conversion-build.user.js
+// @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*
@@ -62,7 +62,7 @@ document.getElementsByTagName('head')[0].innerHTML = ''
document.getElementsByTagName('body')[0].innerHTML = ''
+ 'Loading, please wait
'
+ ''
+ ''
+ '
'
@@ -70,7 +70,7 @@ document.getElementsByTagName('body')[0].innerHTML = ''
+ '
'
+ '
'
+ ''
- + ''
+ + ''
+ '' // enable scrolling for small screens
+ ' '
+ + '
'
+ + '';
// 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
@@ -130,19 +143,31 @@ var RANGE_INDICATOR_COLOR = 'red';
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 = 35; // in meters, max. distance from portal to be able to access it
+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
@@ -163,6 +188,7 @@ var portalsLayers, linksLayer, fieldsLayer;
window.portals = {};
window.links = {};
window.fields = {};
+window.resonators = {};
// plugin framework. Plugins may load earlier than iitc, so don’t
// overwrite data
@@ -171,6 +197,55 @@ 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
@@ -242,8 +317,16 @@ window.handleDataResponse = function(data, textStatus, jqXHR) {
// 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);
});
@@ -265,17 +348,37 @@ window.handleDataResponse = function(data, textStatus, jqXHR) {
ppp.push(ent); // delay portal render
- } else if(ent[2].edge !== undefined)
+ } else if(ent[2].edge !== undefined) {
renderLink(ent);
- else if(ent[2].capturedRegion !== undefined)
+ } 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
+ } 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]) portals[selectedPortal].bringToFront();
+ if(portals[selectedPortal]) {
+ try {
+ portals[selectedPortal].bringToFront();
+ } catch(e) { /* portal is now visible, catch Leaflet error */ }
+ }
if(portalInUrlAvailable) {
renderPortalDetails(urlPortal);
@@ -294,13 +397,16 @@ window.cleanUp = function() {
var minlvl = getMinPortalLevel();
for(var i = 0; i < portalsLayers.length; i++) {
// i is also the portal level
- portalsLayers[i].eachLayer(function(portal) {
+ 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(portal.options.guid == window.selectedPortal ||
- (b.contains(portal.getLatLng()) && i >= minlvl)) return;
+ if(itemGuid == window.selectedPortal ||
+ (b.contains(item.getLatLng()) && i >= minlvl)) return true;
cnt[0]++;
- portalsLayers[i].removeLayer(portal);
+ portalsLayers[i].removeLayer(item);
});
}
linksLayer.eachLayer(function(link) {
@@ -316,31 +422,30 @@ window.cleanUp = function() {
console.log('removed out-of-bounds: '+cnt[0]+' portals, '+cnt[1]+' links, '+cnt[2]+' fields');
}
+
// removes given entity from map
window.removeByGuid = 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
- switch(guid.slice(33)) {
- case '11':
- case '12':
+ 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 '9':
+ case TYPE_LINK:
if(!window.links[guid]) return;
linksLayer.removeLayer(window.links[guid]);
break;
- case 'b':
+ 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();
@@ -351,59 +456,148 @@ window.removeByGuid = function(guid) {
// renders a portal on the map from the given entity
window.renderPortal = function(ent) {
- removeByGuid(ent[0]);
-
if(Object.keys(portals).length >= MAX_DRAWN_PORTALS && ent[0] != selectedPortal)
- return;
-
- var latlng = [ent[2].locationE6.latE6/1E6, ent[2].locationE6.lngE6/1E6];
- // needs to be checked before, so the portal isn’t added to the
- // details list and other places
- //if(!getPaddedBounds().contains(latlng)) return;
+ return removeByGuid(ent[0]);
// hide low level portals on low zooms
var portalLevel = getPortalLevel(ent[2]);
- if(portalLevel < getMinPortalLevel() && ent[0] != selectedPortal) return;
-
- // pre-load player names for high zoom levels
- if(map.getZoom() >= PRECACHE_PLAYER_NAMES_ZOOM) {
- if(ent[2].captured && ent[2].captured.capturingPlayerId)
- getPlayerName(ent[2].captured.capturingPlayerId);
- if(ent[2].resonatorArray && ent[2].resonatorArray.resonators)
- $.each(ent[2].resonatorArray.resonators, function(ind, reso) {
- if(reso) getPlayerName(reso.ownerGuid);
- });
- }
+ 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: 7,
+ radius: lvRadius,
color: ent[0] == selectedPortal ? COLOR_SELECTED_PORTAL : COLORS[team],
opacity: 1,
- weight: 3,
+ weight: lvWeight,
fillColor: COLORS[team],
fillOpacity: 0.5,
clickable: true,
level: portalLevel,
+ team: team,
details: ent[2],
guid: ent[0]});
- p.on('remove', function() { delete window.portals[this.options.guid]; });
- p.on('add', function() {
+ 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(portalsLayers[parseInt(portalLevel)]);
+ 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) {
@@ -412,8 +606,12 @@ window.portalResetColor = function(portal) {
// renders a link on the map from the given entity
window.renderLink = function(ent) {
- removeByGuid(ent[0]);
- if(Object.keys(links).length >= MAX_DRAWN_LINKS) return;
+ 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;
@@ -433,14 +631,23 @@ window.renderLink = function(ent) {
if(!getPaddedBounds().intersects(poly.getBounds())) return;
poly.on('remove', function() { delete window.links[this.options.guid]; });
- poly.on('add', function() { window.links[this.options.guid] = this; });
- poly.addTo(linksLayer).bringToBack();
+ 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) {
- window.removeByGuid(ent[0]);
- if(Object.keys(fields).length >= MAX_DRAWN_FIELDS) return;
+ 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;
@@ -455,13 +662,40 @@ window.renderField = function(ent) {
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() { window.fields[this.options.guid] = this; });
- poly.addTo(fieldsLayer).bringToBack();
+ 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;
}
@@ -517,7 +751,7 @@ window.renderUpdateStatus = function() {
t += ' RENDER LIMIT '
if(window.failedRequestCount > 0)
- t += ' ' + window.failedRequestCount + ' requests failed.'
+ t += ' ' + window.failedRequestCount + ' failed.'
t += '
(';
var minlvl = getMinPortalLevel();
@@ -677,6 +911,13 @@ window.rangeLinkClick = function() {
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';
@@ -718,8 +959,51 @@ window.scrollBottom = function(elm) {
}
window.zoomToAndShowPortal = function(guid, latlng) {
- renderPortalDetails(guid);
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() {
@@ -733,6 +1017,24 @@ if (typeof String.prototype.startsWith !== 'function') {
};
}
+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;
+ });
+}
+
@@ -763,16 +1065,18 @@ window.setupLargeImagePreview = function() {
window.setupStyles = function() {
$('head').append('');
}
@@ -829,7 +1133,22 @@ window.setupMap = function() {
map.attributionControl.setPrefix('');
// listen for changes and store them in cookies
map.on('moveend', window.storeMapPosition);
- map.on('zoomend', 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());
});
@@ -890,6 +1209,24 @@ window.setupPlayerStat = function() {
);
}
+window.setupSidebarToggle = function() {
+ $('#sidebartoggle').on('click', function() {
+ var toggle = $('#sidebartoggle');
+ var sidebar = $('#scrollwrapper');
+ if(sidebar.is(':visible')) {
+ sidebar.hide().css('z-index', 1);
+ $('.leaflet-right').css('margin-right','0');
+ toggle.html('');
+ toggle.css('right', '0');
+ } else {
+ sidebar.css('z-index', 1001).show();
+ $('.leaflet-right').css('margin-right', SIDEBAR_WIDTH+1+'px');
+ toggle.html('');
+ toggle.css('right', SIDEBAR_WIDTH+1+'px');
+ }
+ });
+}
+
// BOOTING ///////////////////////////////////////////////////////////
@@ -900,6 +1237,7 @@ function boot() {
window.setupGeosearch();
window.setupRedeem();
window.setupLargeImagePreview();
+ window.setupSidebarToggle();
window.updateGameScore();
window.setupPlayerStat();
window.chat.setup();
@@ -928,10 +1266,10 @@ function asyncLoadScript(a){return function(b,c){var d=document.createElement("s
// 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/external/leaflet_google.js';
+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/external/autolink.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) {
@@ -1397,7 +1735,7 @@ window.chat.renderMsg = function(msg, nick, time, team) {
var t = '';
var s = 'style="color:'+COLORS[team]+'"';
var title = nick.length >= 8 ? 'title="'+nick+'" class="help"' : '';
- return ''+t+''+nick+''+msg+'
';
+ return ''+t+' <'+nick+'> '+msg+'
';
}
@@ -1410,14 +1748,16 @@ window.chat.getActive = function() {
window.chat.toggle = function() {
var c = $('#chat, #chatcontrols');
if(c.hasClass('expand')) {
- $('#chatcontrols a:first').text('expand');
+ $('#chatcontrols a:first').html('');
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').text('shrink');
+ $('#chatcontrols a:first').html('');
c.addClass('expand');
+ $('.leaflet-control').css('margin-left', '720px');
chat.needMoreMessages();
}
}
@@ -1635,12 +1975,12 @@ window.chat.postMsg = function() {
// returns displayable text+link about portal range
window.getRangeText = function(d) {
var range = getPortalRange(d);
- return 'range: '
- + ''
+ return ['range',
+ ''
+ (range > 1000
? Math.round(range/1000) + ' km'
: Math.round(range) + ' m')
- + '';
+ + ''];
}
// generates description text from details for portal
@@ -1694,30 +2034,31 @@ window.getModDetails = function(d) {
}
window.getEnergyText = function(d) {
- var nrg = getPortalEnergy(d);
- return 'energy: ' + (nrg > 1000 ? Math.round(nrg/1000) +' k': nrg);
+ var currentNrg = getCurrentPortalEnergy(d);
+ var totalNrg = getTotalPortalEnergy(d);
+ var inf = currentNrg + ' / ' + totalNrg;
+ var fill = prettyEnergy(currentNrg) + ' / ' + prettyEnergy(totalNrg)
+ return ['energy', '' + fill + ''];
}
window.getAvgResoDistText = function(d) {
var avgDist = Math.round(10*getAvgResoDist(d))/10;
- return '⌀ res dist: ' + avgDist + ' m';
-}
-
-window.getReportIssueInfoText = function(d) {
- return ('Your Nick: '+PLAYER.nickname+' '
- + 'Portal: '+d.portalV2.descriptiveText.TITLE+' '
- + 'Location: '+d.portalV2.descriptiveText.ADDRESS
- +' (lat '+(d.locationE6.latE6/1E6)+'; lng '+(d.locationE6.lngE6/1E6)+')'
- ).replace(/['"]/, '');
+ return ['⌀ res dist', avgDist + ' m'];
}
window.getResonatorDetails = function(d) {
- console.log('rendering reso details');
var resoDetails = '';
- var slotsFilled = 0;
- $.each(d.resonatorArray.resonators, function(ind, reso) {
+ // 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(slotsFilled++, 0);
+ resoDetails += renderResonatorDetails(slot, 0, 0, null, null, isLeft);
return true;
}
@@ -1725,9 +2066,11 @@ window.getResonatorDetails = function(d) {
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);
- slotsFilled++;
- resoDetails += renderResonatorDetails(parseInt(reso.slot), l, v, dist, nick);
+ resoDetails += renderResonatorDetails(slot, l, v, dist, nick, isLeft);
});
return resoDetails;
}
@@ -1736,15 +2079,15 @@ window.getResonatorDetails = function(d) {
// 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) {
- if(level == 0) {
- var meter = '';
+window.renderResonatorDetails = function(slot, level, nrg, dist, nick, isLeft) {
+ if(level === 0) {
+ var meter = '';
} 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'
+ 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];
@@ -1757,12 +2100,41 @@ window.renderResonatorDetails = function(slot, level, nrg, dist, nick) {
var fill = '';
- var meter = ''
- + fill + lbar + '';
+ var meter = '' + fill + lbar + '';
}
- var cls = slot <= 3 ? 'left' : 'right';
+ var cls = isLeft ? 'left' : 'right';
var text = ''+(nick||'')+'';
- return (slot <= 3 ? text+meter : meter+text) + '
';
+ return (isLeft ? text+meter : meter+text) + '
';
+}
+
+// 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 '' + digits(text) + '';
+ }
+
+ return [tt('AP Gain'), tt(sum)];
}
@@ -1977,22 +2349,23 @@ window.renderPortalDetails = function(guid) {
links[link.isOrigin ? 'outgoing' : 'incoming']++;
});
function linkExpl(t) { return ''+t+''; }
- var linksText = linkExpl('links')+':'+linkExpl(' ↳ ' + links.incoming+' • '+links.outgoing+' ↴');
+ var linksText = [linkExpl('links'), linkExpl(' ↳ ' + links.incoming+' • '+links.outgoing+' ↴')];
var player = d.captured && d.captured.capturingPlayerId
? getPlayerName(d.captured.capturingPlayerId)
: null;
- var playerText = player ? 'owner: ' + player : null;
+ var playerText = player ? ['owner', player] : null;
var time = d.captured ? unixTimeToString(d.captured.capturedTime) : null;
- var sinceText = time ? 'since: ' + time : 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)];
+ var randDetails = [playerText, sinceText, getRangeText(d), getEnergyText(d), linksText, getAvgResoDistText(d), linkedFields, getDestroyAP(d)];
randDetails = randDetails.map(function(detail) {
if(!detail) return '';
- detail = detail.split(':');
- detail = '';
+ detail = '';
return detail;
}).join('\n');
@@ -2026,7 +2399,7 @@ window.renderPortalDetails = function(guid) {
+ ''+getResonatorDetails(d)+'
'
+ ''
);
}
@@ -2188,6 +2561,21 @@ window.setPlayerName = function(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.
@@ -2273,7 +2661,21 @@ window.getPortalLevel = function(d) {
return hasReso ? Math.max(1, lvl/8) : 0;
}
-window.getPortalEnergy = function(d) {
+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;