Merge remote-tracking branch 'upstream/gh-pages' into gh-pages

This commit is contained in:
Jon Atkins 2013-02-14 16:50:47 +00:00
commit 2da1db219a
38 changed files with 1163 additions and 416 deletions

View File

@ -32,6 +32,7 @@ Features
- 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. - 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 - double clicking a portal zooms in and focuses it
- display of XM and AP rewards for redeemed passcodes - display of XM and AP rewards for redeemed passcodes
- [extend it even more with the use of plugins](https://github.com/breunigs/ingress-intel-total-conversion/plugins)
Install Install
@ -70,9 +71,7 @@ Please do!
(Obviously, Resistance folks must send in complete patches while Enlightenment gals and guys may just open feature request ☺) (Obviously, Resistance folks must send in complete patches while Enlightenment gals and guys may just open feature request ☺)
**So far, these people have contributed:**
Contributors
------------
[Bananeweizen](https://github.com/Bananeweizen), [Bananeweizen](https://github.com/Bananeweizen),
[cmrn](https://github.com/cmrn), [cmrn](https://github.com/cmrn),
@ -83,8 +82,10 @@ Contributors
[OshiHidra](https://github.com/OshiHidra), [OshiHidra](https://github.com/OshiHidra),
[phoenixsong6](https://github.com/phoenixsong6), [phoenixsong6](https://github.com/phoenixsong6),
[Pirozek](https://github.com/Pirozek), [Pirozek](https://github.com/Pirozek),
[saithis](https://github.com/saithis),
[Scrool](https://github.com/Scrool), [Scrool](https://github.com/Scrool),
[sorgo](https://github.com/sorgo), [sorgo](https://github.com/sorgo),
[vita10gy](https://github.com/vita10gy),
[Xelio](https://github.com/Xelio), [Xelio](https://github.com/Xelio),
[ZauberNerd](https://github.com/ZauberNerd), [ZauberNerd](https://github.com/ZauberNerd),
[jonatkins](https://github.com/jonatkins) [jonatkins](https://github.com/jonatkins)
@ -115,9 +116,10 @@ Attribution & License
This project is licensed under the permissive ISC license. Parts imported from other projects remain under their respective licenses: This project is licensed under the permissive ISC license. Parts imported from other projects remain under their respective licenses:
- [load.js by Chris O'Hara; MIT](https://github.com/chriso/load.js)
- [autolink-js by Bryan Woods; MIT](https://github.com/bryanwoods/autolink-js) - [autolink-js by Bryan Woods; MIT](https://github.com/bryanwoods/autolink-js)
- [load.js by Chris O'Hara; MIT](https://github.com/chriso/load.js)
- [leaflet.js; custom license (but appears free)](http://leafletjs.com/) - [leaflet.js; custom license (but appears free)](http://leafletjs.com/)
- [leaflet.draw.js; by jacobtoye; MIT](https://github.com/Leaflet/Leaflet.draw)
- [`leaflet_google.js` by Pavel Shramov; same as Leaftlet](https://github.com/shramov/leaflet-plugins) (modified, though) - [`leaflet_google.js` by Pavel Shramov; same as Leaftlet](https://github.com/shramov/leaflet-plugins) (modified, though)
- StackOverflow-CopyPasta is attributed in the source; [CC-Wiki](https://creativecommons.org/licenses/by-sa/3.0/) - StackOverflow-CopyPasta is attributed in the source; [CC-Wiki](https://creativecommons.org/licenses/by-sa/3.0/)
- all Ingress/Niantic related stuff obviously remains non-free and is still copyrighted by Niantic/Google - all Ingress/Niantic related stuff obviously remains non-free and is still copyrighted by Niantic/Google

View File

@ -36,7 +36,7 @@ window.setupStyles = function() {
'.leaflet-right { margin-right: '+(SIDEBAR_WIDTH+1)+'px } ', '.leaflet-right { margin-right: '+(SIDEBAR_WIDTH+1)+'px } ',
'#updatestatus { width:'+(SIDEBAR_WIDTH-2*4+1)+'px; } ', '#updatestatus { width:'+(SIDEBAR_WIDTH-2*4+1)+'px; } ',
'#sidebar { width:'+(SIDEBAR_WIDTH + HIDDEN_SCROLLBAR_ASSUMED_WIDTH + 1 /*border*/)+'px; } ', '#sidebar { width:'+(SIDEBAR_WIDTH + HIDDEN_SCROLLBAR_ASSUMED_WIDTH + 1 /*border*/)+'px; } ',
'#sidebartoggle { right:'+SIDEBAR_WIDTH+'px; } ', '#sidebartoggle { right:'+(SIDEBAR_WIDTH+1)+'px; } ',
'#scrollwrapper { width:'+(SIDEBAR_WIDTH + 2*HIDDEN_SCROLLBAR_ASSUMED_WIDTH)+'px; right:-'+(2*HIDDEN_SCROLLBAR_ASSUMED_WIDTH-2)+'px } ', '#scrollwrapper { width:'+(SIDEBAR_WIDTH + 2*HIDDEN_SCROLLBAR_ASSUMED_WIDTH)+'px; right:-'+(2*HIDDEN_SCROLLBAR_ASSUMED_WIDTH-2)+'px } ',
'#sidebar input, h2 { width:'+(SIDEBAR_WIDTH - 2*4)+'px !important } ', '#sidebar input, h2 { width:'+(SIDEBAR_WIDTH - 2*4)+'px !important } ',
'#sidebar > *, #gamestat span, .imgpreview img { width:'+SIDEBAR_WIDTH+'px; }'].join("\n") '#sidebar > *, #gamestat span, .imgpreview img { width:'+SIDEBAR_WIDTH+'px; }'].join("\n")
@ -83,7 +83,7 @@ window.setupMap = function() {
map.addLayer(linksLayer, true); map.addLayer(linksLayer, true);
addLayers['Links'] = linksLayer; addLayers['Links'] = linksLayer;
map.addControl(new L.Control.Layers({ window.layerChooser = new L.Control.Layers({
'OSM Cloudmade Midnight': views[0], 'OSM Cloudmade Midnight': views[0],
'OSM Cloudmade Minimal': views[1], 'OSM Cloudmade Minimal': views[1],
'OSM Mapnik': views[2], 'OSM Mapnik': views[2],
@ -91,7 +91,9 @@ window.setupMap = function() {
'Google Roads': views[4], 'Google Roads': views[4],
'Google Satellite': views[5], 'Google Satellite': views[5],
'Google Hybrid': views[6] 'Google Hybrid': views[6]
}, addLayers)); }, addLayers);
map.addControl(window.layerChooser);
map.attributionControl.setPrefix(''); map.attributionControl.setPrefix('');
// listen for changes and store them in cookies // listen for changes and store them in cookies
map.on('moveend', window.storeMapPosition); map.on('moveend', window.storeMapPosition);
@ -111,8 +113,10 @@ window.setupMap = function() {
console.log('Remove all resonators'); console.log('Remove all resonators');
}); });
$("[name='leaflet-base-layers']").change(function () {
writeCookie('ingress.intelmap.type', $(this).parent().index()); map.on('baselayerchange', function () {
var selInd = $('[name=leaflet-base-layers]:checked').parent().index();
writeCookie('ingress.intelmap.type', selInd);
}); });
// map update status handling // map update status handling
@ -153,11 +157,11 @@ window.setupPlayerStat = function() {
var cls = PLAYER.team === 'ALIENS' ? 'enl' : 'res'; var cls = PLAYER.team === 'ALIENS' ? 'enl' : 'res';
var t = 'Level:\t\t' + level + '\n' var t = 'Level:\t' + level + '\n'
+ 'XM:\t\t\t' + PLAYER.energy + ' / ' + xmMax + '\n' + 'XM:\t' + PLAYER.energy + ' / ' + xmMax + '\n'
+ 'AP:\t\t\t' + digits(ap) + '\n' + 'AP:\t' + digits(ap) + '\n'
+ (level < 8 ? 'level up in:\t' + lvlUpAp + ' AP' : 'Congrats! (neeeeerd)') + (level < 8 ? 'level up in:\t' + lvlUpAp + ' AP' : 'Congrats! (neeeeerd)')
+ '\n\Invites:\t\t'+PLAYER.available_invites; + '\n\Invites:\t'+PLAYER.available_invites;
+ '\n\nNote: your player stats can only be updated by a full reload (F5)'; + '\n\nNote: your player stats can only be updated by a full reload (F5)';
$('#playerstat').html('' $('#playerstat').html(''
@ -189,10 +193,57 @@ window.setupSidebarToggle = function() {
}); });
} }
window.setupTooltips = function() {
$(document).tooltip({
// disable show/hide animation
show: { effect: "hide", duration: 0 } ,
hide: false,
open: function(event, ui) {
ui.tooltip.delay(300).fadeIn(0);
},
content: function() {
var title = $(this).attr('title');
// check if it should be converted to a table
if(!title.match(/\t/)) {
return title.replace(/\n/g, '<br />');
}
var data = [];
var columnCount = 0;
// parse data
var rows = title.split('\n');
$.each(rows, function(i, row) {
data[i] = row.split('\t');
if(data[i].length > columnCount) columnCount = data[i].length;
});
// build the table
var tooltip = '<table>';
$.each(data, function(i, row) {
tooltip += '<tr>';
$.each(data[i], function(k, cell) {
var attributes = '';
if(k === 0 && data[i].length < columnCount) {
attributes = ' colspan="'+(columnCount - data[i].length + 1)+'"';
}
tooltip += '<td'+attributes+'>'+cell+'</td>';
});
tooltip += '</tr>';
});
tooltip += '</table>';
return tooltip;
}
});
}
// BOOTING /////////////////////////////////////////////////////////// // BOOTING ///////////////////////////////////////////////////////////
function boot() { function boot() {
window.debug.console.overwriteNativeIfRequired();
console.log('loading done, booting'); console.log('loading done, booting');
window.setupStyles(); window.setupStyles();
window.setupMap(); window.setupMap();
@ -202,6 +253,7 @@ function boot() {
window.setupSidebarToggle(); window.setupSidebarToggle();
window.updateGameScore(); window.updateGameScore();
window.setupPlayerStat(); window.setupPlayerStat();
window.setupTooltips();
window.chat.setup(); window.chat.setup();
// read here ONCE, so the URL is only evaluated one time after the // read here ONCE, so the URL is only evaluated one time after the
// necessary data has been loaded. // necessary data has been loaded.
@ -230,10 +282,11 @@ function asyncLoadScript(a){return function(b,c){var d=document.createElement("s
// contains the default Ingress map style. // contains the default Ingress map style.
var LLGMAPS = 'http://breunigs.github.com/ingress-intel-total-conversion/dist/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 JQUERY = 'https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js';
var JQUERYUI = 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.0/jquery-ui.min.js';
var LEAFLET = 'http://cdn.leafletjs.com/leaflet-0.5/leaflet.js'; var LEAFLET = 'http://cdn.leafletjs.com/leaflet-0.5/leaflet.js';
var AUTOLINK = 'http://breunigs.github.com/ingress-intel-total-conversion/dist/autolink.js'; var AUTOLINK = 'http://breunigs.github.com/ingress-intel-total-conversion/dist/autolink.js';
// after all scripts have loaded, boot the actual app // after all scripts have loaded, boot the actual app
load(JQUERY, LEAFLET, AUTOLINK).then(LLGMAPS).onError(function (err) { load(JQUERY, LEAFLET, AUTOLINK).then(LLGMAPS, JQUERYUI).onError(function (err) {
alert('Could not all resources, the script likely wont work.\n\nIf this happend the first time for you, its probably a temporary issue. Just wait a bit and try again.\n\nIf you installed the script for the first time and this happens:\n try disabling NoScript if you have it installed\n press CTRL+SHIFT+K in Firefox or CTRL+SHIFT+I in Chrome/Opera and reload the page. Additional info may be available in the console.\n Open an issue at https://github.com/breunigs/ingress-intel-total-conversion/issues'); alert('Could not all resources, the script likely wont work.\n\nIf this happend the first time for you, its probably a temporary issue. Just wait a bit and try again.\n\nIf you installed the script for the first time and this happens:\n try disabling NoScript if you have it installed\n press CTRL+SHIFT+K in Firefox or CTRL+SHIFT+I in Chrome/Opera and reload the page. Additional info may be available in the console.\n Open an issue at https://github.com/breunigs/ingress-intel-total-conversion/issues');
}).thenRun(boot); }).thenRun(boot);

View File

@ -1,22 +1,14 @@
window.chat = function() {}; window.chat = function() {};
window.chat._lastNicksForAutocomplete = [[], []];
window.chat.addNickForAutocomplete = function(nick, isFaction) {
var r = chat._lastNicksForAutocomplete[isFaction ? 0 : 1];
if(r.indexOf(nick) !== -1) return;
r.push(nick);
if(r.length >= 15)
r.shift();
}
window.chat.handleTabCompletion = function() { window.chat.handleTabCompletion = function() {
var el = $('#chatinput input'); var el = $('#chatinput input');
var curPos = el.get(0).selectionStart; var curPos = el.get(0).selectionStart;
var text = el.val(); var text = el.val();
var word = text.slice(0, curPos).replace(/.*\b([a-z0-9-_])/, '$1').toLowerCase(); var word = text.slice(0, curPos).replace(/.*\b([a-z0-9-_])/, '$1').toLowerCase();
var list = window.chat._lastNicksForAutocomplete; var list = $('#chat > div:visible mark');
list = list[1].concat(list[0]); list = list.map(function(ind, mark) { return $(mark).text(); } );
list = uniqueArray(list);
var nick = null; var nick = null;
for(var i = 0; i < list.length; i++) { for(var i = 0; i < list.length; i++) {
@ -43,27 +35,24 @@ window.chat.handleTabCompletion = function() {
// timestamp and clear management // timestamp and clear management
// //
window.chat._oldFactionTimestamp = -1; window.chat.getTimestamps = function(isFaction) {
window.chat._newFactionTimestamp = -1; var storage = isFaction ? chat._factionData : chat._publicData;
window.chat._oldPublicTimestamp = -1; return $.map(storage, function(v, k) { return [v[0]]; });
window.chat._newPublicTimestamp = -1;
window.chat.getOldestTimestamp = function(public) {
return chat['_old'+(public ? 'Public' : 'Faction')+'Timestamp'];
} }
window.chat.getNewestTimestamp = function(public) { window.chat.getOldestTimestamp = function(isFaction) {
return chat['_new'+(public ? 'Public' : 'Faction')+'Timestamp']; var t = Math.min.apply(null, chat.getTimestamps(isFaction));
return t === Infinity ? -1 : t;
} }
window.chat.clearIfRequired = function(elm) { window.chat.getNewestTimestamp = function(isFaction) {
if(!elm.data('needsClearing')) return; var t = Math.max.apply(null, chat.getTimestamps(isFaction));
elm.data('ignoreNextScroll', true).data('needsClearing', false).html(''); return t === -1*Infinity ? -1 : t;
} }
window.chat._oldBBox = null; window.chat._oldBBox = null;
window.chat.genPostData = function(public, getOlderMsgs) { window.chat.genPostData = function(isFaction, getOlderMsgs) {
if(typeof public !== 'boolean') throw('Need to know if public or faction chat.'); if(typeof isFaction !== 'boolean') throw('Need to know if public or faction chat.');
chat._localRangeCircle.setLatLng(map.getCenter()); chat._localRangeCircle.setLatLng(map.getCenter());
var b = map.getBounds().extend(chat._localRangeCircle.getBounds()); var b = map.getBounds().extend(chat._localRangeCircle.getBounds());
@ -78,35 +67,30 @@ window.chat.genPostData = function(public, getOlderMsgs) {
// need to reset these flags now because clearing will only occur // need to reset these flags now because clearing will only occur
// after the request is finished i.e. there would be one almost // after the request is finished i.e. there would be one almost
// useless request. // useless request.
chat._displayedFactionGuids = []; chat._factionData = {};
chat._displayedPublicGuids = []; chat._publicData = {};
chat._displayedPlayerActionTime = {};
chat._oldFactionTimestamp = -1;
chat._newFactionTimestamp = -1;
chat._oldPublicTimestamp = -1;
chat._newPublicTimestamp = -1;
} }
chat._oldBBox = bbs; chat._oldBBox = bbs;
var ne = b.getNorthEast(); var ne = b.getNorthEast();
var sw = b.getSouthWest(); var sw = b.getSouthWest();
var data = { var data = {
desiredNumItems: public ? CHAT_PUBLIC_ITEMS : CHAT_FACTION_ITEMS, desiredNumItems: isFaction ? CHAT_FACTION_ITEMS : CHAT_PUBLIC_ITEMS ,
minLatE6: Math.round(sw.lat*1E6), minLatE6: Math.round(sw.lat*1E6),
minLngE6: Math.round(sw.lng*1E6), minLngE6: Math.round(sw.lng*1E6),
maxLatE6: Math.round(ne.lat*1E6), maxLatE6: Math.round(ne.lat*1E6),
maxLngE6: Math.round(ne.lng*1E6), maxLngE6: Math.round(ne.lng*1E6),
minTimestampMs: -1, minTimestampMs: -1,
maxTimestampMs: -1, maxTimestampMs: -1,
factionOnly: !public factionOnly: isFaction
} }
if(getOlderMsgs) { if(getOlderMsgs) {
// ask for older chat when scrolling up // ask for older chat when scrolling up
data = $.extend(data, {maxTimestampMs: chat.getOldestTimestamp(public)}); data = $.extend(data, {maxTimestampMs: chat.getOldestTimestamp(isFaction)});
} else { } else {
// ask for newer chat // ask for newer chat
var min = chat.getNewestTimestamp(public); var min = chat.getNewestTimestamp(isFaction);
// the inital request will have both timestamp values set to -1, // the inital request will have both timestamp values set to -1,
// thus we receive the newest desiredNumItems. After that, we will // thus we receive the newest desiredNumItems. After that, we will
// only receive messages with a timestamp greater or equal to min // only receive messages with a timestamp greater or equal to min
@ -130,322 +114,219 @@ window.chat.genPostData = function(public, getOlderMsgs) {
// //
// requesting faction // faction
// //
window.chat._requestOldFactionRunning = false; window.chat._requestFactionRunning = false;
window.chat.requestOldFaction = function(isRetry) { window.chat.requestFaction = function(getOlderMsgs, isRetry) {
if(chat._requestOldFactionRunning) return; if(chat._requestFactionRunning && !isRetry) return;
if(isIdle()) return renderUpdateStatus(); if(isIdle()) return renderUpdateStatus();
chat._requestOldFactionRunning = true; chat._requestFactionRunning = true;
var d = chat.genPostData(false, true); var d = chat.genPostData(true, getOlderMsgs);
var r = window.postAjax( var r = window.postAjax(
'getPaginatedPlextsV2', 'getPaginatedPlextsV2',
d, d,
chat.handleOldFaction, chat.handleFaction,
isRetry isRetry
? function() { window.chat._requestOldFactionRunning = false; } ? function() { window.chat._requestFactionRunning = false; }
: function() { window.chat.requestOldFaction(true) } : function() { window.chat.requestFaction(getOlderMsgs, 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); requests.add(r);
} }
// window.chat._factionData = {};
// handle faction window.chat.handleFaction = function(data, textStatus, jqXHR) {
// chat._requestFactionRunning = false;
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) { if(!data || !data.result) {
window.failedRequestCount++; window.failedRequestCount++;
return console.warn('faction chat error. Waiting for next auto-refresh.'); return console.warn('faction chat error. Waiting for next auto-refresh.');
} }
var c = $('#chatfaction');
chat.clearIfRequired(c);
if(data.result.length === 0) return; if(data.result.length === 0) return;
chat._newFactionTimestamp = data.result[0][1]; var old = chat.getOldestTimestamp(true);
chat._oldFactionTimestamp = data.result[data.result.length-1][1]; chat.writeDataToHash(data, chat._factionData, false);
var oldMsgsWereAdded = old !== chat.getOldestTimestamp(true);
var scrollBefore = scrollBottom(c); window.chat.renderFaction(oldMsgsWereAdded);
chat.renderPlayerMsgsTo(true, data, isOldMsgs, chat._displayedFactionGuids);
chat.keepScrollPosition(c, scrollBefore, isOldMsgs);
if(data.result.length >= CHAT_FACTION_ITEMS) chat.needMoreMessages(); if(data.result.length >= CHAT_FACTION_ITEMS) chat.needMoreMessages();
} }
window.chat.renderFaction = function(oldMsgsWereAdded) {
chat.renderData(chat._factionData, 'chatfaction', oldMsgsWereAdded);
}
// //
// requesting public // public
// //
window.chat._requestOldPublicRunning = false; window.chat._requestPublicRunning = false;
window.chat.requestOldPublic = function(isRetry) { window.chat.requestPublic = function(getOlderMsgs, isRetry) {
if(chat._requestOldPublicRunning) return; if(chat._requestPublicRunning && !isRetry) return;
if(isIdle()) return renderUpdateStatus(); if(isIdle()) return renderUpdateStatus();
chat._requestOldPublicRunning = true; chat._requestPublicRunning = true;
var d = chat.genPostData(true, true); var d = chat.genPostData(false, getOlderMsgs);
var r = window.postAjax( var r = window.postAjax(
'getPaginatedPlextsV2', 'getPaginatedPlextsV2',
d, d,
chat.handleOldPublic, chat.handlePublic,
isRetry isRetry
? function() { window.chat._requestOldPublicRunning = false; } ? function() { window.chat._requestPublicRunning = false; }
: function() { window.chat.requestOldPublic(true) } : function() { window.chat.requestPublic(getOlderMsgs, true) }
); );
requests.add(r); requests.add(r);
} }
window.chat._requestNewPublicRunning = false; window.chat._publicData = {};
window.chat.requestNewPublic = function(isRetry) { window.chat.handlePublic = function(data, textStatus, jqXHR) {
if(chat._requestNewPublicRunning) return; chat._requestPublicRunning = false;
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) { if(!data || !data.result) {
window.failedRequestCount++; window.failedRequestCount++;
return console.warn('public chat error. Waiting for next auto-refresh.'); 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; if(data.result.length === 0) return;
chat._newPublicTimestamp = data.result[0][1]; var old = chat.getOldestTimestamp(false);
chat._oldPublicTimestamp = data.result[data.result.length-1][1]; chat.writeDataToHash(data, chat._publicData, true);
var oldMsgsWereAdded = old !== chat.getOldestTimestamp(false);
switch(chat.getActive()) {
var scrollBefore = scrollBottom(ca); case 'public': window.chat.renderPublic(oldMsgsWereAdded); break;
chat.handlePublicAutomated(data); case 'compact': window.chat.renderCompact(oldMsgsWereAdded); break;
chat.keepScrollPosition(ca, scrollBefore, isOldMsgs); case 'full': window.chat.renderFull(oldMsgsWereAdded); break;
}
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(); if(data.result.length >= CHAT_PUBLIC_ITEMS) chat.needMoreMessages();
} }
window.chat.renderPublic = function(oldMsgsWereAdded) {
window.chat.handlePublicAutomated = function(data) { // only keep player data
$.each(data.result, function(ind, json) { // newest first! var data = $.map(chat._publicData, function(entry) {
var time = json[1]; if(!entry[1]) return [entry];
// ignore player messages
var t = json[2].plext.plextType;
if(t !== 'SYSTEM_BROADCAST' && t !== 'SYSTEM_NARROWCAST') return true;
var tmpmsg = '', nick = null, pguid, team;
// each automated message is composed of many text chunks. loop
// over them to gather all necessary data.
$.each(json[2].plext.markup, function(ind, part) {
switch(part[0]) {
case 'PLAYER':
pguid = part[1].guid;
var lastAction = window.chat._displayedPlayerActionTime[pguid];
// ignore older messages about player
if(lastAction && lastAction[0] > time) return false;
nick = part[1].plain;
team = part[1].team === 'ALIENS' ? TEAM_ENL : TEAM_RES;
window.setPlayerName(pguid, nick); // free nick name resolves
if(ind > 0) tmpmsg += nick; // dont repeat nick directly
break;
case 'TEXT':
tmpmsg += part[1].plain;
break;
case 'PORTAL':
var latlng = [part[1].latE6/1E6, part[1].lngE6/1E6];
var js = 'window.zoomToAndShowPortal(\''+part[1].guid+'\', ['+latlng[0]+', '+latlng[1]+'])';
tmpmsg += '<a onclick="'+js+'" title="'+part[1].address+'" class="help">'+part[1].name+'</a>';
break;
}
}); });
chat.renderData(data, 'chatpublic', oldMsgsWereAdded);
}
// nick will only be set if we dont have any info about that window.chat.renderCompact = function(oldMsgsWereAdded) {
// player yet. var data = {};
if(nick) { $.each(chat._publicData, function(guid, entry) {
tmpmsg = chat.renderMsg(tmpmsg, nick, time, team); // skip player msgs
window.chat._displayedPlayerActionTime[pguid] = [time, tmpmsg]; if(!entry[1]) return true;
}; var pguid = entry[3];
// ignore if player has newer data
if(data[pguid] && data[pguid][0] > entry[0]) return true;
data[pguid] = entry;
}); });
// data keys are now player guids instead of message guids. However,
if(chat.getActive() === 'automated') // it is all the same to renderData.
window.chat.renderAutomatedMsgsTo(); chat.renderData(data, 'chatcompact', oldMsgsWereAdded);
} }
window.chat.renderAutomatedMsgsTo = function() { window.chat.renderFull = function(oldMsgsWereAdded) {
var x = window.chat._displayedPlayerActionTime; // only keep automatically generated data
// we dont care about the GUIDs anymore var data = $.map(chat._publicData, function(entry) {
var vals = $.map(x, function(v, k) { return [v]; }); if(entry[1]) return [entry];
// sort them old to new });
vals = vals.sort(function(a, b) { return a[0]-b[0]; }); chat.renderData(data, 'chatfull', oldMsgsWereAdded);
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 // common
// //
window.chat.writeDataToHash = function(newData, storageHash, skipSecureMsgs) {
window.chat.renderPlayerMsgsTo = function(isFaction, data, isOldMsgs, dupCheckArr) { $.each(newData.result, function(ind, json) {
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 // avoid duplicates
if(dupCheckArr.indexOf(json[0]) !== -1) return true; if(json[0] in storageHash) return true;
dupCheckArr.push(json[0]);
var skipThisEntry = false;
var time = json[1]; var time = json[1];
var team = json[2].plext.team === 'ALIENS' ? TEAM_ENL : TEAM_RES; var team = json[2].plext.team === 'ALIENS' ? TEAM_ENL : TEAM_RES;
var msg, nick, pguid; var auto = json[2].plext.plextType !== 'PLAYER_GENERATED';
var msg = '', nick = '', pguid;
$.each(json[2].plext.markup, function(ind, markup) { $.each(json[2].plext.markup, function(ind, markup) {
if(markup[0] === 'SENDER') { switch(markup[0]) {
case 'SENDER': // user generated messages
nick = markup[1].plain.slice(0, -2); // cut “: ” at end nick = markup[1].plain.slice(0, -2); // cut “: ” at end
pguid = markup[1].guid; pguid = markup[1].guid;
break;
case 'PLAYER': // automatically generated messages
pguid = markup[1].guid;
nick = markup[1].plain;
team = markup[1].team === 'ALIENS' ? TEAM_ENL : TEAM_RES;
if(ind > 0) msg += nick; // dont repeat nick directly
break;
case 'TEXT':
var tmp = $('<div/>').text(markup[1].plain).html().autoLink();
msg += tmp.replace(window.PLAYER['nickMatcher'], '<em>$1</em>');
break;
case 'PORTAL':
var latlng = [markup[1].latE6/1E6, markup[1].lngE6/1E6];
var js = 'window.zoomToAndShowPortal(\''+markup[1].guid+'\', ['+latlng[0]+', '+latlng[1]+'])';
msg += '<a onclick="'+js+'" title="'+markup[1].address+'" class="help">'+markup[1].name+'</a>';
break;
case 'SECURE':
if(skipSecureMsgs) {
skipThisEntry = true;
return false; // breaks $.each
}
}
});
if(skipThisEntry) return true;
// format: timestamp, autogenerated, HTML message, player guid
storageHash[json[0]] = [json[1], auto, chat.renderMsg(msg, nick, time, team), pguid];
window.setPlayerName(pguid, nick); // free nick name resolves window.setPlayerName(pguid, nick); // free nick name resolves
if(!isOldMsgs) window.chat.addNickForAutocomplete(nick, isFaction);
}
if(markup[0] === 'TEXT') {
msg = markup[1].plain.autoLink();
msg = msg.replace(window.PLAYER['nickMatcher'], '<em>$1</em>');
}
if(!isFaction && markup[0] === 'SECURE') {
nick = null;
return false; // aka break
}
}); });
}
if(!nick) return true; // aka next // renders data from the data-hash to the element defined by the given
// ID. Set 3rd argument to true if it is likely that old data has been
// added. Latter is only required for scrolling.
window.chat.renderData = function(data, element, likelyWereOldMsgs) {
var elm = $('#'+element);
if(elm.is(':hidden')) return;
var nowTime = new Date(time).toLocaleDateString(); // discard guids and sort old to new
if(prevTime && prevTime !== nowTime) var vals = $.map(data, function(v, k) { return [v]; });
msgs += chat.renderDivider(nowTime); vals = vals.sort(function(a, b) { return a[0]-b[0]; });
msgs += chat.renderMsg(msg, nick, time, team); // render to string with date separators inserted
prevTime = nowTime; var msgs = '';
}); var prevTime = null;
$.each(vals, function(ind, msg) {
var addTo = isFaction ? $('#chatfaction') : $('#chatpublic'); var nextTime = new Date(msg[0]).toLocaleDateString();
if(prevTime && prevTime !== nextTime)
// 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); msgs += chat.renderDivider(nextTime);
} msgs += msg[2];
prevTime = nextTime;
});
if(isOldMsgs) var scrollBefore = scrollBottom(elm);
addTo.prepend(msgs); elm.html(msgs);
else chat.keepScrollPosition(elm, scrollBefore, likelyWereOldMsgs);
addTo.append(msgs);
} }
window.chat.renderDivider = function(text) { window.chat.renderDivider = function(text) {
return '<summary>─ '+text+' ────────────────────────────────────────────────────────────────────────────</summary>'; return '<summary>─ '+text+' ──────────────────────────────────────────────────────────────────────────</summary>';
} }
@ -486,27 +367,36 @@ window.chat.toggle = function() {
window.chat.request = function() { window.chat.request = function() {
console.log('refreshing chat'); console.log('refreshing chat');
chat.requestNewFaction(); chat.requestFaction(false);
chat.requestNewPublic(); chat.requestPublic(false);
} }
// checks if there are enough messages in the selected chat tab and // checks if there are enough messages in the selected chat tab and
// loads more if not. // loads more if not.
window.chat.needMoreMessages = function() { window.chat.needMoreMessages = function() {
var activeTab = chat.getActive();
if(activeTab === 'debug') return;
var activeChat = $('#chat > :visible'); var activeChat = $('#chat > :visible');
if(scrollBottom(activeChat) !== 0 || activeChat.scrollTop() !== 0) return;
console.log('no scrollbar in active chat, requesting more msgs'); var hasScrollbar = scrollBottom(activeChat) !== 0 || activeChat.scrollTop() !== 0;
if($('#chatcontrols a:last.active').length) var nearTop = activeChat.scrollTop() <= CHAT_REQUEST_SCROLL_TOP;
chat.requestOldFaction(); if(hasScrollbar && !nearTop) return;
console.log('No scrollbar or near top in active chat. Requesting more data.');
if(activeTab === 'faction')
chat.requestFaction(true);
else else
chat.requestOldPublic(); chat.requestPublic(true);
} }
window.chat.chooser = function(event) { window.chat.chooser = function(event) {
var t = $(event.target); var t = $(event.target);
var tt = t.text(); var tt = t.text();
var span = $('#chatinput span'); var span = $('#chatinput span');
$('#chatcontrols .active').removeClass('active'); $('#chatcontrols .active').removeClass('active');
@ -520,24 +410,26 @@ window.chat.chooser = function(event) {
case 'faction': case 'faction':
span.css('color', ''); span.css('color', '');
span.text('tell faction:'); span.text('tell faction:');
elm = $('#chatfaction');
break; break;
case 'public': case 'public':
span.css('cssText', 'color: red !important'); span.css('cssText', 'color: red !important');
span.text('broadcast:'); span.text('broadcast:');
elm = $('#chatpublic');
break; break;
case 'automated': case 'compact':
case 'full':
span.css('cssText', 'color: #bbb !important'); span.css('cssText', 'color: #bbb !important');
span.text('tell Jarvis:'); span.text('tell Jarvis:');
chat.renderAutomatedMsgsTo();
elm = $('#chatautomated');
break; break;
default:
throw('chat.chooser was asked to handle unknown button: ' + tt);
} }
var elm = $('#chat' + tt);
elm.show(); elm.show();
eval('chat.render' + tt.capitalize() + '(false);');
if(elm.data('needsScrollTop')) { if(elm.data('needsScrollTop')) {
elm.data('ignoreNextScroll', true); elm.data('ignoreNextScroll', true);
elm.scrollTop(elm.data('needsScrollTop')); elm.scrollTop(elm.data('needsScrollTop'));
@ -580,7 +472,10 @@ window.chat.setup = function() {
$('#chatcontrols, #chat, #chatinput').show(); $('#chatcontrols, #chat, #chatinput').show();
$('#chatcontrols a:first').click(window.chat.toggle); $('#chatcontrols a:first').click(window.chat.toggle);
$('#chatcontrols a:not(:first)').click(window.chat.chooser); $('#chatcontrols a').each(function(ind, elm) {
if($.inArray($(elm).text(), ['full', 'compact', 'public', 'faction']) !== -1)
$(elm).click(window.chat.chooser);
});
$('#chatinput').click(function() { $('#chatinput').click(function() {
@ -593,15 +488,15 @@ window.chat.setup = function() {
$('#chatfaction').scroll(function() { $('#chatfaction').scroll(function() {
var t = $(this); var t = $(this);
if(t.data('ignoreNextScroll')) return t.data('ignoreNextScroll', false); if(t.data('ignoreNextScroll')) return t.data('ignoreNextScroll', false);
if(t.scrollTop() < 200) chat.requestOldFaction(); if(t.scrollTop() < CHAT_REQUEST_SCROLL_TOP) chat.requestFaction(true);
if(scrollBottom(t) === 0) chat.requestNewFaction(); if(scrollBottom(t) === 0) chat.requestFaction(false);
}); });
$('#chatpublic, #chatautomated').scroll(function() { $('#chatpublic, #chatfull, #chatcompact').scroll(function() {
var t = $(this); var t = $(this);
if(t.data('ignoreNextScroll')) return t.data('ignoreNextScroll', false); if(t.data('ignoreNextScroll')) return t.data('ignoreNextScroll', false);
if(t.scrollTop() < 200) chat.requestOldPublic(); if(t.scrollTop() < CHAT_REQUEST_SCROLL_TOP) chat.requestPublic(true);
if(scrollBottom(t) === 0) chat.requestNewPublic(); if(scrollBottom(t) === 0) chat.requestPublic(false);
}); });
chat.request(); chat.request();
@ -637,7 +532,6 @@ window.chat.setupTime = function() {
window.chat.setupPosting = function() { window.chat.setupPosting = function() {
$('#chatinput input').keydown(function(event) { $('#chatinput input').keydown(function(event) {
try { try {
var kc = (event.keyCode ? event.keyCode : event.which); var kc = (event.keyCode ? event.keyCode : event.which);
if(kc === 13) { // enter if(kc === 13) { // enter
chat.postMsg(); chat.postMsg();
@ -646,8 +540,6 @@ try{
event.preventDefault(); event.preventDefault();
window.chat.handleTabCompletion(); window.chat.handleTabCompletion();
} }
} catch(error) { } catch(error) {
console.log(error); console.log(error);
debug.printStackTrace(); debug.printStackTrace();
@ -655,19 +547,22 @@ try{
}); });
$('#chatinput').submit(function(event) { $('#chatinput').submit(function(event) {
chat.postMsg();
event.preventDefault(); event.preventDefault();
chat.postMsg();
}); });
} }
window.chat.postMsg = function() { window.chat.postMsg = function() {
var c = chat.getActive(); 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?'); if(c === 'full' || c === 'compact')
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()); var msg = $.trim($('#chatinput input').val());
if(!msg || msg === '') return; if(!msg || msg === '') return;
if(c === 'debug') return new Function (msg)();
var public = c === 'public'; var public = c === 'public';
var latlng = map.getCenter(); var latlng = map.getCenter();
@ -676,11 +571,15 @@ window.chat.postMsg = function() {
lngE6: Math.round(latlng.lng*1E6), lngE6: Math.round(latlng.lng*1E6),
factionOnly: !public}; factionOnly: !public};
var errMsg = 'Your message could not be delivered. You can copy&' +
'paste it here and try again if you want:\n\n' + msg;
window.postAjax('sendPlext', data, window.postAjax('sendPlext', data,
function() { if(public) chat.requestNewPublic(); else chat.requestNewFaction(); }, function(response) {
if(response.error) alert(errMsg);
if(public) chat.requestPublic(false); else chat.requestFaction(false); },
function() { function() {
alert('Your message could not be delivered. You can copy&' + alert(errMsg);
'paste it here and try again if you want:\n\n'+msg);
} }
); );

View File

@ -42,3 +42,68 @@ window.debug.forceSync = function() {
updateGameScore(); updateGameScore();
requestData(); requestData();
} }
window.debug.console = function() {
$('#debugconsole').text();
}
window.debug.console.create = function() {
if($('#debugconsole').length) return;
$('#chatcontrols').append('<a>debug</a>');
$('#chatcontrols a:last').click(function() {
$('#chatinput span').css('cssText', 'color: #bbb !important').text('debug:');
$('#chat > div').hide();
$('#debugconsole').show();
$('#chatcontrols .active').removeClass('active');
$(this).addClass('active');
});
$('#chat').append('<div id="debugconsole" style="display: none"></div>');
}
window.debug.console.renderLine = function(text, errorType) {
debug.console.create();
switch(errorType) {
case 'error': var color = '#FF424D'; break;
case 'warning': var color = '#FFDE42'; break;
case 'alert': var color = '#42FF90'; break;
default: var color = '#eee';
}
if(typeof text !== 'string' && typeof text !== 'number') text = JSON.stringify(text);
var d = new Date();
var ta = d.toLocaleTimeString(); // print line instead maybe?
var tb = d.toLocaleString();
var t = '<time title="'+tb+'" data-timestamp="'+d.getTime()+'">'+ta+'</time>';
var s = 'style="color:'+color+'"';
var l = '<p>'+t+'<mark '+s+'>'+errorType+'</mark><span>'+text+'</span></p>';
$('#debugconsole').prepend(l);
}
window.debug.console.log = function(text) {
debug.console.renderLine(text, 'notice');
}
window.debug.console.warn = function(text) {
debug.console.renderLine(text, 'warning');
}
window.debug.console.error = function(text) {
debug.console.renderLine(text, 'error');
}
window.debug.console.alert = function(text) {
debug.console.renderLine(text, 'alert');
}
window.debug.console.overwriteNative = function() {
window.debug.console.create();
window.console = function() {}
window.console.log = window.debug.console.log;
window.console.warn = window.debug.console.warn;
window.console.error = window.debug.console.error;
window.alert = window.debug.console.alert;
}
window.debug.console.overwriteNativeIfRequired = function() {
if(!window.console || L.Browser.mobile)
window.debug.console.overwriteNative();
}

View File

@ -15,7 +15,7 @@ window.updateGameScore = function(data) {
var es = '<span class="enl" style="width:'+ep+'%;">&nbsp;'+Math.round(ep)+'%</span>'; var es = '<span class="enl" style="width:'+ep+'%;">&nbsp;'+Math.round(ep)+'%</span>';
$('#gamestat').html(rs+es).one('click', function() { window.updateGameScore() }); $('#gamestat').html(rs+es).one('click', function() { window.updateGameScore() });
// help cursor via “#gamestat span” // help cursor via “#gamestat span”
$('#gamestat').attr('title', 'Resistance:\t\t'+r+' MindUnits\nEnlightenment:\t'+e+' MindUnits'); $('#gamestat').attr('title', 'Resistance:\t'+r+' MindUnits\nEnlightenment:\t'+e+' MindUnits');
window.setTimeout('window.updateGameScore', REFRESH_GAME_SCORE*1000); window.setTimeout('window.updateGameScore', REFRESH_GAME_SCORE*1000);
} }

View File

@ -5,11 +5,17 @@
// retrieves current position from map and stores it cookies // retrieves current position from map and stores it cookies
window.storeMapPosition = function() { window.storeMapPosition = function() {
var m = window.map.getCenter(); var m = window.map.getCenter();
if(m['lat'] >= -90 && m['lat'] <= 90)
writeCookie('ingress.intelmap.lat', m['lat']); writeCookie('ingress.intelmap.lat', m['lat']);
if(m['lng'] >= -180 && m['lng'] <= 180)
writeCookie('ingress.intelmap.lng', m['lng']); writeCookie('ingress.intelmap.lng', m['lng']);
writeCookie('ingress.intelmap.zoom', window.map.getZoom()); writeCookie('ingress.intelmap.zoom', window.map.getZoom());
} }
// either retrieves the last shown position from a cookie, from the // either retrieves the last shown position from a cookie, from the
// URL or if neither is present, via Geolocation. If that fails, it // URL or if neither is present, via Geolocation. If that fails, it
// returns a map that shows the whole world. // returns a map that shows the whole world.
@ -28,6 +34,10 @@ window.getPosition = function() {
var lat = parseFloat(readCookie('ingress.intelmap.lat')) || 0.0; var lat = parseFloat(readCookie('ingress.intelmap.lat')) || 0.0;
var lng = parseFloat(readCookie('ingress.intelmap.lng')) || 0.0; var lng = parseFloat(readCookie('ingress.intelmap.lng')) || 0.0;
var z = parseInt(readCookie('ingress.intelmap.zoom')) || 17; var z = parseInt(readCookie('ingress.intelmap.zoom')) || 17;
if(lat < -90 || lat > 90) lat = 0.0;
if(lng < -180 || lng > 180) lng = 0.0;
return {center: new L.LatLng(lat, lng), zoom: z > 18 ? 18 : z}; return {center: new L.LatLng(lat, lng), zoom: z > 18 ? 18 : z};
} }

View File

@ -75,7 +75,7 @@ window.handleDataResponse = function(data, textStatus, jqXHR) {
if(getTypeByGuid(guid) === TYPE_FIELD && window.fields[guid] !== undefined) { if(getTypeByGuid(guid) === TYPE_FIELD && window.fields[guid] !== undefined) {
$.each(window.fields[guid].options.vertices, function(ind, vertex) { $.each(window.fields[guid].options.vertices, function(ind, vertex) {
if(window.portals[vertex.guid] === undefined) return true; if(window.portals[vertex.guid] === undefined) return true;
fieldArray = window.portals[vertex.guid].options.portalV2.linkedFields; fieldArray = window.portals[vertex.guid].options.details.portalV2.linkedFields;
fieldArray.splice($.inArray(guid, fieldArray), 1); fieldArray.splice($.inArray(guid, fieldArray), 1);
}); });
} }
@ -225,9 +225,14 @@ window.renderPortal = function(ent) {
var oo = old.options; var oo = old.options;
var u = oo.team !== team; var u = oo.team !== team;
u = u || oo.level !== portalLevel; u = u || oo.level !== portalLevel;
// nothing for the portal changed, so dont update. Let resonators // nothing changed that requires re-rendering the portal.
// manage themselves if they want to be updated. if(!u) {
if(!u) return renderResonators(ent); // let resos handle themselves if they need to be redrawn
renderResonators(ent, old);
// update stored details for portal details in sidebar.
old.options.details = ent[2];
return;
}
// there were changes, remove old portal // there were changes, remove old portal
removeByGuid(ent[0]); removeByGuid(ent[0]);
} }
@ -237,7 +242,6 @@ window.renderPortal = function(ent) {
// pre-loads player names for high zoom levels // pre-loads player names for high zoom levels
loadPlayerNamesForPortal(ent[2]); loadPlayerNamesForPortal(ent[2]);
var lvWeight = Math.max(2, portalLevel / 1.5); var lvWeight = Math.max(2, portalLevel / 1.5);
var lvRadius = Math.max(portalLevel + 3, 5); var lvRadius = Math.max(portalLevel + 3, 5);
@ -287,7 +291,7 @@ window.renderPortal = function(ent) {
window.map.setView(latlng, 17); window.map.setView(latlng, 17);
}); });
window.renderResonators(ent); window.renderResonators(ent, null);
window.runHooks('portalAdded', {portal: p}); window.runHooks('portalAdded', {portal: p});
@ -295,18 +299,29 @@ window.renderPortal = function(ent) {
p.addTo(layerGroup); p.addTo(layerGroup);
} }
window.renderResonators = function(ent) { window.renderResonators = function(ent, portalLayer) {
var portalLevel = getPortalLevel(ent[2]);
if(portalLevel < getMinPortalLevel() && ent[0] != selectedPortal) return;
if(!isResonatorsShow()) return; if(!isResonatorsShow()) return;
var portalLevel = getPortalLevel(ent[2]);
if(portalLevel < getMinPortalLevel() && ent[0] != selectedPortal) return;
var portalLatLng = [ent[2].locationE6.latE6/1E6, ent[2].locationE6.lngE6/1E6];
var layerGroup = portalsLayers[parseInt(portalLevel)];
var reRendered = false;
for(var i = 0; i < ent[2].resonatorArray.resonators.length; i++) { for(var i = 0; i < ent[2].resonatorArray.resonators.length; i++) {
var rdata = ent[2].resonatorArray.resonators[i]; var rdata = ent[2].resonatorArray.resonators[i];
if(rdata == null) continue; // skip if resonator didn't change
if(portalLayer) {
var oldRes = findEntityInLeaflet(layerGroup, window.resonators, portalResonatorGuid(ent[0], i));
if(oldRes && isSameResonator(oldRes.options.details, rdata)) continue;
}
if(window.resonators[portalResonatorGuid(ent[0],i)]) continue; // skip and remove old resonator if no new resonator
if(rdata === null) {
if(oldRes) removeByGuid(oldRes.options.guid);
continue;
}
// offset in meters // offset in meters
var dn = rdata.distanceToPortal*SLOT_TO_LAT[rdata.slot]; var dn = rdata.distanceToPortal*SLOT_TO_LAT[rdata.slot];
@ -320,7 +335,11 @@ window.renderResonators = function(ent) {
var lat0 = ent[2].locationE6.latE6/1E6 + dLat * 180/Math.PI; var lat0 = ent[2].locationE6.latE6/1E6 + dLat * 180/Math.PI;
var lon0 = ent[2].locationE6.lngE6/1E6 + dLon * 180/Math.PI; var lon0 = ent[2].locationE6.lngE6/1E6 + dLon * 180/Math.PI;
var Rlatlng = [lat0, lon0]; var Rlatlng = [lat0, lon0];
var r = L.circleMarker(Rlatlng, {
var resoGuid = portalResonatorGuid(ent[0], i);
// the resonator
var reso = L.circleMarker(Rlatlng, {
radius: 3, radius: 3,
// #AAAAAA outline seems easier to see the fill opacity // #AAAAAA outline seems easier to see the fill opacity
color: '#AAAAAA', color: '#AAAAAA',
@ -328,17 +347,40 @@ window.renderResonators = function(ent) {
weight: 1, weight: 1,
fillColor: COLORS_LVL[rdata.level], fillColor: COLORS_LVL[rdata.level],
fillOpacity: rdata.energyTotal/RESO_NRG[rdata.level], fillOpacity: rdata.energyTotal/RESO_NRG[rdata.level],
clickable: false, guid: resoGuid // need this here as well for add/remove events
});
// line connecting reso to portal
var conn = L.polyline([Rlatlng, portalLatLng], {
weight: 2,
color: '#FFFFFF',
opacity: 0.2,
dashArray: '10,4',
fill: false,
clickable: false});
// put both in one group, so they can be handled by the same logic.
var r = L.layerGroup([reso, conn]);
r.options = {
level: rdata.level, level: rdata.level,
details: rdata, details: rdata,
pDetails: ent[2], pDetails: ent[2],
guid: portalResonatorGuid(ent[0],i) }); guid: resoGuid
};
r.on('remove', function() { delete window.resonators[this.options.guid]; }); // However, LayerGroups (and FeatureGroups) dont fire add/remove
r.on('add', function() { window.resonators[this.options.guid] = this; }); // events, thus this listener will be attached to the resonator. It
// doesnt matter to which element these are bound since Leaflet
// will add/remove all elements of the LayerGroup at once.
reso.on('remove', function() { delete window.resonators[this.options.guid]; });
reso.on('add', function() { window.resonators[this.options.guid] = r; });
r.addTo(portalsLayers[parseInt(portalLevel)]); r.addTo(portalsLayers[parseInt(portalLevel)]);
reRendered = true;
} }
// if there is any resonator re-rendered, bring portal to front
if(reRendered && portalLayer) portalLayer.bringToFront();
} }
// append portal guid with -resonator-[slot] to get guid for resonators // append portal guid with -resonator-[slot] to get guid for resonators
@ -350,8 +392,17 @@ window.isResonatorsShow = function() {
return map.getZoom() >= RESONATOR_DISPLAY_ZOOM_LEVEL; return map.getZoom() >= RESONATOR_DISPLAY_ZOOM_LEVEL;
} }
window.isSameResonator = function(oldRes, newRes) {
if(!oldRes && !newRes) return true;
if(typeof oldRes !== typeof newRes) return false;
if(oldRes.level !== newRes.level) return false;
if(oldRes.energyTotal !== newRes.energyTotal) return false;
if(oldRes.distanceToPortal !== newRes.distanceToPortal) return false;
return true;
}
window.portalResetColor = function(portal) { window.portalResetColor = function(portal) {
portal.setStyle({color: portal.options.fillColor}); portal.setStyle({color: COLORS[getTeam(portal.options.details)]});
} }
// renders a link on the map from the given entity // renders a link on the map from the given entity

View File

@ -4,13 +4,14 @@
// methods that highlight the portal in the map view. // methods that highlight the portal in the map view.
window.renderPortalDetails = function(guid) { window.renderPortalDetails = function(guid) {
var d = window.portals[guid].options.details; if(!window.portals[guid]) {
if(!d) {
unselectOldPortal(); unselectOldPortal();
urlPortal = guid; urlPortal = guid;
return; return;
} }
var d = window.portals[guid].options.details;
var update = selectPortal(guid); var update = selectPortal(guid);
// collect some random data thats not worth to put in an own method // collect some random data thats not worth to put in an own method

View File

@ -1,4 +1,3 @@
// PORTAL DETAILS DISPLAY //////////////////////////////////////////// // PORTAL DETAILS DISPLAY ////////////////////////////////////////////
// hand any of these functions the details-hash of a portal, and they // hand any of these functions the details-hash of a portal, and they
// will return pretty, displayable HTML or parts thereof. // will return pretty, displayable HTML or parts thereof.
@ -56,10 +55,10 @@ window.getModDetails = function(d) {
} }
}); });
var t = '<span title="'+modsTitle[0]+'" style="color:'+modsColor[0]+'">'+mods[0]+'</span>' var t = '<span'+(modsTitle[0].length ? ' title="'+modsTitle[0]+'"' : '')+' style="color:'+modsColor[0]+'">'+mods[0]+'</span>'
+ '<span title="'+modsTitle[1]+'" style="color:'+modsColor[1]+'">'+mods[1]+'</span>' + '<span'+(modsTitle[1].length ? ' title="'+modsTitle[1]+'"' : '')+' style="color:'+modsColor[1]+'">'+mods[1]+'</span>'
+ '<span title="'+modsTitle[2]+'" style="color:'+modsColor[2]+'">'+mods[2]+'</span>' + '<span'+(modsTitle[2].length ? ' title="'+modsTitle[2]+'"' : '')+' style="color:'+modsColor[2]+'">'+mods[2]+'</span>'
+ '<span title="'+modsTitle[3]+'" style="color:'+modsColor[3]+'">'+mods[3]+'</span>' + '<span'+(modsTitle[3].length ? ' title="'+modsTitle[3]+'"' : '')+' style="color:'+modsColor[3]+'">'+mods[3]+'</span>'
return t; return t;
} }
@ -117,10 +116,10 @@ window.renderResonatorDetails = function(slot, level, nrg, dist, nick, isLeft) {
var max = RESO_NRG[level]; var max = RESO_NRG[level];
var fillGrade = nrg/max*100; var fillGrade = nrg/max*100;
var inf = 'energy:\t\t' + nrg + ' / ' + max + ' (' + Math.round(fillGrade) + '%)\n' var inf = 'energy:\t' + nrg + ' / ' + max + ' (' + Math.round(fillGrade) + '%)\n'
+ 'level:\t\t' + level + '\n' + 'level:\t' + level + '\n'
+ 'distance:\t' + dist + 'm\n' + 'distance:\t' + dist + 'm\n'
+ 'owner:\t\t' + nick + '\n' + 'owner:\t' + nick + '\n'
+ 'octant:\t' + OCTANTS[slot]; + 'octant:\t' + OCTANTS[slot];
var style = 'width:'+fillGrade+'%; background:'+COLORS_LVL[level]+';'; var style = 'width:'+fillGrade+'%; background:'+COLORS_LVL[level]+';';
@ -154,13 +153,16 @@ window.getDestroyAP = function(d) {
var resoAp = resoCount * DESTROY_RESONATOR; var resoAp = resoCount * DESTROY_RESONATOR;
var linkAp = linkCount * DESTROY_LINK; var linkAp = linkCount * DESTROY_LINK;
var fieldAp = fieldCount * DESTROY_FIELD; var fieldAp = fieldCount * DESTROY_FIELD;
var sum = resoAp + linkAp + fieldAp; var sum = resoAp + linkAp + fieldAp + CAPTURE_PORTAL + 8*DEPLOY_RESONATOR + COMPLETION_BONUS;
function tt(text) { function tt(text) {
var t = 'Destroy:\n'; var t = 'Destroy &amp; Capture:\n';
t += resoCount + '×\tResonators\t= ' + digits(resoAp) + '\n'; t += resoCount + '×\tResonators\t= ' + digits(resoAp) + '\n';
t += linkCount + '×\tLinks\t\t= ' + digits(linkAp) + '\n'; t += linkCount + '×\tLinks\t= ' + digits(linkAp) + '\n';
t += fieldCount + '×\tFields\t\t= ' + digits(fieldAp) + '\n'; t += fieldCount + '×\tFields\t= ' + digits(fieldAp) + '\n';
t += '1×\tCapture\t= ' + CAPTURE_PORTAL + '\n';
t += '8×\tDeploy\t= ' + DEPLOY_RESONATOR + '\n';
t += '1×\tBonus\t= ' + COMPLETION_BONUS + '\n';
t += 'Sum: ' + digits(sum) + ' AP'; t += 'Sum: ' + digits(sum) + ' AP';
return '<tt title="'+t+'">' + digits(text) + '</tt>'; return '<tt title="'+t+'">' + digits(text) + '</tt>';
} }

View File

@ -25,10 +25,8 @@ window.requests.abort = function() {
window.activeRequests = []; window.activeRequests = [];
window.failedRequestCount = 0; window.failedRequestCount = 0;
window.chat._requestOldPublicRunning = false; window.chat._requestPublicRunning = false;
window.chat._requestNewPublicRunning = false; window.chat._requestFactionRunning = false;
window.chat._requestOldFactionRunning = false;
window.chat._requestNewFactionRunning = false;
renderUpdateStatus(); renderUpdateStatus();
} }

BIN
dist/images/draw-circle.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
dist/images/draw-marker-icon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

BIN
dist/images/draw-polygon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

BIN
dist/images/draw-polyline.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

BIN
dist/images/draw-rectangle.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

BIN
dist/images/marker-icon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
dist/images/marker-icon@2x.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
dist/images/marker-shadow.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

118
dist/leaflet.draw.0.1.6.css vendored Normal file
View File

@ -0,0 +1,118 @@
/* Leaflet controls */
.leaflet-container .leaflet-control-draw {
margin-left: 13px;
margin-top: 12px;
}
.leaflet-control-draw a {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
width: 22px;
height: 22px;
}
.leaflet-control-draw a:hover {
background-color: #fff;
}
.leaflet-touch .leaflet-control-draw a {
width: 27px;
height: 27px;
}
.leaflet-control-draw-polyline {
background-image: url(images/draw-polyline.png);
}
.leaflet-control-draw-polygon {
background-image: url(images/draw-polygon.png);
}
.leaflet-control-draw-rectangle {
background-image: url(images/draw-rectangle.png);
}
.leaflet-control-draw-circle {
background-image: url(images/draw-circle.png);
}
.leaflet-control-draw-marker {
background-image: url(images/draw-marker-icon.png);
}
.leaflet-mouse-marker {
background-color: #fff;
cursor: crosshair;
}
.leaflet-draw-label {
background-color: #fff;
border: 1px solid #ccc;
color: #222;
font: 12px/18px "Helvetica Neue", Arial, Helvetica, sans-serif;
margin-left: 20px;
margin-top: -21px;
padding: 2px 4px;
position: absolute;
white-space: nowrap;
z-index: 6;
}
.leaflet-error-draw-label {
background-color: #F2DEDE;
border-color: #E6B6BD;
color: #B94A48;
}
.leaflet-draw-label-single {
margin-top: -12px
}
.leaflet-draw-label-subtext {
color: #999;
}
.leaflet-draw-guide-dash {
font-size: 1%;
opacity: 0.6;
position: absolute;
width: 5px;
height: 5px;
}
.leaflet-flash-anim {
-webkit-animation-duration: 0.66s;
-moz-animation-duration: 0.66s;
-o-animation-duration: 0.66s;
animation-duration: 0.66s;
-webkit-animation-fill-mode: both;
-moz-animation-fill-mode: both;
-o-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation-name: leaflet-flash;
-moz-animation-name: leaflet-flash;
-o-animation-name: leaflet-flash;
animation-name: leaflet-flash;
}
@-webkit-keyframes leaflet-flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0.3; }
}
@-moz-keyframes leaflet-flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0.3; }
}
@-o-keyframes leaflet-flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0.3; }
}
@keyframes leaflet-flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0; }
}

6
dist/leaflet.draw.0.1.6.js vendored Normal file

File diff suppressed because one or more lines are too long

10
dist/style.css vendored
View File

@ -36,10 +36,10 @@ body {
#sidebartoggle { #sidebartoggle {
display: block; display: block;
padding: 20px 5px; padding: 20px 5px;
margin-top: -31px; margin-top: -31px; /* -(toggle height / 2) */
line-height: 10px; line-height: 10px;
position: absolute; position: absolute;
top: 50%; top: 340px; /* (sidebar height / 2) */
z-index: 3001; z-index: 3001;
background-color: rgba(8, 48, 78, 0.9); background-color: rgba(8, 48, 78, 0.9);
color: #FFCE00; color: #FFCE00;
@ -561,10 +561,6 @@ aside:nth-child(odd) span {
padding: 5px; padding: 5px;
} }
#spacer {
height: 10px;
}
/* a common portal display takes this much space (prevents moving /* a common portal display takes this much space (prevents moving
* content when first selecting a portal) */ * content when first selecting a portal) */
@ -575,7 +571,7 @@ aside:nth-child(odd) span {
/* update status */ /* update status */
#updatestatus { #updatestatus {
background-color: rgba(8, 48, 78, 1); background-color: rgba(8, 48, 78, 0.9);
border-bottom: 0; border-bottom: 0;
border-top: 1px solid #20A8B1; border-top: 1px solid #20A8B1;
border-left: 1px solid #20A8B1; border-left: 1px solid #20A8B1;

118
external/leaflet.draw.0.1.6.css vendored Normal file
View File

@ -0,0 +1,118 @@
/* Leaflet controls */
.leaflet-container .leaflet-control-draw {
margin-left: 13px;
margin-top: 12px;
}
.leaflet-control-draw a {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
width: 22px;
height: 22px;
}
.leaflet-control-draw a:hover {
background-color: #fff;
}
.leaflet-touch .leaflet-control-draw a {
width: 27px;
height: 27px;
}
.leaflet-control-draw-polyline {
background-image: url(images/draw-polyline.png);
}
.leaflet-control-draw-polygon {
background-image: url(images/draw-polygon.png);
}
.leaflet-control-draw-rectangle {
background-image: url(images/draw-rectangle.png);
}
.leaflet-control-draw-circle {
background-image: url(images/draw-circle.png);
}
.leaflet-control-draw-marker {
background-image: url(images/draw-marker-icon.png);
}
.leaflet-mouse-marker {
background-color: #fff;
cursor: crosshair;
}
.leaflet-draw-label {
background-color: #fff;
border: 1px solid #ccc;
color: #222;
font: 12px/18px "Helvetica Neue", Arial, Helvetica, sans-serif;
margin-left: 20px;
margin-top: -21px;
padding: 2px 4px;
position: absolute;
white-space: nowrap;
z-index: 6;
}
.leaflet-error-draw-label {
background-color: #F2DEDE;
border-color: #E6B6BD;
color: #B94A48;
}
.leaflet-draw-label-single {
margin-top: -12px
}
.leaflet-draw-label-subtext {
color: #999;
}
.leaflet-draw-guide-dash {
font-size: 1%;
opacity: 0.6;
position: absolute;
width: 5px;
height: 5px;
}
.leaflet-flash-anim {
-webkit-animation-duration: 0.66s;
-moz-animation-duration: 0.66s;
-o-animation-duration: 0.66s;
animation-duration: 0.66s;
-webkit-animation-fill-mode: both;
-moz-animation-fill-mode: both;
-o-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation-name: leaflet-flash;
-moz-animation-name: leaflet-flash;
-o-animation-name: leaflet-flash;
animation-name: leaflet-flash;
}
@-webkit-keyframes leaflet-flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0.3; }
}
@-moz-keyframes leaflet-flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0.3; }
}
@-o-keyframes leaflet-flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0.3; }
}
@keyframes leaflet-flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0; }
}

6
external/leaflet.draw.0.1.6.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
images/draw-circle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
images/draw-marker-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

BIN
images/draw-polygon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

BIN
images/draw-polyline.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

BIN
images/draw-rectangle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

BIN
images/marker-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
images/marker-icon@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
images/marker-shadow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

View File

@ -0,0 +1,49 @@
[
"431a30b634394956a16e62954222a37d.d",
1360634878999,
{
"plext": {
"text": "Control Field @In the fishing village ( Lomse (Oktyabr'skaya ulitsa, 1, Kaliningrad, Kaliningrad Oblast, Russia) has decayed -108 MUs",
"markup": [
[
"TEXT",
{
"plain": "Control Field @"
}
],
[
"PORTAL",
{
"name": "In the fishing village ( Lomse",
"plain": "In the fishing village ( Lomse (Oktyabr'skaya ulitsa, 1, Kaliningrad, Kaliningrad Oblast, Russia)",
"team": "ALIENS",
"latE6": 54705182,
"address": "Oktyabr'skaya ulitsa, 1, Kaliningrad, Kaliningrad Oblast, Russia",
"lngE6": 20514959,
"guid": "6af16d09fb574c989b7fa09e718585a7.12"
}
],
[
"TEXT",
{
"plain": " has decayed -"
}
],
[
"TEXT",
{
"plain": "108"
}
],
[
"TEXT",
{
"plain": " MUs"
}
]
],
"plextType": "SYSTEM_BROADCAST",
"team": "NEUTRAL"
}
}
]

71
main.js
View File

@ -62,12 +62,14 @@ document.getElementsByTagName('head')[0].innerHTML = ''
document.getElementsByTagName('body')[0].innerHTML = '' document.getElementsByTagName('body')[0].innerHTML = ''
+ '<div id="map">Loading, please wait</div>' + '<div id="map">Loading, please wait</div>'
+ '<div id="chatcontrols" style="display:none">' + '<div id="chatcontrols" style="display:none">'
+ ' <a><span class="toggle expand"></span></a><a>automated</a><a>public</a><a class="active">faction</a>' + ' <a><span class="toggle expand"></span></a>'
+ '<a>full</a><a>compact</a><a>public</a><a class="active">faction</a>'
+ '</div>' + '</div>'
+ '<div id="chat" style="display:none">' + '<div id="chat" style="display:none">'
+ ' <div id="chatfaction"></div>' + ' <div id="chatfaction"></div>'
+ ' <div id="chatpublic"></div>' + ' <div id="chatpublic"></div>'
+ ' <div id="chatautomated"></div>' + ' <div id="chatcompact"></div>'
+ ' <div id="chatfull"></div>'
+ '</div>' + '</div>'
+ '<form id="chatinput" style="display:none"><time></time><span>tell faction:</span><input type="text"/></form>' + '<form id="chatinput" style="display:none"><time></time><span>tell faction:</span><input type="text"/></form>'
+ '<a id="sidebartoggle"><span class="toggle close"></span></a>' + '<a id="sidebartoggle"><span class="toggle close"></span></a>'
@ -79,7 +81,6 @@ document.getElementsByTagName('body')[0].innerHTML = ''
+ ' <div id="portaldetails"></div>' + ' <div id="portaldetails"></div>'
+ ' <input id="redeem" placeholder="Redeem code…" type="text"/>' + ' <input id="redeem" placeholder="Redeem code…" type="text"/>'
+ ' <div id="toolbox"><a onmouseover="setPermaLink(this)">permalink</a></div>' + ' <div id="toolbox"><a onmouseover="setPermaLink(this)">permalink</a></div>'
+ ' <div id="spacer"></div>'
+ ' </div>' + ' </div>'
+ '</div>' + '</div>'
+ '<div id="updatestatus"></div>'; + '<div id="updatestatus"></div>';
@ -96,47 +97,55 @@ function wrapper() {
L_PREFER_CANVAS = false; L_PREFER_CANVAS = false;
// CONFIG OPTIONS //////////////////////////////////////////////////// // CONFIG OPTIONS ////////////////////////////////////////////////////
var REFRESH = 30; // refresh view every 30s (base time) window.REFRESH = 30; // refresh view every 30s (base time)
var ZOOM_LEVEL_ADJ = 5; // add 5 seconds per zoom level window.ZOOM_LEVEL_ADJ = 5; // add 5 seconds per zoom level
var REFRESH_GAME_SCORE = 5*60; // refresh game score every 5 minutes window.REFRESH_GAME_SCORE = 5*60; // refresh game score every 5 minutes
var MAX_IDLE_TIME = 4; // stop updating map after 4min idling window.MAX_IDLE_TIME = 4; // stop updating map after 4min idling
var PRECACHE_PLAYER_NAMES_ZOOM = 17; // zoom level to start pre-resolving player names window.PRECACHE_PLAYER_NAMES_ZOOM = 17; // zoom level to start pre-resolving player names
var HIDDEN_SCROLLBAR_ASSUMED_WIDTH = 20; window.HIDDEN_SCROLLBAR_ASSUMED_WIDTH = 20;
var SIDEBAR_WIDTH = 300; window.SIDEBAR_WIDTH = 300;
// chat messages are requested for the visible viewport. On high zoom // chat messages are requested for the visible viewport. On high zoom
// levels this gets pretty pointless, so request messages in at least a // levels this gets pretty pointless, so request messages in at least a
// X km radius. // X km radius.
var CHAT_MIN_RANGE = 6; window.CHAT_MIN_RANGE = 6;
// this controls how far data is being drawn outside the viewport. Set // this controls how far data is being drawn outside the viewport. Set
// it 0 to only draw entities that intersect the current view. A value // it 0 to only draw entities that intersect the current view. A value
// of one will render an area twice the size of the viewport (or some- // of one will render an area twice the size of the viewport (or some-
// thing like that, Leaflet doc isnt too specific). Setting it too low // thing like that, Leaflet doc isnt too specific). Setting it too low
// makes the missing data on move/zoom out more obvious. Setting it too // makes the missing data on move/zoom out more obvious. Setting it too
// high causes too many items to be drawn, making drag&drop sluggish. // high causes too many items to be drawn, making drag&drop sluggish.
var VIEWPORT_PAD_RATIO = 0.3; window.VIEWPORT_PAD_RATIO = 0.3;
// how many items to request each query // how many items to request each query
var CHAT_PUBLIC_ITEMS = 200 window.CHAT_PUBLIC_ITEMS = 200;
var CHAT_FACTION_ITEMS = 50 window.CHAT_FACTION_ITEMS = 50;
// how many pixels to the top before requesting new data
window.CHAT_REQUEST_SCROLL_TOP = 200;
window.CHAT_SHRINKED = 60;
// Leaflet will get very slow for MANY items. Its better to display // Leaflet will get very slow for MANY items. Its better to display
// only some instead of crashing the browser. // only some instead of crashing the browser.
var MAX_DRAWN_PORTALS = 1000; window.MAX_DRAWN_PORTALS = 1000;
var MAX_DRAWN_LINKS = 400; window.MAX_DRAWN_LINKS = 400;
var MAX_DRAWN_FIELDS = 200; window.MAX_DRAWN_FIELDS = 200;
// Minimum zoom level resonator will display
window.RESONATOR_DISPLAY_ZOOM_LEVEL = 17;
window.COLOR_SELECTED_PORTAL = '#f00';
var COLOR_SELECTED_PORTAL = '#f00'; window.COLORS = ['#FFCE00', '#0088FF', '#03FE03']; // none, res, enl
var COLORS = ['#FFCE00', '#0088FF', '#03FE03']; // none, res, enl window.COLORS_LVL = ['#000', '#FECE5A', '#FFA630', '#FF7315', '#E40000', '#FD2992', '#EB26CD', '#C124E0', '#9627F4'];
var COLORS_LVL = ['#000', '#FECE5A', '#FFA630', '#FF7315', '#E40000', '#FD2992', '#EB26CD', '#C124E0', '#9627F4']; window.COLORS_MOD = {VERY_RARE: '#F78AF6', RARE: '#AD8AFF', COMMON: '#84FBBD'};
var COLORS_MOD = {VERY_RARE: '#F78AF6', RARE: '#AD8AFF', COMMON: '#84FBBD'};
// circles around a selected portal that show from where you can hack // circles around a selected portal that show from where you can hack
// it and how far the portal reaches (i.e. how far links may be made // it and how far the portal reaches (i.e. how far links may be made
// from this portal) // from this portal)
var ACCESS_INDICATOR_COLOR = 'orange'; window.ACCESS_INDICATOR_COLOR = 'orange';
var RANGE_INDICATOR_COLOR = 'red'; window.RANGE_INDICATOR_COLOR = 'red';
window.DEFAULT_PORTAL_IMG = 'http://commondatastorage.googleapis.com/ingress/img/default-portal-image.png';
window.NOMINATIM = 'http://nominatim.openstreetmap.org/search?format=json&limit=1&q=';
// INGRESS CONSTANTS ///////////////////////////////////////////////// // INGRESS CONSTANTS /////////////////////////////////////////////////
// http://decodeingress.me/2012/11/18/ingress-portal-levels-and-link-range/ // http://decodeingress.me/2012/11/18/ingress-portal-levels-and-link-range/
@ -145,28 +154,22 @@ 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 MIN_AP_FOR_LEVEL = [0, 10000, 30000, 70000, 150000, 300000, 600000, 1200000];
var HACK_RANGE = 40; // in meters, max. distance from portal to be able to access it var 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 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_RESONATOR = 75; //AP for destroying portal
var DESTROY_LINK = 187; //AP for destroying link var DESTROY_LINK = 187; //AP for destroying link
var DESTROY_FIELD = 750; //AP for destroying field var DESTROY_FIELD = 750; //AP for destroying field
var CAPTURE_PORTAL = 500; //AP for capturing a portal
var DEPLOY_RESONATOR = 125; //AP for deploying a resonator
var COMPLETION_BONUS = 250; //AP for deploying all resonators on portal
// OTHER MORE-OR-LESS CONSTANTS ////////////////////////////////////// // 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_NONE = 0, TEAM_RES = 1, TEAM_ENL = 2;
var TEAM_TO_CSS = ['none', 'res', 'enl']; 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; 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_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 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; var EARTH_RADIUS=6378137;
var DEG2RAD = Math.PI / 180;
// STORAGE /////////////////////////////////////////////////////////// // STORAGE ///////////////////////////////////////////////////////////
// global variables used for storage. Most likely READ ONLY. Proper // global variables used for storage. Most likely READ ONLY. Proper

View File

@ -3,7 +3,7 @@
./build.py ./build.py
cp iitc-debug.user.js dist/total-conversion-build.user.js cp iitc-debug.user.js dist/total-conversion-build.user.js
cp style.css dist/style.css cp style.css dist/style.css
cp external/leaflet_google.js dist/leaflet_google.js cp external/* dist/
cp external/autolink.js dist/autolink.js cp -r images/ dist/images
echo 'Change path of style.css to dist/style.css' echo 'Change path of style.css to dist/style.css'

View File

@ -10,17 +10,25 @@ Available Plugins
----------------- -----------------
- [**Guess Player Level**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/guess-player-levels.user.js) looks for the highest placed resonator per player in the current view to guess the player level. - [**Guess Player Level**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/guess-player-levels.user.js) looks for the highest placed resonator per player in the current view to guess the player level.
- [**Highlight Weakened Portals**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/show-portal-weakness.user.js) fill portals with red or orange to indicate portal's state of disrepair. The brighter the color the more attention needed (recharge, shields, resonators). A dashed portal means a resonator is missing.
- [**Draw Tools**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/draw-tools.user.js) allows to draw circles and lines on the map to aid you with planning your next big field.
Hacking Hacking
------- -------
Plugins may be developed in the same way as the total conversion script. Plugins may provide features tailored to specific needs and are allowed to change things as they see fit. You can provide them separately oder submit a pull request to have them managed in this repository. There are currently no hooks that allow integration with the main script, but I will add those if the need arises. Simply open a bug report. Plugins may be developed in the same way as the total conversion script. Plugins may provide features tailored to specific needs and are allowed to change things as they see fit. You can provide them separately or submit a pull request to have them managed in this repository.
If you think a hook in the main script is required, simply open a bug report.
You can use the guess player level script as an example to get you started. Just update the names and the part between `// PLUGIN START` and `// PLUGIN END` and you should be able to develop your plugin. The other code ensures your plugin is executed after the main script. You can use the guess player level script as an example to get you started. Just update the names and the part between `// PLUGIN START` and `// PLUGIN END` and you should be able to develop your plugin. The other code ensures your plugin is executed after the main script.
If you happen the write general purpose functions for your plugin, consider adding them to the main script instead. For example, if you write a `getResoCountFromPortal(details)` function it may be very well added to `code/portal_info.js`. If you happen the write general purpose functions for your plugin, consider adding them to the main script instead. For example, if you write a `getResoCountFromPortal(details)` function it may be very well added to `code/portal_info.js`.
External Dependencies
---------------------
If you have external dependencies put them into `external/` and add a version number to their filename. I will put them in `dist/` once required. Dont forget to add a note about author and license in main `README.md`.
Available Hooks Available Hooks
--------------- ---------------

211
plugins/draw-tools.user.js Normal file
View File

@ -0,0 +1,211 @@
// ==UserScript==
// @id iitc-plugin-draw-tools@breunigs
// @name iitc: draw tools
// @version 0.1
// @namespace https://github.com/breunigs/ingress-intel-total-conversion
// @updateURL https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/draw-tools.user.js
// @downloadURL https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/draw-tools.user.js
// @description Allows you to draw things into the current map so you may plan your next move
// @include http://www.ingress.com/intel*
// @match http://www.ingress.com/intel*
// ==/UserScript==
function wrapper() {
// ensure plugin framework is there, even if iitc is not yet loaded
if(typeof window.plugin !== 'function') window.plugin = function() {};
// PLUGIN START ////////////////////////////////////////////////////////
var DRAW_TOOLS_SHAPE_OPTIONS = {
color: '#BB56FF',
fill: false,
opacity: 1,
weight: 4,
clickable: false
};
// use own namespace for plugin
window.plugin.drawTools = function() {};
window.plugin.drawTools.loadExternals = function() {
var base = 'http://breunigs.github.com/ingress-intel-total-conversion/dist';
//~ var base = 'http://0.0.0.0:8000/dist';
$('head').append('<link rel="stylesheet" href="'+base+'/leaflet.draw.0.1.6.css" />');
load(base+'/leaflet.draw.0.1.6.js').thenRun(window.plugin.drawTools.boot);
// overwrite default Leaflet Marker icon.
L.Icon.Default.imagePath = base + '/images';
}
window.plugin.drawTools.addStyles = function() {
$('head').append('<style>.leaflet-control-draw a { color: #222; text-decoration: none; text-align: center; font-size: 17px; line-height: 22px; }</style>');
}
// renders buttons which are not originally part of Leaflet.draw, such
// as the clear-drawings button.
window.plugin.drawTools.addCustomButtons = function() {
$('.leaflet-control-draw .leaflet-bar-part-bottom').removeClass('leaflet-bar-part-bottom');
var undo = $('<a class="leaflet-bar-part" title="undo last" href="#">⎌</a>')
.click(function() {
var last = null;
window.plugin.drawTools.drawnItems.eachLayer(function(l) {
last = l;
});
if(last) window.plugin.drawTools.drawnItems.removeLayer(last);
}
);
var clear = $('<a class="leaflet-bar-part leaflet-bar-part-bottom" title="clear drawings" href="#">✗</a>')
.click(function() {
window.plugin.drawTools.drawnItems.clearLayers();
}
);
$('.leaflet-control-draw').append(undo).append(clear);
}
// renders the draw control buttons in the top left corner
window.plugin.drawTools.addDrawControl = function() {
var drawControl = new L.Control.Draw({
rectangle: false,
polygon: false,
polyline: {
shapeOptions: DRAW_TOOLS_SHAPE_OPTIONS
},
circle: {
shapeOptions: DRAW_TOOLS_SHAPE_OPTIONS
}
});
map.addControl(drawControl);
plugin.drawTools.addCustomButtons();
}
// hacks into circle to render the radius of the circle while drawing
// and to allow the center of the circle to snap to a nearby portal.
window.plugin.drawTools.enhanceCircle = function() {
// replace _onMouseMove function to also display the radius of the
// circle
L.Circle.Draw.prototype._onMouseMove = function (e) {
var layerPoint = e.layerPoint,
latlng = e.latlng;
this._updateLabelPosition(layerPoint);
if (this._isDrawing) {
var dist = this._startLatLng.distanceTo(latlng);
dist = dist > 1000
? (dist / 1000).toFixed(2) + ' km'
: Math.ceil(dist) + ' m';
this._updateLabelText({
text: 'Release mouse to finish drawing. ',
subtext: 'radius: ' +dist }
);
this._drawShape(latlng);
}
}
// replace _onMouseDown to implement snapping
L.Circle.Draw.prototype._onMouseDown = function (e) {
this._isDrawing = true;
var snapTo = window.plugin.drawTools.getSnapLatLng(e.containerPoint);
this._startLatLng = snapTo || e.latlng;
L.DomEvent
.on(document, 'mouseup', this._onMouseUp, this)
.preventDefault(e.originalEvent);
}
}
// hacks into PolyLine to implement snapping and to remove the polyline
// markers when they are not required anymore for finishing the line.
// Otherwise they get in the way and make it harder to create a closed
// polyline.
window.plugin.drawTools.enhancePolyLine = function() {
// hack in snapping
L.Polyline.Draw.prototype._onClickOld = L.Polyline.Draw.prototype._onClick;
L.Polyline.Draw.prototype._onClick = function(e) {
var cp = map.latLngToContainerPoint(e.target.getLatLng());
var snapTo = window.plugin.drawTools.getSnapLatLng(cp);
if(snapTo) e.target._latlng = snapTo;
return this._onClickOld(e);
}
// remove polyline markers because they get in the way
L.Polyline.Draw.prototype._updateMarkerHandler = function() {
if(!this._markers) return;
if(this._markers.length >= 2)
this._markerGroup.removeLayer(this._markers.shift());
if(this._markers.length >= 1)
this._markers[this._markers.length - 1].on('click', this._finishShape, this);
}
}
// given a container point it tries to find the most suitable portal to
// snap to. It takes the CircleMarkers radius and weight into account.
// Will return null if nothing to snap to or a LatLng instance.
window.plugin.drawTools.getSnapLatLng = function(containerPoint) {
var candidates = [];
$.each(window.portals, function(guid, portal) {
var ll = portal.getLatLng();
var pp = map.latLngToContainerPoint(ll);
var size = portal.options.weight + portal.options.radius;
var dist = pp.distanceTo(containerPoint);
if(dist > size) return true;
candidates.push([dist, ll]);
});
if(candidates.length === 0) return;
candidates = candidates.sort(function(a, b) { return a[0]-b[0]; });
return candidates[0][1];
}
window.plugin.drawTools.boot = function() {
plugin.drawTools.enhanceCircle();
plugin.drawTools.enhancePolyLine();
plugin.drawTools.addStyles();
plugin.drawTools.addDrawControl();
window.plugin.drawTools.drawnItems = new L.LayerGroup();
var drawnItems = window.plugin.drawTools.drawnItems;
map.on('draw:poly-created', function (e) {
drawnItems.addLayer(e.poly);
});
map.on('draw:circle-created', function (e) {
drawnItems.addLayer(e.circ);
});
map.on('draw:marker-created', function (e) {
drawnItems.addLayer(e.marker);
e.marker.dragging.enable();
});
window.layerChooser.addOverlay(drawnItems, 'Drawn Items');
map.addLayer(drawnItems);
}
var setup = window.plugin.drawTools.loadExternals;
// PLUGIN END //////////////////////////////////////////////////////////
if(window.iitcLoaded && typeof setup === 'function') {
setup();
} else {
if(window.bootPlugins)
window.bootPlugins.push(setup);
else
window.bootPlugins = [setup];
}
} // wrapper end
// inject code into site context
var script = document.createElement('script');
script.appendChild(document.createTextNode('('+ wrapper +')();'));
(document.body || document.head || document.documentElement).appendChild(script);

View File

@ -0,0 +1,132 @@
// ==UserScript==
// @id iitc-plugin-show-portal-weakness@vita10gy
// @name iitc: show portal weakness
// @version 0.2
// @namespace https://github.com/breunigs/ingress-intel-total-conversion
// @updateURL https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/show-portal-weekness.user.js
// @downloadURL https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/show-portal-weekness.user.js
// @description Uses the fill color of the portals to denote if the portal is weak (Needs recharging, missing a resonator, needs shields)
// @include http://www.ingress.com/intel*
// @match http://www.ingress.com/intel*
// ==/UserScript==
function wrapper() {
// ensure plugin framework is there, even if iitc is not yet loaded
if(typeof window.plugin !== 'function') window.plugin = function() {};
// PLUGIN START ////////////////////////////////////////////////////////
// use own namespace for plugin
window.plugin.portalWeakness = function() {};
window.plugin.portalWeakness.getPortalWeaknessFactor = function(d)
{
var portal_weakness = 0;
if(getTeam(d) != 0)
{
if(window.getTotalPortalEnergy(d)> 0 && window.getCurrentPortalEnergy(d) < window.getTotalPortalEnergy(d))
{
portal_weakness = 1 - (window.getPortalEnergy(d)/window.getTotalPortalEnergy(d));
}
//Ding the portal for every missing sheild.
$.each(d.portalV2.linkedModArray, function(ind, mod)
{
if(mod == null)
{
portal_weakness += .05;
}
});
//Ding the portal for every missing resonator.
var resCount = 0;
$.each(d.resonatorArray.resonators, function(ind, reso)
{
if(reso == null) {
portal_weakness += .125;
}
else {
resCount++;
}
});
if(portal_weakness<0) {
portal_weakness = 0;
}
if(portal_weakness>1)
{
portal_weakness = 1;
}
}
return(Math.round(portal_weakness*100)/100);
}
window.plugin.portalWeakness.portalAdded = function(data) {
var d = data.portal.options.details;
var portal_weakness = 0;
if(getTeam(d) != 0)
{
if(window.getTotalPortalEnergy(d)> 0 && window.getCurrentPortalEnergy(d) < window.getTotalPortalEnergy(d))
{
portal_weakness = 1 - (window.getCurrentPortalEnergy(d)/window.getTotalPortalEnergy(d));
}
//Ding the portal for every missing sheild.
$.each(d.portalV2.linkedModArray, function(ind, mod)
{
if(mod == null)
{
portal_weakness += .03;
}
});
//Ding the portal for every missing resonator.
var resCount = 0;
$.each(d.resonatorArray.resonators, function(ind, reso)
{
if(reso == null) {
portal_weakness += .125;
}
else {
resCount++;
}
});
if(portal_weakness<0) {
portal_weakness = 0;
}
if(portal_weakness>1)
{
portal_weakness = 1;
}
if(portal_weakness>0)
{
var color = 'red';
var fill_opacity = Math.round((portal_weakness*.8 + .2)*100)/100;
var params = {fillColor: color, fillOpacity: fill_opacity, radius: data.portal.options.radius+1};
if(resCount<8)
{
params["dashArray"] = "4,6";
}
data.portal.setStyle(params);
}
}
}
var setup = function() {
window.addHook('portalAdded', window.plugin.portalWeakness.portalAdded);
window.COLOR_SELECTED_PORTAL = '#f0f';
}
// PLUGIN END //////////////////////////////////////////////////////////
if(window.iitcLoaded && typeof setup === 'function') {
setup();
} else {
if(window.bootPlugins)
window.bootPlugins.push(setup);
else
window.bootPlugins = [setup];
}
} // wrapper end
// inject code into site context
var script = document.createElement('script');
script.appendChild(document.createTextNode('('+ wrapper +')();'));
(document.body || document.head || document.documentElement).appendChild(script);

View File

@ -2,7 +2,7 @@
body:after { body:after {
background: none repeat scroll 0 0 #FFFFFF; background: none repeat scroll 0 0 #FFFFFF;
color: red; color: red;
content: "Please update your total conversion script or it will not work properly anymore."; content: "Please update your total conversion script or it will not work properly anymore. \a Google \"ingress total conversion\".";
display: block; display: block;
font-weight: bold; font-weight: bold;
left: 381px; left: 381px;
@ -10,6 +10,8 @@ body:after {
position: absolute; position: absolute;
top: 0; top: 0;
z-index: 9001; z-index: 9001;
white-space: pre;
text-align: center;
} }
/* general rules ******************************************************/ /* general rules ******************************************************/
@ -50,10 +52,10 @@ body {
#sidebartoggle { #sidebartoggle {
display: block; display: block;
padding: 20px 5px; padding: 20px 5px;
margin-top: -31px; margin-top: -31px; /* -(toggle height / 2) */
line-height: 10px; line-height: 10px;
position: absolute; position: absolute;
top: 50%; top: 340px; /* (sidebar height / 2) */
z-index: 3001; z-index: 3001;
background-color: rgba(8, 48, 78, 0.9); background-color: rgba(8, 48, 78, 0.9);
color: #FFCE00; color: #FFCE00;
@ -189,7 +191,7 @@ em {
top: 25px; top: 25px;
} }
#chatpublic, #chatautomated { #chatpublic, #chatfull, #chatcompact, /* FIXME DEPRECATED: */#chatautomated {
display: none; display: none;
} }
@ -224,6 +226,7 @@ time {
display: inline-block; display: inline-block;
width: 44px; width: 44px;
color: #bbb; color: #bbb;
overflow: hidden;
} }
mark { mark {
@ -447,7 +450,7 @@ h3 {
border: 1px solid #666; border: 1px solid #666;
} }
.mods span[title=""] { .mods span:not([title]) {
cursor: auto; cursor: auto;
} }
@ -575,10 +578,6 @@ aside:nth-child(odd) span {
padding: 5px; padding: 5px;
} }
#spacer {
height: 10px;
}
/* a common portal display takes this much space (prevents moving /* a common portal display takes this much space (prevents moving
* content when first selecting a portal) */ * content when first selecting a portal) */
@ -589,7 +588,7 @@ aside:nth-child(odd) span {
/* update status */ /* update status */
#updatestatus { #updatestatus {
background-color: rgba(8, 48, 78, 1); background-color: rgba(8, 48, 78, 0.9);
border-bottom: 0; border-bottom: 0;
border-top: 1px solid #20A8B1; border-top: 1px solid #20A8B1;
border-left: 1px solid #20A8B1; border-left: 1px solid #20A8B1;
@ -617,3 +616,23 @@ aside:nth-child(odd) span {
#largepreview img { #largepreview img {
border: 2px solid #f8ff5e; border: 2px solid #f8ff5e;
} }
/* tooltips */
.ui-tooltip {
max-width: 300px;
position: absolute;
z-index: 9999;
background-color: #fff;
border: 1px solid #ccc;
color: #222;
font: 13px/15px "Helvetica Neue", Arial, Helvetica, sans-serif;
padding: 2px 4px;
}
td {
padding: 0;
}
td + td {
padding-left: 4px;
}