diff --git a/NEWS.md b/NEWS.md
index 8c8b8f99..38f289be 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,6 +1,26 @@
-CHANGES IN 0.3
+CHANGES in 0.4
==============
+- Feature: display resonator charge percentage in tooltip (by Xelio)
+- Feature: display resonator level in reso bar (by JasonMillward)
+- Feature: portals may be filtered by level (using the layer switcher)
+- Feature: build script in Python (by epf)
+- Feature: plugins
+- Change: Portal mods are colored according to their rare-ness (by OshiHidra)
+- Change: nick highlight in chat now case-insensitive
+- Change: +/- zoom buttons visible by default now
+- Bugfix: title bar text broken
+- Bugfix: rename cardinal to octant (by mledoze)
+- Bugfix: Chat display broken in Opera
+- Bugfix: Chat tab completion in Chrome
+- Bugfix: wrong timestamps displayed in chat input bar
+- Bugfix: don’t autobuild when git meta info changes (by ZauberNerd)
+- Bugfix: resistance owned portals had wrong border when viewing the full image
+
+
+CHANGES IN 0.3
+--------------
+
- Feature: more info for shields in tooltip (by JasonMillward)
- Feature: pretty display for redeemed codes (by integ3r)
- Change: Portal details are now scrollable when height is too small
diff --git a/README.md b/README.md
index 156d85d0..ba4c719d 100644
--- a/README.md
+++ b/README.md
@@ -31,20 +31,14 @@ Features
- may toggle portals/links/fields
- hack range (yellow circle) and link range (large red circle) for portals. Click on the range link in the sidebar to zoom to link range.
- double clicking a portal zooms in and focuses it
-- display of XM and AP rewards for redeemed passcodes by [Dovahkiin](http://bit.ly/mjcode)
-
-
-Missing
--------
-
-(and probably not going to implement it)
-
-- logout link (but you wouldn’t want to *quit*, would you?), privacy link, etc.
+- display of XM and AP rewards for redeemed passcodes
Install
-------
+Current version is 0.4. 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)
@@ -69,11 +63,10 @@ Install
[**INSTALL**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/total-conversion-build.user.js)
-
Contributing
------------
-Please do!
+Please do!
(Obviously, Resistance folks must send in complete patches while Enlightenment gals and guys may just open feature request ☺)
@@ -81,16 +74,23 @@ Please do!
Contributors
------------
-- [integ3r](https://github.com/integ3r)
-- [Bananeweizen](https://github.com/Bananeweizen)
-- [epf](https://github.com/epf)
-- [JasonMillward](https://github.com/JasonMillward)
+[Bananeweizen](https://github.com/Bananeweizen),
+[cmrn](https://github.com/cmrn),
+[epf](https://github.com/epf),
+[integ3r](https://github.com/integ3r),
+[JasonMillward](https://github.com/JasonMillward),
+[mledoze](https://github.com/mledoze),
+[OshiHidra](https://github.com/OshiHidra),
+[Scrool](https://github.com/Scrool),
+[sorgo](https://github.com/sorgo),
+[Xelio](https://github.com/Xelio),
+[ZauberNerd](https://github.com/ZauberNerd)
Hacking
-------
-Execute `./build.rb` or `./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 `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.
`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.
diff --git a/autobuild.sh b/autobuild.sh
index df151d1a..1730aadd 100755
--- a/autobuild.sh
+++ b/autobuild.sh
@@ -1,8 +1,8 @@
#!/bin/sh
-./build.rb
+./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" .
do
- ./build.rb
+ ./build.py
done
diff --git a/build.py b/build.py
old mode 100644
new mode 100755
index b690a2ed..611c86e3
--- a/build.py
+++ b/build.py
@@ -1,10 +1,10 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
import glob
import time
def readfile(fn):
- with open(fn, 'Ur') as f:
+ with open(fn, 'Ur', encoding='utf8') as f:
return f.read()
c = '\n\n'.join(map(readfile, glob.glob('code/*')))
@@ -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') as f:
+with open('total-conversion-build.user.js', 'w', encoding='utf8') as f:
f.write(t)
# vim: ai si ts=4 sw=4 sts=4 et
diff --git a/build.rb b/build.rb
deleted file mode 100755
index 81789aea..00000000
--- a/build.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env ruby
-# encoding: utf-8
-
-c = Dir.glob('code/*').map { |f| File.read(f) }
-n = Time.now.strftime('%Y-%m-%d-%H%M%S')
-m = File.read('main.js').gsub('@@BUILDDATE@@', n)
-m = m.split('@@INJECTHERE@@')
-t = m.insert(1, c).flatten.join("\n\n")
-File.open('total-conversion-build.user.js', 'w') {|f| f.write(t) }
diff --git a/code/boot.js b/code/boot.js
index 8ebce5b0..dbd9b265 100644
--- a/code/boot.js
+++ b/code/boot.js
@@ -27,16 +27,17 @@ window.setupLargeImagePreview = function() {
window.setupStyles = function() {
$('head').append('');
}
@@ -93,7 +94,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());
});
@@ -154,6 +170,24 @@ window.setupPlayerStat = function() {
);
}
+window.setupSidebarToggle = function() {
+ $('#sidebartoggle').on('click', function() {
+ var toggle = $('#sidebartoggle');
+ var sidebar = $('#sidebar');
+ if(sidebar.is(':visible')) {
+ sidebar.hide();
+ $('.leaflet-right').css('margin-right','0');
+ toggle.html('◢ ◥');
+ toggle.css('right', '0');
+ } else {
+ sidebar.show();
+ $('.leaflet-right').css('margin-right', SIDEBAR_WIDTH+1+'px');
+ toggle.html('◣ ◤');
+ toggle.css('right', SIDEBAR_WIDTH+1+'px');
+ }
+ });
+}
+
// BOOTING ///////////////////////////////////////////////////////////
@@ -164,6 +198,7 @@ function boot() {
window.setupGeosearch();
window.setupRedeem();
window.setupLargeImagePreview();
+ window.setupSidebarToggle();
window.updateGameScore();
window.setupPlayerStat();
window.chat.setup();
@@ -176,6 +211,11 @@ function boot() {
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
@@ -187,10 +227,12 @@ 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/leaflet_google.js';
+var LLGMAPS = 'http://breunigs.github.com/ingress-intel-total-conversion/external/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://raw.github.com/bryanwoods/autolink-js/master/autolink.js';
+var AUTOLINK = 'http://breunigs.github.com/ingress-intel-total-conversion/external/autolink.js';
// after all scripts have loaded, boot the actual app
-load(JQUERY, LEAFLET, AUTOLINK).then(LLGMAPS).thenRun(boot);
+load(JQUERY, LEAFLET, AUTOLINK).then(LLGMAPS).onError(function (err) {
+ alert('Could not all resources, the script likely won’t work.\n\nIf this happend the first time for you, it’s probably a temporary issue. Just wait a bit and try again.\n\nIf you installed the script for the first time and this happens:\n– try disabling NoScript if you have it installed\n– press CTRL+SHIFT+K in Firefox or CTRL+SHIFT+I in Chrome/Opera and reload the page. Additional info may be available in the console.\n– Open an issue at https://github.com/breunigs/ingress-intel-total-conversion/issues');
+}).thenRun(boot);
diff --git a/code/chat.js b/code/chat.js
index 0ece265f..8e44a562 100644
--- a/code/chat.js
+++ b/code/chat.js
@@ -456,7 +456,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+'
';
}
@@ -469,14 +469,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').text('◢◣');
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').text('◥◤');
c.addClass('expand');
+ $('.leaflet-control').css('margin-left', '720px');
chat.needMoreMessages();
}
}
diff --git a/code/hooks.js b/code/hooks.js
new file mode 100644
index 00000000..f5b90d0b
--- /dev/null
+++ b/code/hooks.js
@@ -0,0 +1,47 @@
+// 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);
+}
diff --git a/code/map_data.js b/code/map_data.js
index 03739be4..bcd8ba11 100644
--- a/code/map_data.js
+++ b/code/map_data.js
@@ -69,8 +69,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);
});
@@ -92,17 +100,34 @@ 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(p2f[portal[0]] === undefined)
+ portal[2].portalV2['linkedFields'] = [];
+ else
+ portal[2].portalV2['linkedFields'] = $.unique(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);
@@ -121,13 +146,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) {
@@ -143,31 +171,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();
@@ -178,59 +205,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) {
@@ -239,8 +355,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;
@@ -260,14 +380,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;
@@ -282,11 +411,38 @@ 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;
}
diff --git a/code/player_names.js b/code/player_names.js
index 47caed80..9bbc251e 100644
--- a/code/player_names.js
+++ b/code/player_names.js
@@ -46,3 +46,18 @@ window.resolvePlayerNames = function() {
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);
+ });
+}
diff --git a/code/portal_detail_display.js b/code/portal_detail_display.js
index d9d308d9..d1cc877d 100644
--- a/code/portal_detail_display.js
+++ b/code/portal_detail_display.js
@@ -29,8 +29,10 @@ window.renderPortalDetails = function(guid) {
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)];
+ var randDetails = [playerText, sinceText, getRangeText(d), getEnergyText(d), linksText, getAvgResoDistText(d), linkedFields];
randDetails = randDetails.map(function(detail) {
if(!detail) return '';
detail = detail.split(':');
@@ -69,7 +71,7 @@ window.renderPortalDetails = function(guid) {
+ '
'+getDestroyAP(d)+'
'
+ '
'
+ ''
- + ''
+ + ''
+ '
'
);
}
diff --git a/code/portal_detail_display_tools.js b/code/portal_detail_display_tools.js
index 4e082670..ee5edb56 100644
--- a/code/portal_detail_display_tools.js
+++ b/code/portal_detail_display_tools.js
@@ -29,10 +29,12 @@ window.getPortalDescriptionFromDetails = function(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';
@@ -46,23 +48,29 @@ window.getModDetails = function(d) {
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]+''
+ var t = ''+mods[0]+''
+ + ''+mods[1]+''
+ + ''+mods[2]+''
+ + ''+mods[3]+''
return t;
}
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)
+ var meter = 'energy: ' + fill + '';
+ return meter;
}
window.getAvgResoDistText = function(d) {
@@ -70,21 +78,19 @@ window.getAvgResoDistText = function(d) {
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(/['"]/, '');
-}
-
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;
}
@@ -92,9 +98,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;
}
@@ -103,15 +111,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];
@@ -124,12 +132,11 @@ 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
diff --git a/code/portal_info.js b/code/portal_info.js
index c75d748d..edf8298d 100644
--- a/code/portal_info.js
+++ b/code/portal_info.js
@@ -17,7 +17,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;
diff --git a/code/request_handling.js b/code/request_handling.js
index 60aa00f6..042fc397 100644
--- a/code/request_handling.js
+++ b/code/request_handling.js
@@ -50,7 +50,7 @@ window.renderUpdateStatus = function() {
t += ' RENDER LIMIT '
if(window.failedRequestCount > 0)
- t += ' ' + window.failedRequestCount + ' requests failed.'
+ t += ' ' + window.failedRequestCount + ' failed.'
t += ' (';
var minlvl = getMinPortalLevel();
diff --git a/code/utils_misc.js b/code/utils_misc.js
index 3fca9f17..75cd1d72 100644
--- a/code/utils_misc.js
+++ b/code/utils_misc.js
@@ -93,6 +93,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';
@@ -134,8 +141,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() {
@@ -148,3 +198,15 @@ if (typeof String.prototype.startsWith !== 'function') {
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);
+}
diff --git a/external/autolink.js b/external/autolink.js
new file mode 100644
index 00000000..013025a7
--- /dev/null
+++ b/external/autolink.js
@@ -0,0 +1,33 @@
+// Generated by CoffeeScript 1.4.0
+(function() {
+ var autoLink,
+ __slice = [].slice;
+
+ autoLink = function() {
+ var callbackThunk, key, link_attributes, option, options, url_pattern, value;
+ options = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+ link_attributes = '';
+ option = options[0];
+ url_pattern = /(^|\s)(\b(https?|ftp):\/\/[\-A-Z0-9+\u0026@#\/%?=~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~_|])/gi;
+ if (!(options.length > 0)) {
+ return this.replace(url_pattern, "$1$2");
+ }
+ if ((option['callback'] != null) && typeof option['callback'] === 'function') {
+ callbackThunk = option['callback'];
+ delete option['callback'];
+ }
+ for (key in option) {
+ value = option[key];
+ link_attributes += " " + key + "='" + value + "'";
+ }
+ return this.replace(url_pattern, function(match, space, url) {
+ var link, returnCallback;
+ returnCallback = callbackThunk && callbackThunk(url);
+ link = returnCallback || ("" + url + "");
+ return "" + space + link;
+ });
+ };
+
+ String.prototype['autoLink'] = autoLink;
+
+}).call(this);
diff --git a/external/leaflet_google.js b/external/leaflet_google.js
new file mode 100644
index 00000000..37d6dd8c
--- /dev/null
+++ b/external/leaflet_google.js
@@ -0,0 +1,152 @@
+/*
+ * L.TileLayer is used for standard xyz-numbered tile layers.
+ */
+L.Google = L.Class.extend({
+ includes: L.Mixin.Events,
+
+ options: {
+ minZoom: 0,
+ maxZoom: 18,
+ tileSize: 256,
+ subdomains: 'abc',
+ errorTileUrl: '',
+ attribution: '',
+ opacity: 1,
+ continuousWorld: false,
+ noWrap: false,
+ },
+
+ // Possible types: SATELLITE, ROADMAP, HYBRID, INGRESS
+ initialize: function(type, options, styles) {
+ L.Util.setOptions(this, options);
+ if(type === 'INGRESS') {
+ type = 'ROADMAP';
+ this._styles = [{featureType:"all", elementType:"all", stylers:[{visibility:"on"}, {hue:"#0091ff"}, {invert_lightness:true}]}, {featureType:"water", elementType:"all", stylers:[{visibility:"on"}, {hue:"#005eff"}, {invert_lightness:true}]}, {featureType:"poi", stylers:[{visibility:"off"}]}, {featureType:"transit", elementType:"all", stylers:[{visibility:"off"}]}];
+ } else {
+ this._styles = null;
+ }
+ this._type = google.maps.MapTypeId[type || 'SATELLITE'];
+ },
+
+ onAdd: function(map, insertAtTheBottom) {
+ this._map = map;
+ this._insertAtTheBottom = insertAtTheBottom;
+
+ // create a container div for tiles
+ this._initContainer();
+ this._initMapObject();
+
+ // set up events
+ map.on('viewreset', this._resetCallback, this);
+
+ this._limitedUpdate = L.Util.limitExecByInterval(this._update, 150, this);
+ map.on('move', this._update, this);
+ //map.on('moveend', this._update, this);
+
+ this._reset();
+ this._update();
+ },
+
+ onRemove: function(map) {
+ this._map._container.removeChild(this._container);
+ //this._container = null;
+
+ this._map.off('viewreset', this._resetCallback, this);
+
+ this._map.off('move', this._update, this);
+ //this._map.off('moveend', this._update, this);
+ },
+
+ getAttribution: function() {
+ return this.options.attribution;
+ },
+
+ setOpacity: function(opacity) {
+ this.options.opacity = opacity;
+ if (opacity < 1) {
+ L.DomUtil.setOpacity(this._container, opacity);
+ }
+ },
+
+ _initContainer: function() {
+ var tilePane = this._map._container
+ first = tilePane.firstChild;
+
+ if (!this._container) {
+ this._container = L.DomUtil.create('div', 'leaflet-google-layer leaflet-top leaflet-left');
+ this._container.id = "_GMapContainer";
+ }
+
+ if (true) {
+ tilePane.insertBefore(this._container, first);
+
+ this.setOpacity(this.options.opacity);
+ var size = this._map.getSize();
+ this._container.style.width = size.x + 'px';
+ this._container.style.height = size.y + 'px';
+ }
+ },
+
+ _initMapObject: function() {
+ this._google_center = new google.maps.LatLng(0, 0);
+ var map = new google.maps.Map(this._container, {
+ center: this._google_center,
+ zoom: 0,
+ styles: this._styles,
+ mapTypeId: this._type,
+ disableDefaultUI: true,
+ keyboardShortcuts: false,
+ draggable: false,
+ disableDoubleClickZoom: true,
+ scrollwheel: false,
+ streetViewControl: false
+ });
+
+ var _this = this;
+ this._reposition = google.maps.event.addListenerOnce(map, "center_changed",
+ function() { _this.onReposition(); });
+
+ map.backgroundColor = '#ff0000';
+ this._google = map;
+ },
+
+ _resetCallback: function(e) {
+ this._reset(e.hard);
+ },
+
+ _reset: function(clearOldContainer) {
+ this._initContainer();
+ },
+
+ _update: function() {
+ this._resize();
+
+ var bounds = this._map.getBounds();
+ var ne = bounds.getNorthEast();
+ var sw = bounds.getSouthWest();
+ var google_bounds = new google.maps.LatLngBounds(
+ new google.maps.LatLng(sw.lat, sw.lng),
+ new google.maps.LatLng(ne.lat, ne.lng)
+ );
+ var center = this._map.getCenter();
+ var _center = new google.maps.LatLng(center.lat, center.lng);
+
+ this._google.setCenter(_center);
+ this._google.setZoom(this._map.getZoom());
+ //this._google.fitBounds(google_bounds);
+ },
+
+ _resize: function() {
+ var size = this._map.getSize();
+ if (this._container.style.width == size.x &&
+ this._container.style.height == size.y)
+ return;
+ this._container.style.width = size.x + 'px';
+ this._container.style.height = size.y + 'px';
+ google.maps.event.trigger(this._google, "resize");
+ },
+
+ onReposition: function() {
+ //google.maps.event.trigger(this._google, "resize");
+ }
+});
diff --git a/main.js b/main.js
index f36b3b70..31bd434e 100644
--- a/main.js
+++ b/main.js
@@ -1,7 +1,7 @@
// ==UserScript==
// @id ingress-intel-total-conversion@breunigs
// @name intel map total conversion
-// @version 0.3-@@BUILDDATE@@
+// @version 0.4-@@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
@@ -28,6 +28,19 @@ for(var x in scr) {
break;
}
+
+if(!d) {
+ // page doesn’t have a script tag with player information.
+ if(document.getElementById('header_email')) {
+ // however, we are logged in.
+ setTimeout('location.reload();', 10*1000);
+ throw('Page doesn’t have player data, but you are logged in. Reloading in 10s.');
+ }
+ // FIXME: handle nia takedown in progress
+ throw('Couldn’t retrieve player data. Are you logged in?');
+}
+
+
for(var i = 0; i < d.length; i++) {
if(!d[i].match('var PLAYER = ')) continue;
eval(d[i].match(/^var /, 'window.'));
@@ -36,7 +49,6 @@ for(var i = 0; i < d.length; i++) {
// player information is now available in a hash like this:
// window.PLAYER = {"ap": "123", "energy": 123, "available_invites": 123, "nickname": "somenick", "team": "ALIENS||RESISTANCE"};
-
// remove complete page. We only wanted the user-data and the page’s
// security context so we can access the API easily. Setup as much as
// possible without requiring scripts.
@@ -50,7 +62,7 @@ document.getElementsByTagName('head')[0].innerHTML = ''
document.getElementsByTagName('body')[0].innerHTML = ''
+ '