From a76f96c3fb36bec31e4d6d5beb438f4b346fee6b Mon Sep 17 00:00:00 2001 From: Stefan Breunig Date: Sat, 23 Feb 2013 14:19:31 +0100 Subject: [PATCH] release 0.7. --- NEWS.md | 46 +- README.md | 4 +- dist/smartphone.0.7.css | 110 ++++ dist/style.0.7.css | 713 ++++++++++++++++++++++++ dist/total-conversion-build.user.js | 829 +++++++++++++++++++++------- plugins/README.md | 6 +- 6 files changed, 1489 insertions(+), 219 deletions(-) create mode 100644 dist/smartphone.0.7.css create mode 100644 dist/style.0.7.css diff --git a/NEWS.md b/NEWS.md index 45e0205e..54d4041e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,49 @@ +CHANGES IN 0.7 +============== + +### General +- from now on there will be [nightly builds](https://www.dropbox.com/sh/lt9p0s40kt3cs6m/3xzpyiVBnF) available. You need to manually update them if you want to stay on nightly. You should be offered to update to the next release version, though. Be sure to [have read the guide on how to report bugs](https://github.com/breunigs/ingress-intel-total-conversion/blob/gh-pages/HACKING.md#how-do-i-report-bugs) before using a nightly version. +- IITC has [a shiny new user guide now](https://github.com/breunigs/ingress-intel-total-conversion/blob/gh-pages/USERGUIDE.md). Please point new users to it, it should answer most of their questions and also teach them how to make good bug reports. + +### Main Script +- Feature: resonators for the selected portal are now highlighted (by Xelio) +- Feature: resonator charge percentage shown in tooltip (by Xelio) +- Feature: link to Google Maps for each portal (by vita10gy) +- Change: Update wording for redeeming to match vanilla Ingress Intel. +- Change: recommend Tampermonkey for Chrome users. It makes everything easier. +- Change: portal image is now shrinked to fit in, instead of cut off +- Change: use the same jQuery version as the vanilla Intel map. +- Change: replaced native `alert` dialogs with own implementation. Should avoid overflowing or unaligned texts. +- Bugfix: IITC would not display any portals/data for some people. **If you were affected by the “empty map” problem try the new version.** +- Bugfix: selected portal would be unselected on certain conditions +- Bugfix: portals were not clickable below the sidebar +- Bugfix: map wasn’t rendered properly sometimes (only a gray area was shown) +- Bugfix: resonators were duplicated sometimes +- Bugfix: AP calulation was wrong +- Bugfix: Permalink gave the wrong zoom level +- Bugfix: zoom position not saved sometimes + +### IITC Plugins + +**New Plugins:** +- Render limit increase for people with beefy hardware (by Jon Atkins) +- Render resonators earlier (by Xelio) +- Player tracker +- compute AP stats for current view (by Hollow011) +- show portal address in sidebar (by vita10gy) + +**Updated:** +- the guess players plugin now groups and sorts by level. It also remembers the players now, so zooming in won’t make a player “lower level”. + +[You can obtain them in the plugins directory](https://github.com/breunigs/ingress-intel-total-conversion/tree/gh-pages/plugins#readme). + +### IITC Mobile + +An alpha quality **developer only** preview of IITC for mobile devices is available. [For more information see the guide in the mobile section](https://github.com/breunigs/ingress-intel-total-conversion/tree/gh-pages/mobile#readme). + + CHANGES IN 0.6 / 0.61 -===================== +--------------------- 0.6 had a broken link to style sheets. Fixed in 0.61. diff --git a/README.md b/README.md index b7675bfe..c29ade7c 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ IITC can be [extended with the use of plugins](https://github.com/breunigs/ingre Install ------- -Current version is 0.61. [See NEWS.md for details](https://github.com/breunigs/ingress-intel-total-conversion/blob/gh-pages/NEWS.md) . **THIS VERSION CONTAINS A SECURITY UPDATE.** Please update as soon as possible and also alert friends about it. +Current version is 0.7. [See NEWS.md for details](https://github.com/breunigs/ingress-intel-total-conversion/blob/gh-pages/NEWS.md). [**INSTALL**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/dist/total-conversion-build.user.js) @@ -30,7 +30,7 @@ Current version is 0.61. [See NEWS.md for details](https://github.com/breunigs/i - Confirm security question. - Reload page. -*NoScript:* The newest, not yet released version appears to work with NoScript. Until it is released disable NoScript if you have problems. To make the script work whitelist at least these domains: `ingress.com github.com leafletjs.com googleapis.com`. If you want to see the cool font also whitelist `googleusercontent.com`. +*NoScript:* To make the script work whitelist at least these domains: `ingress.com github.com leafletjs.com googleapis.com`. If you want to see the cool font also whitelist `googleusercontent.com`. ### Chrome diff --git a/dist/smartphone.0.7.css b/dist/smartphone.0.7.css new file mode 100644 index 00000000..b04c1b21 --- /dev/null +++ b/dist/smartphone.0.7.css @@ -0,0 +1,110 @@ +body { + background: #000; + color: #fff; +} + +#sidebar, #updatestatus, #chatcontrols, #chat, #chatinput { + background: #0B3351 !important +} + + +.leaflet-control-layers { + margin-left: 0 !important; + margin-top: 40px !important; +} + +#chatcontrols { + height: 38px; +} + +/* hide shrink button */ +#chatcontrols a:first-child { + display: none; +} + +#chatcontrols a { + width: 50px; + height:36px; + overflow: hidden; + vertical-align: middle; + line-height: 36px; + text-decoration: none; +} + +#chat { + left:0; + right:0; + top:37px !important; + bottom:30px; + width: auto; +} + +#chatinput { + width: 100%; + height: 30px; +} + +#chat td:nth-child(2), #chatinput td:nth-child(2) { + width: 77px; +} + + + + +#sidebartoggle { + display: none !important; +} + +#scrollwrapper { + top: 36px; + bottom: 0; + max-height: none !important; + width: 100% !important; + right: 0; + left:0; +} + +#sidebar { + width: 100% !important; + min-height: 100%; + border:0; +} + +#sidebar > * { + width: 100%; +} + +#playerstat { + margin-top: 5px; +} + +#portaldetails { + min-height: 0; +} + +.fullimg { + width: 100%; +} + +.leaflet-control-layers-base { + float: left; +} + +.leaflet-control-layers-overlays { + float: left; + margin-left: 8px; + border-left: 1px solid #DDDDDD; + padding-left: 8px; +} + +.leaflet-control-layers-separator { + display: none; +} + +.leaflet-control-layers-list label { + padding: 6px 0; +} + +.leaflet-control-attribution { + +} diff --git a/dist/style.0.7.css b/dist/style.0.7.css new file mode 100644 index 00000000..43bc610c --- /dev/null +++ b/dist/style.0.7.css @@ -0,0 +1,713 @@ +/* general rules ******************************************************/ + +html, body, #map { + height: 100%; + width: 100%; +} + +body { + font-size: 14px; + font-family: "coda",arial,helvetica,sans-serif; + margin: 0; +} + +#scrollwrapper { + overflow: hidden; + position: fixed; + right: -38px; + top: 0; + width: 340px; + bottom: 45px; + z-index: 1001; +} + +#sidebar { + background-color: rgba(8, 48, 78, 0.9); + border-left: 1px solid #20A8B1; + color: #888; + position: relative; + left: 0; + top: 0; + max-height: 100%; + overflow-y:scroll; + overflow-x:hidden; + z-index: 3000; +} + +#sidebartoggle { + display: block; + padding: 20px 5px; + margin-top: -31px; /* -(toggle height / 2) */ + line-height: 10px; + position: absolute; + top: 340px; /* (sidebar height / 2) */ + z-index: 3001; + background-color: rgba(8, 48, 78, 0.9); + color: #FFCE00; + border: 1px solid #20A8B1; + border-right: none; + border-radius: 5px 0 0 5px; + text-decoration: none; +} + +.enl { + color: #03fe03 !important; +} + +.res { + color: #00c5ff !important; +} + +.none { + color: #fff; +} + +a { + color: #ffce00; + cursor: pointer; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* map display, required because GMaps uses a high z-index which is + * normally above Leaflet’s vector pane */ +.leaflet-map-pane { + z-index: 1000; +} + +.leaflet-control-layers-overlays label.disabled { + text-decoration: line-through; + cursor: help; +} + +.help { + cursor: help; +} + +.toggle { + display: block; + height: 0; + width: 0; +} + + +/* chat ***************************************************************/ + +#chatcontrols { + color: #FFCE00; + background: rgba(8, 48, 78, 0.9); + position: absolute; + left: 0; + z-index: 3001; + height: 26px; + padding-left:1px; +} + +#chatcontrols.expand { + top: 0; + bottom: auto; +} + +#chatcontrols a { + margin-left: -1px; + display: inline-block; + width: 94px; + text-align: center; + height: 24px; + line-height: 24px; + border: 1px solid #20A8B1; + vertical-align: top; +} + +#chatcontrols a:first-child { + letter-spacing:-1px; + text-decoration: none !important; +} + +#chatcontrols a.active { + border-color: #FFCE00; + border-bottom-width:0px; + font-weight:bold +} + +#chatcontrols a.active + a { + border-left-color: #FFCE00 +} + + +#chatcontrols .toggle { + border-left: 10px solid transparent; + border-right: 10px solid transparent; + margin: 6px auto auto; +} + +#chatcontrols .expand { + border-bottom: 10px solid #FFCE00; +} + +#chatcontrols .shrink { + border-top: 10px solid #FFCE00; +} + + +#chat { + position: absolute; + width: 708px; + bottom: 23px; + left: 0; + z-index: 3000; + background: rgba(8, 48, 78, 0.9); + font-size: 12.6px; + color: #eee; + border: 1px solid #20A8B1; + border-bottom: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +em { + color: red; + font-style: normal; +} + +#chat.expand { + height:auto; + top: 25px; +} + +#chatpublic, #chatfull, #chatcompact { + display: none; +} + +#chat > div { + overflow-x:hidden; + overflow-y:scroll; + height: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 2px; + position:relative; +} + +#chat table, #chatinput table { + width: 100%; + table-layout: fixed; + border-spacing: 0m; + border-collapse: collapse; +} + +#chatinput table { + height: 100%; +} + +#chat td, #chatinput td { + font-family: Verdana, sans-serif; + font-size: 12.6px; + vertical-align: top; + padding-bottom: 3px; +} + +/* time */ +#chat td:first-child, #chatinput td:first-child { + width: 44px; + overflow: hidden; + padding-left: 2px; + color: #bbb; + white-space: nowrap; +} + +#chat time { + cursor: help; +} + +/* nick */ +#chat td:nth-child(2), #chatinput td:nth-child(2) { + width: 91px; + overflow: hidden; + padding-left: 2px; + white-space: nowrap; +} + +mark { + background: transparent; +} + +.invisep { + display: inline-block; + width: 1px; + height: 1px; + overflow:hidden; + color: transparent; +} + +/* divider */ +summary { + color: #bbb; + display: inline-block; + font-family: Verdana,sans-serif; + height: 16px; + overflow: hidden; + padding: 0 2px; + white-space: nowrap; + width: 100%; +} + +#chatinput { + position: absolute; + bottom: 0; + left: 0; + padding: 0 2px; + background: rgba(8, 48, 78, 0.9); + width: 708px; + border: 1px solid #20A8B1; + z-index: 3001; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +#chatinput td { + padding-bottom: 1px; + vertical-align: middle; +} + + +#chatinput input { + background: transparent; + font-size: 12.6px; + font-family: Verdana,sans-serif; + color: #EEEEEE; + width: 100%; + height: 100%; +} + + + +/* sidebar ************************************************************/ + +#sidebar > * { + border-bottom: 1px solid #20A8B1; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + + + +#sidebartoggle .toggle { + border-bottom: 10px solid transparent; + border-top: 10px solid transparent; +} + +#sidebartoggle .open { + border-right: 10px solid #FFCE00; +} + +#sidebartoggle .close { + border-left: 10px solid #FFCE00; +} + +/* player stats */ +#playerstat { + height: 30px; +} + +h2 { + color: #ffce00; + font-size: 21px; + padding: 0 4px; + margin: 0; + cursor:help; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 100%; +} + +h2 span { + display: inline-block; + overflow: hidden; + text-overflow: "~"; + vertical-align: top; + white-space: nowrap; + width: 205px; +} + +h2 div { + float: right; + height: 100%; + overflow: hidden; +} + +h2 sup, h2 sub { + display: block; + font-size: 11px; + margin-bottom: -1px; +} + + +/* gamestats */ +#gamestat { + height: 22px; +} + +#gamestat span { + display: block; + float: left; + font-weight: bold; + cursor:help; + height: 21px; + line-height: 22px; +} + +#gamestat .res { + background: #005684; + text-align: right; +} + +#gamestat .enl { + background: #017f01; +} + + +/* geosearch input, and others */ +input { + background-color: rgba(0, 0, 0, 0.3); + color: #ffce00; + height: 24px; + padding:3px 4px 1px 4px; + font-size: 14px; + border:0; + font-family:inherit; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +::-webkit-input-placeholder { + font-style: italic; +} + +:-moz-placeholder { + font-style: italic; +} + +::-moz-placeholder { + font-style: italic; +} + + +/* portal title and image */ +h3 { + font-size: 17px; + padding: 0 4px; + margin:0; + height: 25px; + width: 100%; + overflow:hidden; + text-overflow: "~"; + white-space: nowrap; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.imgpreview { + height: 200px; + background: no-repeat center center; + background-size: contain; + cursor: help; + overflow: hidden; +} + +.imgpreview img.hide { + display: none; +} + +#level { + font-size: 40px; + text-shadow: -1px -1px #000, 1px -1px #000, -1px 1px #000, 1px 1px #000, 0 0 5px #fff; + display: block; + margin-right: 15px; + text-align:right; +} + +/* portal mods */ +.mods { + margin: 5px auto 1px auto; + padding: 0 2px; + width: 296px; + height: 75px; + text-align: center; +} + +.mods span { + background-color: rgba(0, 0, 0, 0.3); + /* can’t use inline-block because Webkit’s implementation is buggy and + * introduces additional margins in random cases. No clear necessary, + * as that’s solved by setting height on .mods. */ + display: block; + float:left; + height: 63px; + margin: 0 2px; + overflow: hidden; + padding: 2px; + text-align: center; + width: 63px; + cursor:help; + border: 1px solid #666; +} + +.mods span:not([title]) { + cursor: auto; +} + +.res .mods span, .res .meter { + border: 1px solid #0076b6; +} +.enl .mods span, .enl .meter { + border: 1px solid #017f01; +} + +/* random details, resonator details */ +#randdetails, #resodetails { + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0 4px; + table-layout: fixed; + border-spacing: 0m; + border-collapse: collapse; +} + +#randdetails td, #resodetails td { + overflow: hidden; + text-overflow: "~"; + vertical-align: top; + white-space: nowrap; + width: 50%; + width: calc(50% - 62px); +} + +#randdetails th, #resodetails th { + font-weight: normal; + text-align: right; + width: 62px; + padding-right:4px; + padding-left:4px; +} + +#randdetails th + th, #resodetails th + th { + text-align: left; + padding-right: 4px; + padding-left: 4px; +} + +#randdetails td:first-child, #resodetails td:first-child { + text-align: right; + padding-left: 2px; +} + +#randdetails td:last-child, #resodetails td:last-child { + text-align: left; + padding-right: 2px; +} + + +#randdetails { + margin-top: 9px; + margin-bottom: 9px; +} + + +#randdetails tt { + font-family: inherit; + cursor: help; +} + +/* resonators */ +#resodetails { + margin-bottom: 9px; +} + +.meter { + background: #000; + cursor: help; + display: inline-block; + height: 18px; + padding: 1px; + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + position: relative; + left: 0; + top: 0; +} + +.meter span { + display: block; + height: 14px; +} + +.meter-level { + position: absolute; + top: -2px; + left: 50%; + margin-left: -6px; + text-shadow: 0.0em 0.0em 0.3em #808080; +} +/* links below resos */ + +.linkdetails { + margin-bottom: 8px; +} + +aside { + display: inline-block; + padding-right: 9px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + text-align: center; +} + +.linkdetails aside:last-child { + padding-right: 0; +} + +.linkdetails aside:nth-child(1) { + text-align: right; + width:88px; +} + +.linkdetails aside:nth-child(2) { + text-align: right; + width:67px; +} + +.linkdetails aside:nth-child(4) { + margin-left:13px; +} + +#toolbox { + padding: 4px 2px; + font-size:90%; +} + +#toolbox > a { + padding: 4px; +} + +/* a common portal display takes this much space (prevents moving + * content when first selecting a portal) */ + +#portaldetails { + min-height: 553px; +} + + +/* update status */ +#updatestatus { + background-color: rgba(8, 48, 78, 0.9); + border-bottom: 0; + border-top: 1px solid #20A8B1; + border-left: 1px solid #20A8B1; + bottom: 0; + color: #ffce00; + font-size:13px; + padding: 4px; + position: fixed; + right: 0; + z-index:3002; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + + +/* preview */ + +#largepreview { + left: 50%; + position: fixed; + top: 50%; + z-index: 2000; +} +#largepreview img { + box-shadow: 0 0 40px #000; +} +#largepreview img { + border: 2px solid #f8ff5e; +} + +/* tooltips, dialogs */ +.ui-tooltip, .ui-dialog { + 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; +} + +.ui-dialog { + border: 1px solid #0F0F0F; + padding: 0; + border-radius: 2px; +} + +.ui-widget-overlay { + height: 100%; + left: 0; + position: fixed; + top: 0; + width: 100%; + z-index:9998; + background: #444; + opacity: 0.6; +} + +.ui-dialog-titlebar { + display: none; +} + +.ui-dialog-content { + padding: 12px; + overflow-y: auto; + overflow-x: hidden; + max-height: 600px !important; + max-width: 700px !important; +} + +.ui-dialog-buttonpane { + background: #F2F2F2; + padding: 12px; + border-top: 1px solid #E6E6E6; +} + +.ui-dialog-buttonset { + text-align: right; +} + +.ui-dialog-buttonset button { + padding: 2px; + min-width: 80px; +} + +td { + padding: 0; + vertical-align: top; +} + +td + td { + padding-left: 4px; +} diff --git a/dist/total-conversion-build.user.js b/dist/total-conversion-build.user.js index babe8e00..7957b94d 100644 --- a/dist/total-conversion-build.user.js +++ b/dist/total-conversion-build.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @id ingress-intel-total-conversion@breunigs // @name intel map total conversion -// @version 0.61-2013-02-14-1313127 +// @version 0.7-2013-02-23-141531 // @namespace https://github.com/breunigs/ingress-intel-total-conversion // @updateURL https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/dist/total-conversion-build.user.js // @downloadURL https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/dist/total-conversion-build.user.js @@ -15,6 +15,8 @@ if(document.getElementsByTagName('html')[0].getAttribute('itemscope') != null) throw('Ingress Intel Website is down, not a userscript issue.'); +window.iitcBuildDate = '2013-02-23-141531'; + // disable vanilla JS window.onload = function() {}; @@ -49,15 +51,32 @@ 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"}; +var ir = window.internalResources || []; + +var mainstyle = 'http://breunigs.github.com/ingress-intel-total-conversion/dist/style.0.7.css'; +var smartphone = 'http://breunigs.github.com/ingress-intel-total-conversion/dist/smartphone.0.7.css'; +var leaflet = 'http://cdn.leafletjs.com/leaflet-0.5/leaflet.css'; +var coda = 'http://fonts.googleapis.com/css?family=Coda'; + // 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. document.getElementsByTagName('head')[0].innerHTML = '' - //~ + '' + //+ '' + 'Ingress Intel Map' - + '' - + '' - + ''; + + (ir.indexOf('mainstyle') === -1 + ? '' + : '') + + (ir.indexOf('leafletcss') === -1 + ? '' + : '') + // this navigator check is also used in code/smartphone.js + + (ir.indexOf('smartphonecss') === -1 && navigator.userAgent.match(/Android.*Mobile/) + ? '' + : '') + + (ir.indexOf('codafont') === -1 + ? '' + : ''); document.getElementsByTagName('body')[0].innerHTML = '' + '
Loading, please wait
' @@ -71,7 +90,11 @@ document.getElementsByTagName('body')[0].innerHTML = '' + '
' + '
' + '' - + '' + + '' + '' + '
' // enable scrolling for small screens + ' ' + '
' + '' - + '
'; + + '
' + + '
'; // putting everything in a wrapper function that in turn is placed in a // script tag on the website allows us to execute in the site’s context @@ -138,12 +162,21 @@ window.COLORS = ['#FFCE00', '#0088FF', '#03FE03']; // none, res, enl window.COLORS_LVL = ['#000', '#FECE5A', '#FFA630', '#FF7315', '#E40000', '#FD2992', '#EB26CD', '#C124E0', '#9627F4']; window.COLORS_MOD = {VERY_RARE: '#F78AF6', RARE: '#AD8AFF', COMMON: '#84FBBD'}; +window.OPTIONS_RESONATOR_SELECTED = { color: '#fff', weight: 2, radius: 4}; +window.OPTIONS_RESONATOR_NON_SELECTED = { color: '#aaa', weight: 1, radius: 3}; + +window.OPTIONS_RESONATOR_LINE_SELECTED = {opacity: 0.7, weight: 3}; +window.OPTIONS_RESONATOR_LINE_NON_SELECTED = {opacity: 0.25, weight: 2}; // circles around a selected portal that show from where you can hack // it and how far the portal reaches (i.e. how far links may be made // from this portal) window.ACCESS_INDICATOR_COLOR = 'orange'; -window.RANGE_INDICATOR_COLOR = 'red'; +window.RANGE_INDICATOR_COLOR = 'red' + +// by how much pixels should the portal range be expanded on mobile +// devices. This should make clicking them easier. +window.PORTAL_RADIUS_ENLARGE_MOBILE = 5; window.DEFAULT_PORTAL_IMG = 'http://commondatastorage.googleapis.com/ingress/img/default-portal-image.png'; @@ -151,27 +184,35 @@ window.NOMINATIM = 'http://nominatim.openstreetmap.org/search?format=json&limit= // INGRESS CONSTANTS ///////////////////////////////////////////////// // http://decodeingress.me/2012/11/18/ingress-portal-levels-and-link-range/ -var RESO_NRG = [0, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 6000]; -var MAX_XM_PER_LEVEL = [0, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000]; -var MIN_AP_FOR_LEVEL = [0, 10000, 30000, 70000, 150000, 300000, 600000, 1200000]; -var HACK_RANGE = 40; // in meters, max. distance from portal to be able to access it -var OCTANTS = ['E', 'NE', 'N', 'NW', 'W', 'SW', 'S', 'SE']; -var DESTROY_RESONATOR = 75; //AP for destroying portal -var DESTROY_LINK = 187; //AP for destroying link -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 +window.RESO_NRG = [0, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 6000]; +window.MAX_XM_PER_LEVEL = [0, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000]; +window.MIN_AP_FOR_LEVEL = [0, 10000, 30000, 70000, 150000, 300000, 600000, 1200000]; +window.HACK_RANGE = 40; // in meters, max. distance from portal to be able to access it +window.OCTANTS = ['E', 'NE', 'N', 'NW', 'W', 'SW', 'S', 'SE']; +window.DESTROY_RESONATOR = 75; //AP for destroying portal +window.DESTROY_LINK = 187; //AP for destroying link +window.DESTROY_FIELD = 750; //AP for destroying field +window.CAPTURE_PORTAL = 500; //AP for capturing a portal +window.DEPLOY_RESONATOR = 125; //AP for deploying a resonator +window.COMPLETION_BONUS = 250; //AP for deploying all resonators on portal // OTHER MORE-OR-LESS CONSTANTS ////////////////////////////////////// -var TEAM_NONE = 0, TEAM_RES = 1, TEAM_ENL = 2; -var TEAM_TO_CSS = ['none', 'res', 'enl']; -var TYPE_UNKNOWN = 0, TYPE_PORTAL = 1, TYPE_LINK = 2, TYPE_FIELD = 3, TYPE_PLAYER = 4, TYPE_CHAT = 5, TYPE_RESONATOR = 6; +window.TEAM_NONE = 0; +window.TEAM_RES = 1; +window.TEAM_ENL = 2; +window.TEAM_TO_CSS = ['none', 'res', 'enl']; +window.TYPE_UNKNOWN = 0; +window.TYPE_PORTAL = 1; +window.TYPE_LINK = 2; +window.TYPE_FIELD = 3; +window.TYPE_PLAYER = 4; +window.TYPE_CHAT = 5; +window.TYPE_RESONATOR = 6; -var SLOT_TO_LAT = [0, Math.sqrt(2)/2, 1, Math.sqrt(2)/2, 0, -Math.sqrt(2)/2, -1, -Math.sqrt(2)/2]; -var SLOT_TO_LNG = [1, Math.sqrt(2)/2, 0, -Math.sqrt(2)/2, -1, -Math.sqrt(2)/2, 0, Math.sqrt(2)/2]; -var EARTH_RADIUS=6378137; -var DEG2RAD = Math.PI / 180; +window.SLOT_TO_LAT = [0, Math.sqrt(2)/2, 1, Math.sqrt(2)/2, 0, -Math.sqrt(2)/2, -1, -Math.sqrt(2)/2]; +window.SLOT_TO_LNG = [1, Math.sqrt(2)/2, 0, -Math.sqrt(2)/2, -1, -Math.sqrt(2)/2, 0, Math.sqrt(2)/2]; +window.EARTH_RADIUS=6378137; +window.DEG2RAD = Math.PI / 180; // STORAGE /////////////////////////////////////////////////////////// // global variables used for storage. Most likely READ ONLY. Proper @@ -215,7 +256,7 @@ if(typeof window.plugin !== 'function') window.plugin = function() {}; // // 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 +// code before “// PLUGIN START” and after “// PLUGIN END” is // required to successfully boot the plugin. // // Here’s more specific information about each event: @@ -226,9 +267,34 @@ if(typeof window.plugin !== 'function') window.plugin = function() {}; // 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. +// portalDetailsUpdated: fired after the details in the sidebar have +// been (re-)rendered Provides data about the portal that +// has been selected. +// publicChatDataAvailable: this hook runs after data for any of the +// public chats has been received and processed, but not +// yet been displayed. The data hash contains both the un- +// processed raw ajax response as well as the processed +// chat data that is going to be used for display. +// portalDataLoaded: callback is passed the argument of +// {portals : [portal, portal, ...]} where "portal" is the +// data element and not the leaflet object. "portal" is an +// array [GUID, time, details]. Plugin can manipulate the +// array to change order or add additional values to the +// details of a portal. +// beforePortalReRender: the callback argument is +// {portal: ent[2], oldPortal : d, reRender : false}. +// The callback needs to update the value of reRender to +// true if the plugin has a reason to have the portal +// redrawn. It is called early on in the +// code/map_data.js#renderPortal as long as there was an +// old portal for the guid. + + + window._hooks = {} -window.VALID_HOOKS = ['portalAdded']; +window.VALID_HOOKS = ['portalAdded', 'portalDetailsUpdated', + 'publicChatDataAvailable', 'portalDataLoaded']; window.runHooks = function(event, data) { if(VALID_HOOKS.indexOf(event) === -1) throw('Unknown event type: ' + event); @@ -341,13 +407,13 @@ window.handleDataResponse = function(data, textStatus, jqXHR) { // format for portals: { controllingTeam, turret } if(ent[2].turret !== undefined) { - if(selectedPortal == ent[0]) portalUpdateAvailable = true; + if(selectedPortal === ent[0]) portalUpdateAvailable = true; if(urlPortal && ent[0] == urlPortal) portalInUrlAvailable = true; var latlng = [ent[2].locationE6.latE6/1E6, ent[2].locationE6.lngE6/1E6]; if(!window.getPaddedBounds().contains(latlng) - && selectedPortal != ent[0] - && urlPortal != ent[0] + && selectedPortal !== ent[0] + && urlPortal !== ent[0] ) return; @@ -378,10 +444,18 @@ window.handleDataResponse = function(data, textStatus, jqXHR) { } }); + // Preserve and restore "selectedPortal" between portal re-render + if(portalUpdateAvailable) var oldSelectedPortal = selectedPortal; + + runHooks('portalDataLoaded', {portals : ppp}); $.each(ppp, function(ind, portal) { renderPortal(portal); }); - if(portals[selectedPortal]) { + + var selectedPortalLayer = portals[oldSelectedPortal]; + if(portalUpdateAvailable && selectedPortalLayer) selectedPortal = oldSelectedPortal; + + if(selectedPortalLayer) { try { - portals[selectedPortal].bringToFront(); + selectedPortalLayer.bringToFront(); } catch(e) { /* portal is now visible, catch Leaflet error */ } } @@ -461,12 +535,12 @@ window.removeByGuid = function(guid) { // renders a portal on the map from the given entity window.renderPortal = function(ent) { - if(Object.keys(portals).length >= MAX_DRAWN_PORTALS && ent[0] != selectedPortal) + if(Object.keys(portals).length >= MAX_DRAWN_PORTALS && ent[0] !== selectedPortal) return removeByGuid(ent[0]); // hide low level portals on low zooms var portalLevel = getPortalLevel(ent[2]); - if(portalLevel < getMinPortalLevel() && ent[0] != selectedPortal) + if(portalLevel < getMinPortalLevel() && ent[0] !== selectedPortal) return removeByGuid(ent[0]); var team = getTeam(ent[2]); @@ -476,8 +550,16 @@ window.renderPortal = function(ent) { var old = findEntityInLeaflet(layerGroup, window.portals, ent[0]); if(old) { var oo = old.options; + + // Default checks to see if a portal needs to be re-rendered var u = oo.team !== team; u = u || oo.level !== portalLevel; + + // Allow plugins to add additional conditions as to when a portal gets re-rendered + var hookData = {portal: ent[2], oldPortal: oo.details, reRender: false}; + runHooks('beforePortalReRender', hookData); + u = u || hookData.reRender; + // nothing changed that requires re-rendering the portal. if(!u) { // let resos handle themselves if they need to be redrawn @@ -486,10 +568,13 @@ window.renderPortal = function(ent) { old.options.details = ent[2]; return; } - // there were changes, remove old portal - removeByGuid(ent[0]); } + // there were changes, remove old portal. Don’t put this in old, in + // case the portal changed level and findEntityInLeaflet doesn’t find + // it. + removeByGuid(ent[0]); + var latlng = [ent[2].locationE6.latE6/1E6, ent[2].locationE6.lngE6/1E6]; // pre-loads player names for high zoom levels @@ -499,8 +584,8 @@ window.renderPortal = function(ent) { var lvRadius = Math.max(portalLevel + 3, 5); var p = L.circleMarker(latlng, { - radius: lvRadius, - color: ent[0] == selectedPortal ? COLOR_SELECTED_PORTAL : COLORS[team], + radius: lvRadius + (L.Browser.mobile ? PORTAL_RADIUS_ENLARGE_MOBILE : 0), + color: ent[0] === selectedPortal ? COLOR_SELECTED_PORTAL : COLORS[team], opacity: 1, weight: lvWeight, fillColor: COLORS[team], @@ -534,7 +619,7 @@ window.renderPortal = function(ent) { 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) + if(window.selectedPortal !== this.options.guid) window.portalResetColor(this); }); @@ -547,8 +632,6 @@ window.renderPortal = function(ent) { window.renderResonators(ent, null); window.runHooks('portalAdded', {portal: p}); - - // portalLevel contains a float, need to round down p.addTo(layerGroup); } @@ -556,24 +639,25 @@ window.renderResonators = function(ent, portalLayer) { if(!isResonatorsShow()) return; var portalLevel = getPortalLevel(ent[2]); - if(portalLevel < getMinPortalLevel() && ent[0] != selectedPortal) return; + 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++) { - var rdata = ent[2].resonatorArray.resonators[i]; - + $.each(ent[2].resonatorArray.resonators, function(i, rdata) { // 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(oldRes && isSameResonator(oldRes.options.details, rdata)) return true; + if(oldRes) { + if(isSameResonator(oldRes.options.details, rdata)) return true; + removeByGuid(oldRes.options.guid); + } } // skip and remove old resonator if no new resonator if(rdata === null) { - if(oldRes) removeByGuid(oldRes.options.guid); - continue; + return true; } // offset in meters @@ -592,26 +676,29 @@ window.renderResonators = function(ent, portalLayer) { var resoGuid = portalResonatorGuid(ent[0], i); // the resonator - var reso = L.circleMarker(Rlatlng, { - radius: 3, - // #AAAAAA outline seems easier to see the fill opacity - color: '#AAAAAA', + var resoStyle = + ent[0] === selectedPortal ? OPTIONS_RESONATOR_SELECTED : OPTIONS_RESONATOR_NON_SELECTED; + var resoProperty = $.extend({ opacity: 1, - weight: 1, fillColor: COLORS_LVL[rdata.level], fillOpacity: rdata.energyTotal/RESO_NRG[rdata.level], clickable: false, - guid: resoGuid // need this here as well for add/remove events - }); + guid: resoGuid + }, resoStyle); + + var reso = L.circleMarker(Rlatlng, resoProperty); // line connecting reso to portal - var conn = L.polyline([portalLatLng, Rlatlng], { - weight: 2, + var connStyle = + ent[0] === selectedPortal ? OPTIONS_RESONATOR_LINE_SELECTED : OPTIONS_RESONATOR_LINE_NON_SELECTED; + var connProperty = $.extend({ color: '#FFA000', - opacity: 0.25, dashArray: '0,10,8,4,8,4,8,4,8,4,8,4,8,4,8,4,8,4,8,4', fill: false, - clickable: false}); + clickable: false + }, connStyle); + + var conn = L.polyline([portalLatLng, Rlatlng], connProperty); // put both in one group, so they can be handled by the same logic. @@ -628,11 +715,17 @@ window.renderResonators = function(ent, portalLayer) { // doesn’t 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; }); + reso.on('add', function() { + if(window.resonators[this.options.guid]) { + console.error('dup reso: ' + this.options.guid); + window.debug.printStackTrace(); + } + window.resonators[this.options.guid] = r; + }); r.addTo(portalsLayers[parseInt(portalLevel)]); reRendered = true; - } + }); // if there is any resonator re-rendered, bring portal to front if(reRendered && portalLayer) portalLayer.bringToFront(); } @@ -657,6 +750,33 @@ window.isSameResonator = function(oldRes, newRes) { window.portalResetColor = function(portal) { portal.setStyle({color: COLORS[getTeam(portal.options.details)]}); + resonatorsResetStyle(portal.options.guid); +} + +window.resonatorsResetStyle = function(portalGuid) { + window.resonatorsSetStyle(portalGuid, OPTIONS_RESONATOR_NON_SELECTED, OPTIONS_RESONATOR_LINE_NON_SELECTED); +} + +window.resonatorsSetSelectStyle = function(portalGuid) { + window.resonatorsSetStyle(portalGuid, OPTIONS_RESONATOR_SELECTED, OPTIONS_RESONATOR_LINE_SELECTED); +} + +window.resonatorsSetStyle = function(portalGuid, resoStyle, lineStyle) { + for(var i = 0; i < 8; i++) { + resonatorLayerGroup = resonators[portalResonatorGuid(portalGuid, i)]; + if(!resonatorLayerGroup) continue; + // bring resonators and their connection lines to front separately. + // this way the resonators are drawn on top of the lines. + resonatorLayerGroup.eachLayer(function(layer) { + if (!layer.options.guid) // Resonator line + layer.bringToFront().setStyle(lineStyle); + }); + resonatorLayerGroup.eachLayer(function(layer) { + if (layer.options.guid) // Resonator + layer.bringToFront().setStyle(resoStyle); + }); + } + portals[portalGuid].bringToFront(); } // renders a link on the map from the given entity @@ -755,6 +875,108 @@ window.findEntityInLeaflet = function(layerGroup, entityHash, guid) { +window.extendLeafletWithText = function() { + + + L.CircleMarkerWithText = L.CircleMarker.extend({ + options: { + fontFamily: 'Coda', + fontStroke: '#fff', + fontWeight: 'bold', + fontStrokeWidth: 2.5, + fontStrokeOpacity: 0.6, + fontFill: '#000', + fontSize: 14 + }, + + initialize: function (latlng, options) { + L.CircleMarker.prototype.initialize.call(this, latlng, options); + }, + + setText: function(text) { + this.options.text = text; + this._updateText(); + }, + + _updateStyle: function() { + L.CircleMarker.prototype._updateStyle.call(this); + this._updateText(); + }, + + _updatePath: function() { + if(this._oldPoint == this._point) return; + this._oldPoint = this._point; + + L.CircleMarker.prototype._updatePath.call(this); + this._updateTextPosition(); + }, + + _updateTextPosition: function() { + if(!this._textOuter) return; + this._textOuter.setAttribute('x', this._point.x); + this._textOuter.setAttribute('y', this._point.y); + this._textInner.setAttribute('x', this._point.x); + this._textInner.setAttribute('y', this._point.y); + }, + + _updateText: function() { + if(!L.Browser.svg) return; + + if(this._text && !this.options.text) { + this._container.removeChild(this._text); + } + + if(!this.options.text) return; + + if(!this._text) { + this._textOuter = this._createElement('text'); + this._textInner = this._createElement('text'); + this._text = this._createElement('g'); + this._text.setAttribute('text-anchor', 'middle'); + + if(this.options.clickable) { + this._text.setAttribute('class', 'leaflet-clickable'); + } + + this._text.appendChild(this._textOuter); + this._text.appendChild(this._textInner); + this._container.appendChild(this._text); + } + + // _textOuter contains the stroke information so the stroke is + // below the text and does not bleed into it. + this._textOuter.setAttribute('stroke', this.options.fontStroke); + this._textOuter.setAttribute('stroke-width', this.options.fontStrokeWidth); + this._textOuter.setAttribute('stroke-opacity', this.options.fontStrokeOpacity); + this._textOuter.setAttribute('dy', this.options.fontSize/2 - 1); + this._textInner.setAttribute('dy', this.options.fontSize/2 - 1); + + // _text contains properties that apply to both stroke and text. + this._text.setAttribute('fill', this.options.fontFill); + this._text.setAttribute('font-size', this.options.fontSize); + this._text.setAttribute('font-family', this.options.fontFamily); + this._text.setAttribute('font-weight', this.options.fontWeight); + + // group elements can’t have positions + this._updateTextPosition(); + + if(this._textOuter.firstChild) { + this._textOuter.firstChild.nodeValue = this.options.text; + this._textInner.firstChild.nodeValue = this.options.text; + } else { + this._textOuter.appendChild(document.createTextNode(this.options.text)); + this._textInner.appendChild(document.createTextNode(this.options.text)); + } + } + }); + + L.circleMarkerWithText = function (latlng, options) { + return new L.CircleMarkerWithText(latlng, options); + }; +} + + + // REQUEST HANDLING ////////////////////////////////////////////////// // note: only meant for portal/links/fields request, everything else // does not count towards “loading” @@ -905,7 +1127,7 @@ window.writeCookie = function(name, val) { // add thousand separators to given number. // http://stackoverflow.com/a/1990590/1684530 by Doug Neiner. window.digits = function(d) { - return (d+"").replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1 "); + return (d+"").replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1 "); } // posts AJAX request to Ingress API. @@ -922,7 +1144,10 @@ window.postAjax = function(action, data, success, error) { var remove = function(data, textStatus, jqXHR) { window.requests.remove(jqXHR); }; var errCnt = function(jqXHR) { window.failedRequestCount++; window.requests.remove(jqXHR); }; return $.ajax({ - url: 'rpc/dashboard.'+action, + // use full URL to avoid issues depending on how people set their + // slash. See: + // https://github.com/breunigs/ingress-intel-total-conversion/issues/56 + url: 'http://www.ingress.com/rpc/dashboard.'+action, type: 'POST', data: data, dataType: 'json', @@ -960,6 +1185,8 @@ window.unixTimeToHHmm = function(time) { window.rangeLinkClick = function() { if(window.portalRangeIndicator) window.map.fitBounds(window.portalRangeIndicator.getBounds()); + if(window.isSmartphone) + window.smartphone.mapButton.click(); } window.reportPortalIssue = function(info) { @@ -1071,14 +1298,14 @@ if (typeof String.prototype.startsWith !== 'function') { } window.prettyEnergy = function(nrg) { - return nrg> 1000 ? Math.round(nrg/1000) + ' k': 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(); + var qry = 'latE6='+lat+'&lngE6='+lng+'&z=' + (map.getZoom()-1); $(elm).attr('href', 'http://www.ingress.com/intel?' + qry); } @@ -1088,6 +1315,52 @@ window.uniqueArray = function(arr) { }); } +window.genFourColumnTable = function(blocks) { + var t = $.map(blocks, function(detail, index) { + if(!detail) return ''; + if(index % 2 === 0) + return ''+detail[1]+''+detail[0]+''; + else + return ' '+detail[0]+''+detail[1]+''; + }).join(''); + if(t.length % 2 === 1) t + ''; + return t; +} + + +// converts given text with newlines (\n) and tabs (\t) to a HTML +// table automatically. +window.convertTextToTableMagic = function(text) { + // check if it should be converted to a table + if(!text.match(/\t/)) return text.replace(/\n/g, '
'); + + var data = []; + var columnCount = 0; + + // parse data + var rows = text.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 table = ''; + $.each(data, function(i, row) { + table += ''; + $.each(data[i], function(k, cell) { + var attributes = ''; + if(k === 0 && data[i].length < columnCount) { + attributes = ' colspan="'+(columnCount - data[i].length + 1)+'"'; + } + table += ''+cell+''; + }); + table += ''; + }); + table += '
'; + return table; +} + @@ -1103,12 +1376,12 @@ window.setupLargeImagePreview = function() { ex.remove(); return; } - var img = $(this).html(); - var w = $(this).find('img')[0].naturalWidth/2; - var h = $(this).find('img')[0].naturalHeight/2; + var img = $(this).find('img')[0]; + var w = img.naturalWidth/2; + var h = img.naturalHeight/2; var c = $('#portaldetails').attr('class'); $('body').append( - '
' + img + '
' + '
' + img.outerHTML + '
' ); $('#largepreview').click(function() { $(this).remove() }); }); @@ -1117,19 +1390,17 @@ window.setupLargeImagePreview = function() { window.setupStyles = function() { $('head').append(''); } @@ -1188,7 +1459,7 @@ window.setupMap = function() { // listen for changes and store them in cookies map.on('moveend', window.storeMapPosition); map.on('zoomend', function() { - window.storeMapPosition; + window.storeMapPosition(); // remove all resonators if zoom out to < RESONATOR_DISPLAY_ZOOM_LEVEL if(isResonatorsShow()) return; @@ -1283,8 +1554,9 @@ window.setupSidebarToggle = function() { }); } -window.setupTooltips = function() { - $(document).tooltip({ +window.setupTooltips = function(element) { + element = element || $(document); + element.tooltip({ // disable show/hide animation show: { effect: "hide", duration: 0 } , hide: false, @@ -1293,49 +1565,51 @@ window.setupTooltips = function() { }, 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, '
'); - } - - 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 = ''; - $.each(data, function(i, row) { - tooltip += ''; - $.each(data[i], function(k, cell) { - var attributes = ''; - if(k === 0 && data[i].length < columnCount) { - attributes = ' colspan="'+(columnCount - data[i].length + 1)+'"'; - } - tooltip += ''+cell+''; - }); - tooltip += ''; - }); - tooltip += '
'; - return tooltip; + return window.convertTextToTableMagic(title); } }); + + if(!window.tooltipClearerHasBeenSetup) { + window.tooltipClearerHasBeenSetup = true; + $(document).on('click', '.ui-tooltip', function() { $(this).remove(); }); + } } +window.setupDialogs = function() { + $('#dialog').dialog({ + autoOpen: false, + modal: true, + buttons: [ + { text: 'OK', click: function() { $(this).dialog('close'); } } + ] + }); + + window.alert = function(text, isHTML) { + var h = isHTML ? text : window.convertTextToTableMagic(text); + $('#dialog').html(h).dialog('open'); + } +} + + // BOOTING /////////////////////////////////////////////////////////// function boot() { window.debug.console.overwriteNativeIfRequired(); - console.log('loading done, booting'); + console.log('loading done, booting. Built: ' + window.iitcBuildDate); + if(window.deviceID) console.log('Your device ID: ' + window.deviceID); + window.runOnSmartphonesBeforeBoot(); + + // overwrite default Leaflet Marker icon to be a neutral color + var base = 'http://breunigs.github.com/ingress-intel-total-conversion/dist/images/'; + L.Icon.Default.imagePath = base; + + window.iconEnl = L.Icon.Default.extend({options: { iconUrl: base + 'marker-green.png' } }); + window.iconRes = L.Icon.Default.extend({options: { iconUrl: base + 'marker-blue.png' } }); + window.setupStyles(); + window.setupDialogs(); window.setupMap(); window.setupGeosearch(); window.setupRedeem(); @@ -1358,6 +1632,16 @@ function boot() { if(window.bootPlugins) $.each(window.bootPlugins, function(ind, ref) { ref(); }); + // sidebar is now at final height. Adjust scrollwrapper so scrolling + // is possible for small screens and it doesn’t block the area below + // it. + $('#scrollwrapper').css('max-height', ($('#sidebar').get(0).scrollHeight+3) + 'px'); + + window.runOnSmartphonesAfterBoot(); + + // workaround for #129. Not sure why this is required. + setTimeout('window.map.invalidateSize(false);', 500); + window.iitcLoaded = true; } @@ -1370,14 +1654,24 @@ 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/dist/leaflet_google.js'; -var JQUERY = 'https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js'; +var LEAFLETGOOGLE = 'http://breunigs.github.com/ingress-intel-total-conversion/dist/leaflet_google.js'; +var JQUERY = 'https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/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 AUTOLINK = 'http://breunigs.github.com/ingress-intel-total-conversion/dist/autolink.js'; +var EMPTY = 'data:text/javascript;base64,'; + +// don’t download resources which have been injected already +var ir = window && window.internalResources ? window.internalResources : []; +if(ir.indexOf('jquery') !== -1) JQUERY = EMPTY; +if(ir.indexOf('jqueryui') !== -1) JQUERYUI = EMPTY; +if(ir.indexOf('leaflet') !== -1) LEAFLET = EMPTY; +if(ir.indexOf('autolink') !== -1) AUTOLINK = EMPTY; +if(ir.indexOf('leafletgoogle') !== -1) LEAFLETGOOGLE = EMPTY; + // after all scripts have loaded, boot the actual app -load(JQUERY, LEAFLET, AUTOLINK).then(LLGMAPS, JQUERYUI).onError(function (err) { +load(JQUERY, LEAFLET, AUTOLINK).then(LEAFLETGOOGLE, JQUERYUI).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); @@ -1584,6 +1878,8 @@ window.chat.handlePublic = function(data, textStatus, jqXHR) { chat.writeDataToHash(data, chat._publicData, true); var oldMsgsWereAdded = old !== chat.getOldestTimestamp(false); + runHooks('publicChatDataAvailable', {raw: data, processed: chat._publicData}); + switch(chat.getActive()) { case 'public': window.chat.renderPublic(oldMsgsWereAdded); break; case 'compact': window.chat.renderCompact(oldMsgsWereAdded); break; @@ -1704,13 +2000,14 @@ window.chat.renderData = function(data, element, likelyWereOldMsgs) { }); var scrollBefore = scrollBottom(elm); - elm.html(msgs); + elm.html('' + msgs + '
'); chat.keepScrollPosition(elm, scrollBefore, likelyWereOldMsgs); } window.chat.renderDivider = function(text) { - return '─ '+text+' ──────────────────────────────────────────────────────────────────────────'; + var d = ' ──────────────────────────────────────────────────────────────────────────'; + return '─ ' + text + d + ''; } @@ -1721,7 +2018,8 @@ 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+'

'; + var i = ['<', '>']; + return ''+t+''+i[0]+''+nick+''+i[1]+''+msg+''; } @@ -1781,7 +2079,7 @@ window.chat.chooser = function(event) { var t = $(event.target); var tt = t.text(); - var span = $('#chatinput span'); + var mark = $('#chatinput mark'); $('#chatcontrols .active').removeClass('active'); t.addClass('active'); @@ -1792,19 +2090,19 @@ window.chat.chooser = function(event) { switch(tt) { case 'faction': - span.css('color', ''); - span.text('tell faction:'); + mark.css('color', ''); + mark.text('tell faction:'); break; case 'public': - span.css('cssText', 'color: red !important'); - span.text('broadcast:'); + mark.css('cssText', 'color: red !important'); + mark.text('broadcast:'); break; case 'compact': case 'full': - span.css('cssText', 'color: #bbb !important'); - span.text('tell Jarvis:'); + mark.css('cssText', 'color: #bbb !important'); + mark.text('tell Jarvis:'); break; default: @@ -1888,7 +2186,7 @@ window.chat.setup = function() { window.requests.addRefreshFunction(chat.request); var cls = PLAYER.team === 'ALIENS' ? 'enl' : 'res'; - $('#chatinput span').addClass(cls) + $('#chatinput mark').addClass(cls) } @@ -1981,8 +2279,8 @@ window.getRangeText = function(d) { return ['range', '' + (range > 1000 - ? Math.round(range/1000) + ' km' - : Math.round(range) + ' m') + ? Math.round(range/1000) + ' km' + : Math.round(range) + ' m') + '']; } @@ -2046,22 +2344,22 @@ window.getEnergyText = function(d) { window.getAvgResoDistText = function(d) { var avgDist = Math.round(10*getAvgResoDist(d))/10; - return ['reso dist', avgDist + ' m']; + return ['reso dist', avgDist + ' m']; } window.getResonatorDetails = function(d) { - var resoDetails = ''; + var resoDetails = []; // octant=slot: 0=E, 1=NE, 2=N, 3=NW, 4=W, 5=SW, 6=S, SE=7 // resos in the display should be ordered like this: // N NE Since the view is displayed in columns, they // NW E need to be ordered like this: N, NW, W, SW, NE, // W SE E, SE, S, i.e. 2 3 4 5 1 0 7 6 // SW S - $.each([2, 3, 4, 5, 1, 0, 7, 6], function(ind, slot) { - var isLeft = slot >= 2 && slot <= 5; + + $.each([2, 1, 3, 0, 4, 7, 5, 6], function(ind, slot) { var reso = d.resonatorArray.resonators[slot]; if(!reso) { - resoDetails += renderResonatorDetails(slot, 0, 0, null, null, isLeft); + resoDetails.push(renderResonatorDetails(slot, 0, 0, null, null)); return true; } @@ -2073,16 +2371,16 @@ window.getResonatorDetails = function(d) { // naming will still be correct. slot = parseInt(reso.slot); - resoDetails += renderResonatorDetails(slot, l, v, dist, nick, isLeft); + resoDetails.push(renderResonatorDetails(slot, l, v, dist, nick)); }); - return resoDetails; + return genFourColumnTable(resoDetails); } // helper function that renders the HTML for a given resonator. Does // not work with raw details-hash. Needs digested infos instead: // slot: which slot this resonator occupies. Starts with 0 (east) and // rotates clockwise. So, last one is 7 (southeast). -window.renderResonatorDetails = function(slot, level, nrg, dist, nick, isLeft) { +window.renderResonatorDetails = function(slot, level, nrg, dist, nick) { if(level === 0) { var meter = ''; } else { @@ -2103,44 +2401,28 @@ window.renderResonatorDetails = function(slot, level, nrg, dist, nick, isLeft) { var fill = ''; - var meter = '' + fill + lbar + ''; + var meter = '' + fill + lbar + ''; } - var cls = isLeft ? 'left' : 'right'; - var text = ''+(nick||'')+''; - return (isLeft ? text+meter : meter+text) + '
'; + return [meter, nick || '']; } -// calculate AP gain from destroying portal -// so far it counts only resonators + links -window.getDestroyAP = function(d) { - var resoCount = 0; - - $.each(d.resonatorArray.resonators, function(ind, reso) { - if(!reso) return true; - resoCount += 1; - }); - - var linkCount = d.portalV2.linkedEdges ? d.portalV2.linkedEdges.length : 0; - var fieldCount = d.portalV2.linkedFields ? d.portalV2.linkedFields.length : 0; - - var resoAp = resoCount * DESTROY_RESONATOR; - var linkAp = linkCount * DESTROY_LINK; - var fieldAp = fieldCount * DESTROY_FIELD; - var sum = resoAp + linkAp + fieldAp + CAPTURE_PORTAL + 8*DEPLOY_RESONATOR + COMPLETION_BONUS; +// calculate AP gain from destroying portal and then capturing it by deploying resonators +window.getAttackApGainText = function(d) { + var breakdown = getAttackApGain(d); function tt(text) { var t = 'Destroy & Capture:\n'; - t += resoCount + '×\tResonators\t= ' + digits(resoAp) + '\n'; - t += linkCount + '×\tLinks\t= ' + digits(linkAp) + '\n'; - t += fieldCount + '×\tFields\t= ' + digits(fieldAp) + '\n'; + t += breakdown.resoCount + '×\tResonators\t= ' + digits(breakdown.resoAp) + '\n'; + t += breakdown.linkCount + '×\tLinks\t= ' + digits(breakdown.linkAp) + '\n'; + t += breakdown.fieldCount + '×\tFields\t= ' + digits(breakdown.fieldAp) + '\n'; t += '1×\tCapture\t= ' + CAPTURE_PORTAL + '\n'; - t += '8×\tDeploy\t= ' + DEPLOY_RESONATOR + '\n'; + t += '8×\tDeploy\t= ' + (8 * DEPLOY_RESONATOR) + '\n'; t += '1×\tBonus\t= ' + COMPLETION_BONUS + '\n'; - t += 'Sum: ' + digits(sum) + ' AP'; - return '' + digits(text) + ''; + t += 'Sum: ' + digits(breakdown.totalAp) + ' AP'; + return '' + digits(text) + ''; } - return [tt('AP Gain'), tt(sum)]; + return [tt('AP Gain'), tt(breakdown.totalAp)]; } @@ -2344,7 +2626,6 @@ window.getPosition = function() { } - // PORTAL DETAILS MAIN /////////////////////////////////////////////// // main code block that renders the portal details in the sidebar and // methods that highlight the portal in the map view. @@ -2358,7 +2639,7 @@ window.renderPortalDetails = function(guid) { var d = window.portals[guid].options.details; - var update = selectPortal(guid); + selectPortal(guid); // collect some random data that’s not worth to put in an own method var links = {incoming: 0, outgoing: 0}; @@ -2379,54 +2660,48 @@ window.renderPortalDetails = function(guid) { var linkedFields = ['fields', d.portalV2.linkedFields.length]; // collect and html-ify random data - var randDetails = [playerText, sinceText, getRangeText(d), getEnergyText(d), linksText, getAvgResoDistText(d), linkedFields, getDestroyAP(d)]; - randDetails = randDetails.map(function(detail) { - if(!detail) return ''; - detail = ''; - return detail; - }).join('\n'); + var randDetails = [ + playerText, sinceText, getRangeText(d), getEnergyText(d), + linksText, getAvgResoDistText(d), linkedFields, getAttackApGainText(d) + ]; + randDetails = '' + genFourColumnTable(randDetails) + '
'; - // replacing causes flicker, so if the selected portal does not - // change, only update the data points that are likely to change. - if(update) { - console.log('Updating portal details'); - $('#level').text(Math.floor(getPortalLevel(d))); - $('.mods').html(getModDetails(d)); - $('#randdetails').html(randDetails); - $('#resodetails').html(getResonatorDetails(d)); - $('#portaldetails').attr('class', TEAM_TO_CSS[getTeam(d)]); - } else { - console.log('exchanging portal details'); - setPortalIndicators(d); - var img = d.imageByUrl && d.imageByUrl.imageUrl ? d.imageByUrl.imageUrl : DEFAULT_PORTAL_IMG; + var resoDetails = '' + getResonatorDetails(d) + '
'; - var lat = d.locationE6.latE6; - var lng = d.locationE6.lngE6; - var perma = 'http://ingress.com/intel?latE6='+lat+'&lngE6='+lng+'&z=17&pguid='+guid; - var imgTitle = 'title="'+getPortalDescriptionFromDetails(d)+'\n\nClick to show full image."'; + setPortalIndicators(d); + var img = d.imageByUrl && d.imageByUrl.imageUrl ? d.imageByUrl.imageUrl : DEFAULT_PORTAL_IMG; - $('#portaldetails') - .attr('class', TEAM_TO_CSS[getTeam(d)]) - .html('' - + '

'+d.portalV2.descriptiveText.TITLE+'

' - // help cursor via “.imgpreview img” - + '
' - + '' - + '
' - + ''+Math.floor(getPortalLevel(d))+'' - + '
'+getModDetails(d)+'
' - + '
'+randDetails+'
' - + '
'+getResonatorDetails(d)+'
' - + '
' - + '' - + '' - + '
' - ); - } + var lat = d.locationE6.latE6; + var lng = d.locationE6.lngE6; + var perma = 'http://ingress.com/intel?latE6='+lat+'&lngE6='+lng+'&z=17&pguid='+guid; + var imgTitle = 'title="'+getPortalDescriptionFromDetails(d)+'\n\nClick to show full image."'; + var gmaps = 'https://maps.google.com/?q='+lat/1E6+','+lng/1E6; + var postcard = 'Send in a postcard. Will put it online after receiving. Address:\\n\\nStefan Breunig\\nINF 305 – R045\\n69120 Heidelberg\\nGermany'; + + $('#portaldetails') + .attr('class', TEAM_TO_CSS[getTeam(d)]) + .html('' + + '

'+d.portalV2.descriptiveText.TITLE+'

' + // help cursor via “.imgpreview img” + + '
' + + '' + + ''+Math.floor(getPortalLevel(d))+'' + + '
' + + '
'+getModDetails(d)+'
' + + randDetails + + resoDetails + + '
'+ '' + + '' + + '' + + '' + + '
' + ); // try to resolve names that were required for above functions, but // weren’t available yet. resolvePlayerNames(); + + runHooks('portalDetailsUpdated', {portalDetails: d}); } // draws link-range and hack-range circles around the portal with the @@ -2459,8 +2734,10 @@ window.selectPortal = function(guid) { selectedPortal = guid; - if(portals[guid]) + if(portals[guid]) { + resonatorsSetSelectStyle(guid); portals[guid].bringToFront().setStyle({color: COLOR_SELECTED_PORTAL}); + } return update; } @@ -2468,8 +2745,7 @@ window.selectPortal = function(guid) { window.unselectOldPortal = function() { var oldPortal = portals[selectedPortal]; - if(oldPortal) - oldPortal.setStyle({color: oldPortal.options.fillColor}); + if(oldPortal) portalResetColor(oldPortal); selectedPortal = null; $('#portaldetails').html(''); } @@ -2526,7 +2802,7 @@ window.setupRedeem = function() { if((e.keyCode ? e.keyCode : e.which) != 13) return; var data = {passcode: $(this).val()}; window.postAjax('redeemReward', data, window.handleRedeemResponse, - function() { alert('HTTP request failed. Try again?'); }); + function() { alert('The HTTP request failed. Either your code is invalid or their servers are down. No way to tell.'); }); }); } @@ -2577,6 +2853,10 @@ window.resolvePlayerNames = function() { window.setPlayerName = function(guid, nick) { + if($.trim(('' + nick)).slice(0, 5) === '{"L":' && !window.alertFor37WasShown) { + window.alertFor37WasShown = true; + alert('You have run into bug #37. Please help me solve it!\nCopy and paste this text and post it here:\nhttps://github.com/breunigs/ingress-intel-total-conversion/issues/37\nIf copy & pasting doesn’t work, make a screenshot instead.\n\n\n' + window.debug.printStackTrace() + '\n\n\n' + JSON.stringify(nick)); + } localStorage[guid] = nick; } @@ -2596,6 +2876,94 @@ window.loadPlayerNamesForPortal = function(portal_details) { } +window.isSmartphone = function() { + // this check is also used in main.js. Note it should not detect + // tablets because their display is large enough to use the desktop + // version. + return navigator.userAgent.match(/Android.*Mobile/); +} + +window.smartphone = function() {}; + +window.runOnSmartphonesBeforeBoot = function() { + if(!isSmartphone()) return; + console.warn('running smartphone pre boot stuff'); + + // disable zoom buttons to see if they are really needed + window.localStorage['iitc.zoom.buttons'] = 'false'; + + // don’t need many of those + window.setupStyles = function() { + $('head').append(''); + } + + // this also matches the expand button, but it is hidden via CSS + $('#chatcontrols a').click(function() { + $('#scrollwrapper, #updatestatus').hide(); + // not displaying the map causes bugs in Leaflet + $('#map').css('visibility', 'hidden'); + $('#chat, #chatinput').show(); + }); + + window.smartphone.mapButton = $('map').click(function() { + $('#chat, #chatinput, #scrollwrapper').hide(); + $('#map').css('visibility', 'visible'); + $('#updatestatus').show(); + $('.active').removeClass('active'); + $(this).addClass('active'); + }); + + window.smartphone.sideButton = $('info').click(function() { + $('#chat, #chatinput, #updatestatus').hide(); + $('#map').css('visibility', 'hidden'); + $('#scrollwrapper').show(); + $('.active').removeClass('active'); + $(this).addClass('active'); + }); + + $('#chatcontrols').append(smartphone.mapButton).append(smartphone.sideButton); + + // add event to portals that allows long press to switch to sidebar + window.addHook('portalAdded', function(data) { + data.portal.on('dblclick', function() { + window.lastClickedPortal = this.options.guid; + }); + }); + + window.addHook('portalDetailsUpdated', function(data) { + var x = $('.imgpreview img').removeClass('hide'); + + if(!x.length) { + $('.fullimg').remove(); + return; + } + + if($('.fullimg').length) { + $('.fullimg').replaceWith(x.addClass('fullimg')); + } else { + x.addClass('fullimg').appendTo('#sidebar'); + } + }); +} + +window.runOnSmartphonesAfterBoot = function() { + if(!isSmartphone()) return; + console.warn('running smartphone post boot stuff'); + + chat.toggle(); + smartphone.mapButton.click(); + + // disable img full view + $('#portaldetails').off('click', '**'); + + $('.leaflet-right').addClass('leaflet-left').removeClass('leaflet-right'); +} + + // DEBUGGING TOOLS /////////////////////////////////////////////////// // meant to be used from browser debugger tools and the like. @@ -2611,6 +2979,7 @@ window.debug.renderDetails = function() { window.debug.printStackTrace = function() { var e = new Error('dummy'); console.log(e.stack); + return e.stack; } window.debug.clearPortals = function() { @@ -2649,13 +3018,13 @@ window.debug.console.create = function() { if($('#debugconsole').length) return; $('#chatcontrols').append('debug'); $('#chatcontrols a:last').click(function() { - $('#chatinput span').css('cssText', 'color: #bbb !important').text('debug:'); + $('#chatinput mark').css('cssText', 'color: #bbb !important').text('debug:'); $('#chat > div').hide(); $('#debugconsole').show(); $('#chatcontrols .active').removeClass('active'); $(this).addClass('active'); }); - $('#chat').append(''); + $('#chat').append(''); } window.debug.console.renderLine = function(text, errorType) { @@ -2672,8 +3041,8 @@ window.debug.console.renderLine = function(text, errorType) { var tb = d.toLocaleString(); var t = ''; var s = 'style="color:'+color+'"'; - var l = '

'+t+''+errorType+''+text+'

'; - $('#debugconsole').prepend(l); + var l = ''+t+''+errorType+''+text+''; + $('#debugconsole table').prepend(l); } window.debug.console.log = function(text) { @@ -2796,6 +3165,38 @@ window.getAvgResoDist = function(d) { return sum/resos; } +window.getAttackApGain = function(d) { + var resoCount = 0; + + $.each(d.resonatorArray.resonators, function(ind, reso) { + if (!reso) + return true; + resoCount += 1; + }); + + var linkCount = d.portalV2.linkedEdges ? d.portalV2.linkedEdges.length : 0; + var fieldCount = d.portalV2.linkedFields ? d.portalV2.linkedFields.length : 0; + + var resoAp = resoCount * DESTROY_RESONATOR; + var linkAp = linkCount * DESTROY_LINK; + var fieldAp = fieldCount * DESTROY_FIELD; + var destroyAp = resoAp + linkAp + fieldAp; + var captureAp = CAPTURE_PORTAL + 8 * DEPLOY_RESONATOR + COMPLETION_BONUS; + var totalAp = destroyAp + captureAp; + + return { + totalAp: totalAp, + destroyAp: destroyAp, + captureAp: captureAp, + resoCount: resoCount, + resoAp: resoAp, + linkCount: linkCount, + linkAp: linkAp, + fieldCount: fieldCount, + fieldAp: fieldAp + }; +} + diff --git a/plugins/README.md b/plugins/README.md index 878eb796..25d781fd 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -9,9 +9,11 @@ Plugins are installed the same way the total conversion script is. Please see th Available Plugins ----------------- +- [**Compute AP Stats**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/compute-ap-stats.user.js) Shows the potential AP an agent could obtain by destroying and rebuilding all the portals in the current zoom area. - [**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. [View screenshot](http://breunigs.github.com/ingress-intel-total-conversion/screenshots/plugin_draw_tools.png) - [**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 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. +- [**Player Tracker**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/player-tracker.user.js) Draws trails for user actions in the last hour. At the last known location there’s a tooltip that shows the data in a table. [View screenshot](http://breunigs.github.com/ingress-intel-total-conversion/screenshots/plugin_player_tracker.png). - [**Render Limit Increase**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/render-limit-increase.user.js) increases render limits. Good for high density areas (e.g. London, UK) and faster PCs. - [**Resonator Display Zoom Level Decrease**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/resonator-display-zoom-level-decrease.user.js) Resonator start displaying earlier. - [**Show Portal Address**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/show-address.user.js) Shows portal address in the side panel. @@ -21,8 +23,8 @@ Available Plugins [Read HACKING.md file](https://github.com/breunigs/ingress-intel-total-conversion/blob/gh-pages/HACKING.md#hacking) to learn how to build the development version yourself. If **and only if** [you have read how to report bugs](https://github.com/breunigs/ingress-intel-total-conversion/blob/gh-pages/HACKING.md#how-do-i-report-bugs), you may beta test the [nightly](https://www.dropbox.com/sh/lt9p0s40kt3cs6m/3xzpyiVBnF) version. -- [**Compute AP Stats**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/compute-ap-stats.user.js) Shows the potential AP an agent could obtain by destroying and rebuilding all the portals in the current zoom area. **REQUIRES 2013-02-22+** -- [**Player Tracker**](https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/player-tracker.user.js) Draws trails for user actions in the last hour. At the last known location there’s a tooltip that shows the data in a table. [View screenshot](http://breunigs.github.com/ingress-intel-total-conversion/screenshots/plugin_player_tracker.png). **REQUIRES 2013-02-19+** + + Hacking -------