diff --git a/.gitignore b/.gitignore index 6a70ed57..7f654c5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ iitc-debug.user.js +mobile/IngressIntelTC/bin 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 f32219af..eacceeb6 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 @@ -68,6 +68,7 @@ Please do! **So far, these people have contributed:** [Bananeweizen](https://github.com/Bananeweizen), +[blakjakau](https://github.com/blakjakau), [cmrn](https://github.com/cmrn), [epf](https://github.com/epf), [integ3r](https://github.com/integ3r), @@ -80,6 +81,7 @@ Please do! [saithis](https://github.com/saithis), [Scrool](https://github.com/Scrool), [sorgo](https://github.com/sorgo), +[tpenner](https://github.com/tpenner), [vita10gy](https://github.com/vita10gy), [Xelio](https://github.com/Xelio), [ZauberNerd](https://github.com/ZauberNerd) diff --git a/USERGUIDE.md b/USERGUIDE.md index 42cd502c..c345ab4b 100644 --- a/USERGUIDE.md +++ b/USERGUIDE.md @@ -32,7 +32,7 @@ The chat is split up into several categories. It usually only shows messages for **The chat categories are:** - full: shows all automated messages *(23:57 \ destroyed an L3 Resonator on Two Spikes)* -- compact: shows exactly one automated message per user +- compact: shows only the latest automated message per user - public: shows user generated public messages (both Enlightenment and Resistance can read it) - faction: shows messages for own faction (e.g. only Resistance can read Resistance messages) @@ -101,7 +101,7 @@ Starting from the top, the sidebar shows this information: - Details about you, the logged in user. This is only updated if you reload the page. This is a limitation of Ingress, not IITC. - it shows your current level followed by your nick - to the right, it shows to percentages. The upper one, e.g. “XM: 37%” tells you how much your XM bar is filled. The lower one, e.g. “level: 37%“ tells you that you have gathered 37% of the AP required for the next level. It shows “max level” if you have reached max level. - - the tooltip mainly shows you absolute numbers instead of percentages. It also shows how much invites you have. + - the tooltip mainly shows you absolute numbers instead of percentages. It also shows how many invites you have. - The next bar is a visual representation of global MindUnits (MU) per faction. It is updated every now and then. The tooltip shows the absolute MU count per faction. - Search Location: You can search for continents, countries, cities or street addresses. If there is at least one result your are taken to the most likely immediately. There is no feedback if the entered location was not found. Rule of thumb: if it takes longer than three seconds, try again. [Read about supported formats in the user guide for this service](https://wiki.openstreetmap.org/wiki/Nominatim). @@ -123,7 +123,7 @@ Starting from the top, the sidebar shows this information: - links: shows incoming and outgoing links. The tooltip explains the icons. - reso dist: shows the average distance the resonators have to the portal. - fields: how many fields are connected to this portal -- AP Gain: estimate of how much AP you gain if you take down this portal and deploy resonators of your own faction. Tooltip breaks this number down into parts. +- AP Gain: estimate of how many AP you gain if you take down this portal and deploy resonators of your own faction. Tooltip breaks this number down into parts. ##### Resonators diff --git a/code/boot.js b/code/boot.js index 83be4f9c..70542bab 100644 --- a/code/boot.js +++ b/code/boot.js @@ -233,7 +233,8 @@ window.setupDialogs = function() { 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 diff --git a/code/hooks.js b/code/hooks.js index 43f4cc35..a2b81b68 100644 --- a/code/hooks.js +++ b/code/hooks.js @@ -11,7 +11,7 @@ // // 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: @@ -36,6 +36,16 @@ // 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', 'portalDetailsUpdated', diff --git a/code/map_data.js b/code/map_data.js index 6729c0f0..a28af7b4 100644 --- a/code/map_data.js +++ b/code/map_data.js @@ -127,7 +127,7 @@ 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); }); @@ -231,8 +231,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 diff --git a/smartphone.css b/dist/smartphone.0.7.css similarity index 100% rename from smartphone.css rename to dist/smartphone.0.7.css 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/main.js b/main.js index 1b5779e4..6f0b6f16 100644 --- a/main.js +++ b/main.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 = '@@BUILDDATE@@'; + // disable vanilla JS window.onload = function() {}; @@ -52,7 +54,7 @@ for(var i = 0; i < d.length; i++) { var ir = window.internalResources || []; var mainstyle = 'http://breunigs.github.com/ingress-intel-total-conversion/style.css?@@BUILDDATE@@'; -var smartphone = 'http://breunigs.github.com/ingress-intel-total-conversion/smartphone.css?@@BUILDDATE@@'; +var smartphone = 'http://breunigs.github.com/ingress-intel-total-conversion/mobile/smartphone.css?@@BUILDDATE@@'; var leaflet = 'http://cdn.leafletjs.com/leaflet-0.5/leaflet.css'; var coda = 'http://fonts.googleapis.com/css?family=Coda'; diff --git a/mobile/IngressIntelTC/.classpath b/mobile/IngressIntelTC/.classpath new file mode 100644 index 00000000..a4f1e405 --- /dev/null +++ b/mobile/IngressIntelTC/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/mobile/IngressIntelTC/.project b/mobile/IngressIntelTC/.project new file mode 100644 index 00000000..25a5ec79 --- /dev/null +++ b/mobile/IngressIntelTC/.project @@ -0,0 +1,33 @@ + + + AppTemplate + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/mobile/IngressIntelTC/AndroidManifest.xml b/mobile/IngressIntelTC/AndroidManifest.xml new file mode 100644 index 00000000..dbf96330 --- /dev/null +++ b/mobile/IngressIntelTC/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/IngressIntelTC/assets/js/ingressSplash.html b/mobile/IngressIntelTC/assets/js/ingressSplash.html new file mode 100644 index 00000000..620be905 --- /dev/null +++ b/mobile/IngressIntelTC/assets/js/ingressSplash.html @@ -0,0 +1,90 @@ + + + + Ingress Intel Total Converion - Mobile & Tablet + + + + +

Ingress Intel Total Conversion

+



loading...

+
+ Disclaimer: Ingress Intel Total Conversion is in no way affiliated with, connected to, or sanctioned by Ingress, Google or Niantic.
+ Full source for Ingress Intel Total Conversion can be found @ https://github.com/breunigs/ingress-intel-total-conversion
+ Source for this wrapper app available soon. +
+ + + diff --git a/mobile/IngressIntelTC/gen/com/jakbox/ingressTC/BuildConfig.java b/mobile/IngressIntelTC/gen/com/jakbox/ingressTC/BuildConfig.java new file mode 100644 index 00000000..c53b896c --- /dev/null +++ b/mobile/IngressIntelTC/gen/com/jakbox/ingressTC/BuildConfig.java @@ -0,0 +1,6 @@ +/** Automatically generated file. DO NOT MODIFY */ +package com.jakbox.ingressTC; + +public final class BuildConfig { + public final static boolean DEBUG = true; +} \ No newline at end of file diff --git a/mobile/IngressIntelTC/gen/com/jakbox/ingressTC/R.java b/mobile/IngressIntelTC/gen/com/jakbox/ingressTC/R.java new file mode 100644 index 00000000..162c7876 --- /dev/null +++ b/mobile/IngressIntelTC/gen/com/jakbox/ingressTC/R.java @@ -0,0 +1,27 @@ +/* AUTO-GENERATED FILE. DO NOT MODIFY. + * + * This class was automatically generated by the + * aapt tool from the resource data it found. It + * should not be modified by hand. + */ + +package com.jakbox.ingressTC; + +public final class R { + public static final class attr { + } + public static final class drawable { + public static final int ic_launcher=0x7f020000; + } + public static final class layout { + public static final int main=0x7f030000; + } + public static final class raw { + public static final int loader=0x7f040000; + } + public static final class string { + public static final int app_name=0x7f050000; + public static final int first_page=0x7f050001; + public static final int jakboxBootLoader=0x7f050002; + } +} diff --git a/mobile/IngressIntelTC/proguard.cfg b/mobile/IngressIntelTC/proguard.cfg new file mode 100644 index 00000000..b1cdf17b --- /dev/null +++ b/mobile/IngressIntelTC/proguard.cfg @@ -0,0 +1,40 @@ +-optimizationpasses 5 +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-dontpreverify +-verbose +-optimizations !code/simplification/arithmetic,!field/*,!class/merging/* + +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Application +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference +-keep public class com.android.vending.licensing.ILicensingService + +-keepclasseswithmembernames class * { + native ; +} + +-keepclasseswithmembers class * { + public (android.content.Context, android.util.AttributeSet); +} + +-keepclasseswithmembers class * { + public (android.content.Context, android.util.AttributeSet, int); +} + +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} + +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} diff --git a/mobile/IngressIntelTC/project.properties b/mobile/IngressIntelTC/project.properties new file mode 100644 index 00000000..f049142c --- /dev/null +++ b/mobile/IngressIntelTC/project.properties @@ -0,0 +1,11 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "ant.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-10 diff --git a/mobile/IngressIntelTC/res/drawable-hdpi/ic_launcher.png b/mobile/IngressIntelTC/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 00000000..55c83277 Binary files /dev/null and b/mobile/IngressIntelTC/res/drawable-hdpi/ic_launcher.png differ diff --git a/mobile/IngressIntelTC/res/drawable-ldpi/ic_launcher.png b/mobile/IngressIntelTC/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 00000000..b06e1f85 Binary files /dev/null and b/mobile/IngressIntelTC/res/drawable-ldpi/ic_launcher.png differ diff --git a/mobile/IngressIntelTC/res/drawable-mdpi/ic_launcher.png b/mobile/IngressIntelTC/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 00000000..f9b7522d Binary files /dev/null and b/mobile/IngressIntelTC/res/drawable-mdpi/ic_launcher.png differ diff --git a/mobile/IngressIntelTC/res/layout/main.xml b/mobile/IngressIntelTC/res/layout/main.xml new file mode 100644 index 00000000..e33294fb --- /dev/null +++ b/mobile/IngressIntelTC/res/layout/main.xml @@ -0,0 +1,10 @@ + + + + diff --git a/mobile/IngressIntelTC/res/raw/loader.js b/mobile/IngressIntelTC/res/raw/loader.js new file mode 100644 index 00000000..bcb336ca --- /dev/null +++ b/mobile/IngressIntelTC/res/raw/loader.js @@ -0,0 +1,13 @@ +function rebuildUI() { + if(window.UIDone == true) { return; } + if(!document.getElementById("map_canvas")) { + setTimeout(rebuildUI, 50); + return; + } else { + window.UIDone = true; + } + window.console.log(window.deviceID); + window.loadJS("http://mathphys.fsk.uni-heidelberg.de:8000/test.js") + //window.Android.TCReady(); +} +rebuildUI(); \ No newline at end of file diff --git a/mobile/IngressIntelTC/res/values/strings.xml b/mobile/IngressIntelTC/res/values/strings.xml new file mode 100644 index 00000000..cd948842 --- /dev/null +++ b/mobile/IngressIntelTC/res/values/strings.xml @@ -0,0 +1,17 @@ + + + Ingress Intel Total Conversion + Connecting to Ingress Intel Map ... + + bootLoader = function() { + if(document.getElementById(\"map_canvas\")) { + window.stop(); + document.body.innerHTML = ""; + } else { + setTimeout(bootLoader, 50); + } + }; + setTimeout(bootLoader, 5); + + + diff --git a/mobile/IngressIntelTC/src/com/jakbox/ingressTC/MainActivity.java b/mobile/IngressIntelTC/src/com/jakbox/ingressTC/MainActivity.java new file mode 100644 index 00000000..e98f3404 --- /dev/null +++ b/mobile/IngressIntelTC/src/com/jakbox/ingressTC/MainActivity.java @@ -0,0 +1,149 @@ +/******************************************************************************** + Ingress Intel Total Converion - Mobile & Tablet + Android WebView wrapper/loader for iitc (ingress intel total conversion) + iitc source @ https://github.com/breunigs/ingress-intel-total-conversion + + Original Author: Jason Grima - jason@jakbox.net +*********************************************************************************/ + + +package com.jakbox.ingressTC; + +import android.app.*; +import android.os.*; +import android.content.Context; +import android.view.*; +import android.widget.*; +import android.webkit.*; +import android.provider.Settings.Secure; +import android.util.Log; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.BufferedReader; +import java.lang.StringBuffer; + +public class MainActivity extends Activity +{ + WebView web; // webview to hold the ingress site (and login etc) + WebView splash; // splash screen, just a bit of pretty + MyChrome chrome; // for logging, progress, etc + MyClient client; // for controlling the webview's + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) + { + requestWindowFeature(Window.FEATURE_NO_TITLE); // hide the app title bar, Xeen requested + super.onCreate(savedInstanceState); + + // little animated title/splash page for IngressIntelTC + // separate webview so we can be loading stuff in the background + JSInterface js = new JSInterface(this, this); //js.setActivity(this); + this.splash = new WebView(this); + splash.getSettings().setJavaScriptEnabled(true); // because we use JS to trigger the CSS animation + splash.getSettings().setAllowFileAccess(true); // because the splash page is store in the apk + splash.getSettings().setAllowFileAccessFromFileURLs(true); // in case we add more scripts/images to the splash page + splash.loadUrl("file:///android_asset/js/ingressSplash.html"); // there is is! + splash.addJavascriptInterface(js, "android"); + setContentView(splash); + // righto, load the the website (ingress.com/intel) with a bunch of permissions that may/or not be needed + this.web = new WebView(this); + this.client = new MyClient(); + web.setWebViewClient(this.client); + web.getSettings().setJavaScriptEnabled(true); + web.getSettings().setGeolocationEnabled(true); + web.getSettings().setAllowFileAccess(true); + web.getSettings().setAllowFileAccessFromFileURLs(true); + web.getSettings().setDatabaseEnabled(true); + web.getSettings().setDomStorageEnabled(true); + web.getSettings().setGeolocationEnabled(true); + + this.chrome = new MyChrome(); // the chrome let's us get console and progress feedback from the page + web.setWebChromeClient(this.chrome); + web.addJavascriptInterface(js, "android"); // ready to go + + } + + public void onRestoreInstanceState(Bundle state) { + this.splash.restoreState(state); + this.web.restoreState(state); + } + public void onSaveInstanceState(Bundle state) { + this.splash.saveState(state); + this.web.saveState(state); + } + + public void showWeb() { setContentView(web); splash.destroy(); Log.d("com.jakbox.ingressTC", "Flipping to web/ingress view"); } + public void showSplash() { setContentView(splash); web.destroy(); Log.d("com.jakbox.ingressTC", "Flipping to splash/loading"); } + + final class JSInterface { + Context context; MainActivity act; + public JSInterface (Context c, MainActivity a) { this.context = c; this.act = a; } + public void setActivity(MainActivity a) { this.act = a; } + // the @JavascriptInterface is needed for 4.2 devices to access this + @JavascriptInterface + public void pageReady(int ready) { + if(ready != 0) { + // the loader reports we're good to go, switch to ingress webview + this.act.runOnUiThread(new Runnable() { + public void run() { showWeb(); } + }); + } else { + // the loader reports we're not ready, switch to splash page + this.act.runOnUiThread(new Runnable() { + public void run() { showSplash(); } + }); + } + } + @JavascriptInterface + public void loadBehind(final String url) { + this.act.runOnUiThread(new Runnable() { + public void run() { + Log.d("com.jakbox.ingressTC", "Loading (Ingress?) website in back webview"); + web.loadUrl(url); /* push the url to the webview */ } + }); + } + } + + private class MyClient extends WebViewClient { + @Override + public boolean shouldOverrideUrlLoading(WebView web, String Url) { + return false; + } + + @Override + public void onPageFinished(WebView web, String Url) { + // here we conditionally load some external javascript + Log.d("com.jakbox.ingressTC", "Page loading, injecting IITC JS"); + if(web.getUrl().contains("ingress.com/intel")) { + // if NOT ingress, we may have been redirected to the google auth/login page + web.loadUrl("javascript: window.stop(); "); + // first stop the page from loading too much + web.loadUrl("javascript: window.deviceID='"+Secure.getString(getContentResolver(), Secure.ANDROID_ID)+"'; "); + // create a JS loader on the page + web.loadUrl("javascript: if(!window.loadJS) { window.loadJS = function(scr) { var s = document.createElement('script'); s.src = scr+(scr.indexOf('file:///')==-1?'?ts="+System.currentTimeMillis()+"':''); s.type = 'text/javascript'; s.async = true; var st = document.getElementsByTagName('script')[0]; st.parentNode.insertBefore(s, st); } };"); + // actually hijack the page... if it's the right page... let the loader deal with that + // should pull this loader into a local resource, maybe... + // running it from the web for now ... + web.loadUrl("javascript: if(!window.hijacked) { window.loadJS('http://mathphys.fsk.uni-heidelberg.de:8000/bootstrap.js'); window.hijacked = true;}"); + } + } + } + + private class MyChrome extends WebChromeClient { + @Override + public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { + // Always grant permission since the app itself requires location + // permission and the user has therefore already granted it + callback.invoke(origin, true, false); + } + + public boolean onConsoleMessage(ConsoleMessage cm) { + // log window.console stuff into the syslog for ADB output + Log.d("com.jakbox.ingressTC", cm.message() + " -- " + + (cm.sourceId()==null?"main document":cm.sourceId()) + + ":Line " + cm.lineNumber() ); + return true; + } + } +} diff --git a/mobile/README.md b/mobile/README.md new file mode 100644 index 00000000..afaf07e9 --- /dev/null +++ b/mobile/README.md @@ -0,0 +1,75 @@ +IITC Mobile (IITCM) +=================== + +``` +# # ####### ####### ###### ####### # ###### # # +## # # # # # # # # # # # # # +# # # # # # # # # # # # # # # +# # # # # # ###### ##### # # # # # +# # # # # # # # # ####### # # # +# ## # # # # # # # # # # # +# # ####### # # # ####### # # ###### # +``` + +- This is **alpha quality** software +- There are more bugs than in all Indiana Jones titles combined +- It will kill your data plan. Caching has been explicitly disabled for testing, so every restart nets you ~500 KiB of code. Since it uses raster maps instead of vector ones, the transfer volume is larger than for Google Maps, too. + + +### read this guide thoroughly. + +How to report bugs +------------------ + +Be sure to [read the normal guide on how to report bugs](https://github.com/breunigs/ingress-intel-total-conversion/blob/gh-pages/HACKING.md#how-do-i-report-bugs). In addition, provide these details: +- Android version +- device used (exact name) +- screen size in pixels + +**I will close all bug reports without this information without comment.** Gather all information, then make a new issue. It seems you can’t reopen a ticket if I close it, so this is necessary unfortunately. + + +Can I share this? +----------------- + +You may, but only with technically skilled friends. If you do please point them to this page only. Reading these guides ensures only high quality bug reports are made which speeds up development. This is in everyone’s interest, as the time spent on working on bogus reports is not spent on improvements. + +Once the app is ready for a broader audience, this will be made easier. For now it should only be used by developers who can help or send in improvements. + + +How do I…? +---------- + +- **Login:** on tablets, this should just work. On smartphones you’ll see a black screen with some blue lines. This is the normal Ingress login page and you need to scroll to see the login button. +- **Clear Data/Cache:** Open app launcher and find the IITCM icon. Press-and-hold and then drag it to “App Info” on the top right of the screen. There’s a “clear data” and “clear cache” button. +- **Logout:** Not possible from within the app. See *Clear Data/Cache* and use “clear data”. +- **Reload:** Not possible from within the app. Instead, open your recent applications and force close IITCM. It should reload after restarting. +- **“Your account has not been enabled to play Ingress“**: See *Reload*. +- **Install:** You can find this out yourself. If you don’t know how, then please wait for a more polished version of IITCM. +- **Update:** You can check if there’s a new APK build by looking at the [apk_version](https://github.com/breunigs/ingress-intel-total-conversion/raw/gh-pages/mobile/apk_version file)[.](https://mathphys.fsk.uni-heidelberg.de/~stefan/test/IngressIntelTC.apk) It contains the date of the last update. The app uses the latest IITC hourly build automatically. You may need to clear your cache to force a reload, see *Clear Data/Cache*. Also note that the dot after “apk_version” is the download link to the APK. + +How does this basically work? +----------------------------- + +The Android App uses a WebView to render the normal web page. Some code is required to make this work like one would expect, but in a nutshell this is a webbrowser without URL bar. On page load, the app injects `bootstrap.js` which is a specialized loader. While it has some additional checks to make the boot process nicer on slower mobile phones but is mainly required to load vanilla IITC. It’s not possible to load IITC directly due to contraints defined by the injection method. After IITC is injected, it works the same way as on desktop browsers. Tablets are served the desktop version and apart from larger portals there is no change to the GUI. Smartphones execute the code in `code/smartphone.js` and load `smartphone.css`. This is required because their display is too small for the desktop version. + +Debugging +--------- + +If you want to debug the APK, I suggest [reading up on Google’s documentation](https://developer.android.com/index.html). + +Debugging IITC(M) **after** it has booted is relatively easy: you can switch to the “debug” tab, which is a low end developer console. It renders all calls to `console.*`, so you can use it just like you expect. It may be easier to develop in a desktop browser. Set it up like explained [in the normal hacking guide](https://github.com/breunigs/ingress-intel-total-conversion/blob/gh-pages/HACKING.md), but fake your user agent or modify the detection in `code/smartphone.js` and `main.js`. You don’t need to rebuild the APK to point it to your `iitc-test.user.js` file. Instead, modify `mobile/bootstrap.js` and add yourself with device ID and URL, then [send in a pull request](https://github.com/breunigs/ingress-intel-total-conversion/blob/gh-pages/HACKING.md#sending-patches). The device ID is printed to the debug console on IITC boot. + +Debugging IITC(M) **before** it has booted requires the Android Developer Tools. Connecting your device and running `adb logcat` should print the debug log to your computer until the low-end dev console mentioned above is available. You may need to root your device. + + +Building the APK +---------------- + +No idea. Please write docs and send patches. + + +Download APK +------------ + +Well, glad you read to the end. Enjoy and send patches, bug reports, postcards and love. (If you are wondering where the download link is, I’m wondering if you have actually read the page.) diff --git a/mobile/apk_version b/mobile/apk_version new file mode 100644 index 00000000..1a9fd432 --- /dev/null +++ b/mobile/apk_version @@ -0,0 +1 @@ +0.1-2013-02-23 diff --git a/mobile/bootstrap.js b/mobile/bootstrap.js new file mode 100644 index 00000000..4e37a02b --- /dev/null +++ b/mobile/bootstrap.js @@ -0,0 +1,59 @@ +var rebuidAttemptCount=0; +var iitcReadyCount=0; +var iitcURL; + +function rebuildUI() { + // this is sub-optimal, but we need a way to know if we're on a login page, or the real page. + if(document.getElementById('dashboard_container')) { + var dash = document.getElementById('dashboard_container'); + // dashboard is there, not conclusive + if(dash.childNodes[0].innerHTML == 'Welcome to Ingress') { + // not really 'ready', but the user needs the page for login purposes + android.pageReady(1); + return; + } else if(!document.getElementById('map_canvas')) { + // not loaded yet? we'll wait + if(rebuidAttemptCount < 10) { // wait up to 5 seconds, then assume failure and show the page + setTimeout(rebuildUI, 500); + rebuidAttemptCount++; + if(window.console) { console.log('Looking for Ingress Intel UI elements: ' + rebuidAttemptCount); } + return; + } else { + if(window.console) { console.log('Something is wrong, flipping views'); } + android.pageReady(1); + return; + } + } else { + window.UIDone = true; + } + + window.console.log(window.deviceID); + window.console.log(navigator.userAgent); + + switch(window.deviceID) { + case '41ddb619ea1fe75a': // blakjakau - TABLET + iitcURL = 'http://mathphys.fsk.uni-heidelberg.de:8000/test.js'; + break; + + case 'f30c2cce86c1c7': // breunigs + iitcURL = 'http://mathphys.fsk.uni-heidelberg.de:8000/test.js'; + break; + + default: + iitcURL = 'http://mathphys.fsk.uni-heidelberg.de:8000/test.js'; + } + window.loadJS(iitcURL); // load iitc and let it do its thing. + window.iitcReadyTimer = setInterval(function() { + if(window.iitcLoaded == true || iitcReadyCount > 10) { + //wait up to 10 seconds from calling iitc to fliping the webviews + //if for some reason iitc isn't loading, the user will just get the vanila ingres.com/intel experience + try { + android.pageReady(1); // tell the app to flip the webviews + clearInterval(window.iitcReadyTimer); // and we're done. + } catch(e) { if(window.console) { console.log(e.message); } } + } + iitcReadyCount++; + }, 500); + } +} +rebuildUI(); diff --git a/mobile/smartphone.css b/mobile/smartphone.css new file mode 100644 index 00000000..b04c1b21 --- /dev/null +++ b/mobile/smartphone.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/plugins/README.md b/plugins/README.md index 95a611b7..d23f3a3c 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -9,19 +9,23 @@ Plugins are installed the same way the total conversion script is. Please see th Available Plugins ----------------- -- [**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. +- [**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. -- [**Max-Links**]((https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/rmax-links.user.js) Calculates how to link the portals to create the maximum number of fields. +- [**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. +- [**Max-Links**]((https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/max-links.user.js) Calculates how to link the portals to create the maximum number of fields. ### available only with the development version -The development version is not available publicly, you need to build it yourself. See [HACKING.md](https://github.com/breunigs/ingress-intel-total-conversion/blob/gh-pages/HACKING.md#hacking) for guides. +[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. + + + -- [**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+** -- [**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+** Hacking ------- diff --git a/plugins/compute-ap-stats.user.js b/plugins/compute-ap-stats.user.js index 3c0e8bd7..222e1ecd 100644 --- a/plugins/compute-ap-stats.user.js +++ b/plugins/compute-ap-stats.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @id iitc-plugin-compute-ap-stats@Hollow011 // @name iitc: Compute AP statistics -// @version 0.1 +// @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/compute-ap-stats.user.js // @downloadURL https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/compute-ap-stats.user.js @@ -21,35 +21,59 @@ if(typeof window.plugin !== 'function') window.plugin = function() {}; window.plugin.compAPStats = function() {}; window.plugin.compAPStats.setupCallback = function() { - $('#toolbox').append('Compute AP Stats '); + // add a new div to the bottom of the sidebar and style it + $('#sidebar').append('
'); + $('#available_ap_display').css({'color':'#ffce00', 'font-size':'90%', 'padding':'4px 2px'}); + + // do an initial calc for sidebar sizing purposes + window.plugin.compAPStats.onPositionMove(); + + // make the value update when the map data updates + var handleDataResponseOrig = window.handleDataResponse; + window.handleDataResponse = function(data, textStatus, jqXHR) { + handleDataResponseOrig(data, textStatus, jqXHR); + window.plugin.compAPStats.onPositionMove(); + } +} + +window.plugin.compAPStats.onPositionMove = function() { + var result = window.plugin.compAPStats.compAPStats(); + $('#available_ap_display').html('Available AP in this area:' + + '' + + '' + + '
Enlightened:' + digits(result[1]) + '
Resistance:' + digits(result[0]) + '
'); } window.plugin.compAPStats.compAPStats = function() { - + var totalAP_RES = 0; var totalAP_ENL = 0; - + var allResEdges = []; var allResFields = []; var allEnlEdges = []; var allEnlFields = []; - - + + var displayBounds = map.getBounds(); + // Grab every portal in the viewable area and compute individual AP stats $.each(window.portals, function(ind, portal) { var d = portal.options.details; - + + // eliminate offscreen portals (selected, and in padding) + if(!displayBounds.contains(portal.getLatLng())) return true; + var portalStats = getAttackApGain(d); var portalSum = portalStats.resoAp + portalStats.captureAp; - + if (getTeam(d) === TEAM_ENL) { totalAP_RES += portalSum; - + $.each(d.portalV2.linkedEdges, function(ind, edge) { - if(!edge) return true; - allEnlEdges.push(edge.edgeGuid); + if(!edge) return true; + allEnlEdges.push(edge.edgeGuid); }); - + $.each(d.portalV2.linkedFields, function(ind, field) { if(!field) return true; allEnlFields.push(field); @@ -57,50 +81,38 @@ window.plugin.compAPStats.compAPStats = function() { } else if (getTeam(d) === TEAM_RES) { totalAP_ENL += portalSum; - + $.each(d.portalV2.linkedEdges, function(ind, edge) { if(!edge) return true; allResEdges.push(edge.edgeGuid); }); - + $.each(d.portalV2.linkedFields, function(ind, field) { if(!field) return true; allResFields.push(field); }); - } else { + } else { // it's a neutral portal, potential for both teams. by definition no fields or edges totalAP_ENL += portalSum; totalAP_RES += portalSum; } }); - + // Compute team field AP allResFields = uniqueArray(allResFields); totalAP_ENL += (allResFields.length * DESTROY_FIELD); allEnlFields = uniqueArray(allEnlFields); totalAP_RES += (allEnlFields.length * DESTROY_FIELD); - + // Compute team Link AP allResEdges = uniqueArray(allResEdges); totalAP_ENL += (allResEdges.length * DESTROY_LINK); allEnlEdges = uniqueArray(allEnlEdges); totalAP_RES += (allEnlEdges.length * DESTROY_LINK); - + return [totalAP_RES, totalAP_ENL]; } -window.plugin.compAPStats.guess = function() { - var res = window.plugin.compAPStats.compAPStats(); - var totalAP_RES = res[0]; - var totalAP_ENL = res[1]; - - var s = 'Calculated AP gain potential:\n\n'; - s += 'Available Resistance AP:\t' + digits(totalAP_RES) + '\n'; - s += 'Available Enlightened AP:\t' + digits(totalAP_ENL) + '\n'; - - alert(s); -} - var setup = function() { window.plugin.compAPStats.setupCallback(); } diff --git a/plugins/player-tracker.user.js b/plugins/player-tracker.user.js index 6a2cb50c..08cf3bdf 100644 --- a/plugins/player-tracker.user.js +++ b/plugins/player-tracker.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @id iitc-plugin-player-tracker@breunigs // @name iitc: player tracker -// @version 0.4 +// @version 0.5 // @namespace https://github.com/breunigs/ingress-intel-total-conversion // @updateURL https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/player-tracker.user.js // @downloadURL https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/player-tracker.user.js @@ -76,10 +76,11 @@ window.plugin.playerTracker.processNewData = function(data) { $.each(json[2].plext.markup, function(ind, markup) { switch(markup[0]) { case 'TEXT': - // Destroy link messages depend on how the link was originally - // created. Therefore it’s not clear which portal the player is - // at, so ignore it. - if(markup[1].plain.indexOf('destroyed the Link') !== -1) { + // Destroy link and field messages depend on where the link or + // field was originally created. Therefore it’s not clear which + // portal the player is at, so ignore it. + if(markup[1].plain.indexOf('destroyed the Link') !== -1 + || markup[1].plain.indexOf('destroyed a Control Field') !== -1) { skipThisMessage = true; return false; } diff --git a/plugins/show-address.user.js b/plugins/show-address.user.js new file mode 100644 index 00000000..50c72c14 --- /dev/null +++ b/plugins/show-address.user.js @@ -0,0 +1,55 @@ +// ==UserScript== +// @id iitc-plugin-show-address@vita10gy +// @name iitc: show portal address in sidebar +// @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-address.user.js +// @downloadURL https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/show-address.user.js +// @description Portal address will show in the sidebar. +// @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.portalAddress = function() {}; + +window.plugin.portalAddress.portalDetail = function(data) { + // If there's 4 sets of comma delimieted info the last one is the + // country, so get rid of it. If the country is in the [2] then it + // doesn't matter because address is usually short enough to fit. + var d = data.portalDetails.portalV2; + var address = d.descriptiveText.ADDRESS.split(',').splice(0,3).join(','); + $('.imgpreview').append('
'+address+'
'); +} + +var setup = function() { + window.addHook('portalDetailsUpdated', window.plugin.portalAddress.portalDetail); + $('head').append(''); +} + +// 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); diff --git a/screenshots/plugin_draw_tools.png b/screenshots/plugin_draw_tools.png new file mode 100644 index 00000000..4f2377a5 Binary files /dev/null and b/screenshots/plugin_draw_tools.png differ diff --git a/screenshots/screen.png b/screenshots/screen.png index ddce98ea..cd65b064 100644 Binary files a/screenshots/screen.png and b/screenshots/screen.png differ diff --git a/screenshots/screen_small.png b/screenshots/screen_small.png index 20546e86..8bbe65b7 100644 Binary files a/screenshots/screen_small.png and b/screenshots/screen_small.png differ diff --git a/tools/iitc-nightly.sh b/tools/iitc-nightly.sh new file mode 100644 index 00000000..ad593803 --- /dev/null +++ b/tools/iitc-nightly.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -e + +cd /tmp +rm -rf ingress-intel-total-conversion +git clone --depth=1 git://github.com/breunigs/ingress-intel-total-conversion.git +cd ingress-intel-total-conversion +./build.py + +date=$(date +"%Y-%m-%d") +commit=$(git rev-parse HEAD | head -c 10) + +# https://github.com/andreafabrizi/Dropbox-Uploader +dropbox-up upload iitc-debug.user.js iitc-nightly/iitc-nightly-$date-$commit.user.js