diff --git a/code/boot.js b/code/boot.js index dbd9b265..aa7a7004 100644 --- a/code/boot.js +++ b/code/boot.js @@ -177,12 +177,12 @@ window.setupSidebarToggle = function() { if(sidebar.is(':visible')) { sidebar.hide(); $('.leaflet-right').css('margin-right','0'); - toggle.html('◢
◥'); + toggle.html(''); toggle.css('right', '0'); } else { sidebar.show(); $('.leaflet-right').css('margin-right', SIDEBAR_WIDTH+1+'px'); - toggle.html('◣
◤'); + toggle.html(''); toggle.css('right', SIDEBAR_WIDTH+1+'px'); } }); diff --git a/code/chat.js b/code/chat.js index 8e44a562..77969845 100644 --- a/code/chat.js +++ b/code/chat.js @@ -469,14 +469,14 @@ window.chat.getActive = function() { window.chat.toggle = function() { var c = $('#chat, #chatcontrols'); if(c.hasClass('expand')) { - $('#chatcontrols a:first').text('◢◣'); + $('#chatcontrols a:first').html(''); c.removeClass('expand'); var div = $('#chat > div:visible'); div.data('ignoreNextScroll', true); div.scrollTop(99999999); // scroll to bottom $('.leaflet-control').css('margin-left', '13px'); } else { - $('#chatcontrols a:first').text('◥◤'); + $('#chatcontrols a:first').html(''); c.addClass('expand'); $('.leaflet-control').css('margin-left', '720px'); chat.needMoreMessages(); diff --git a/main.js b/main.js index 31bd434e..1ceac65c 100644 --- a/main.js +++ b/main.js @@ -53,16 +53,16 @@ for(var i = 0; i < d.length; i++) { // 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' - + '' + //~ + '' + '' + ''; document.getElementsByTagName('body')[0].innerHTML = '' + '
Loading, please wait
' + '' + '' + '' - + '
' + + '' + '
' // enable scrolling for small screens + ' ' + + '
' + + '
'; // putting everything in a wrapper function that in turn is placed in a // script tag on the website allows us to execute in the site’s context @@ -130,19 +143,31 @@ var RANGE_INDICATOR_COLOR = 'red'; var RESO_NRG = [0, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 6000]; var MAX_XM_PER_LEVEL = [0, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000]; var MIN_AP_FOR_LEVEL = [0, 10000, 30000, 70000, 150000, 300000, 600000, 1200000]; -var HACK_RANGE = 35; // in meters, max. distance from portal to be able to access it +var HACK_RANGE = 40; // in meters, max. distance from portal to be able to access it var OCTANTS = ['E', 'NE', 'N', 'NW', 'W', 'SW', 'S', 'SE']; var DEFAULT_PORTAL_IMG = 'http://commondatastorage.googleapis.com/ingress/img/default-portal-image.png'; +var DESTROY_RESONATOR = 75; //AP for destroying portal +var DESTROY_LINK = 187; //AP for destroying link +var DESTROY_FIELD = 750; //AP for destroying field // OTHER MORE-OR-LESS CONSTANTS ////////////////////////////////////// var NOMINATIM = 'http://nominatim.openstreetmap.org/search?format=json&limit=1&q='; var DEG2RAD = Math.PI / 180; var TEAM_NONE = 0, TEAM_RES = 1, TEAM_ENL = 2; var TEAM_TO_CSS = ['none', 'res', 'enl']; +var TYPE_UNKNOWN = 0, TYPE_PORTAL = 1, TYPE_LINK = 2, TYPE_FIELD = 3, TYPE_PLAYER = 4, TYPE_CHAT = 5, TYPE_RESONATOR = 6; // make PLAYER variable available in site context var PLAYER = window.PLAYER; var CHAT_SHRINKED = 60; +// Minimum zoom level resonator will display +var RESONATOR_DISPLAY_ZOOM_LEVEL = 17; + +// Constants for resonator positioning +var SLOT_TO_LAT = [0, Math.sqrt(2)/2, 1, Math.sqrt(2)/2, 0, -Math.sqrt(2)/2, -1, -Math.sqrt(2)/2]; +var SLOT_TO_LNG = [1, Math.sqrt(2)/2, 0, -Math.sqrt(2)/2, -1, -Math.sqrt(2)/2, 0, Math.sqrt(2)/2]; +var EARTH_RADIUS=6378137; + // STORAGE /////////////////////////////////////////////////////////// // global variables used for storage. Most likely READ ONLY. Proper // way would be to encapsulate them in an anonymous function and write @@ -163,6 +188,7 @@ var portalsLayers, linksLayer, fieldsLayer; window.portals = {}; window.links = {}; window.fields = {}; +window.resonators = {}; // plugin framework. Plugins may load earlier than iitc, so don’t // overwrite data @@ -171,6 +197,55 @@ if(typeof window.plugin !== 'function') window.plugin = function() {}; +// PLUGIN HOOKS //////////////////////////////////////////////////////// +// Plugins may listen to any number of events by specifying the name of +// the event to listen to and handing a function that should be exe- +// cuted when an event occurs. Callbacks will receive additional data +// the event created as their first parameter. The value is always a +// hash that contains more details. +// +// For example, this line will listen for portals to be added and print +// the data generated by the event to the console: +// window.addHook('portalAdded', function(data) { console.log(data) }); +// +// Boot hook: booting is handled differently because IITC may not yet +// be available. Have a look at the plugins in plugins/. All +// code before “// PLUGIN START” and after “// PLUGIN END” os +// required to successfully boot the plugin. +// +// Here’s more specific information about each event: +// portalAdded: called when a portal has been received and is about to +// be added to its layer group. Note that this does NOT +// mean it is already visible or will be, shortly after. +// If a portal is added to a hidden layer it may never be +// shown at all. Injection point is in +// code/map_data.js#renderPortal near the end. Will hand +// the Leaflet CircleMarker for the portal in "portal" var. + +window._hooks = {} +window.VALID_HOOKS = ['portalAdded']; + +window.runHooks = function(event, data) { + if(VALID_HOOKS.indexOf(event) === -1) throw('Unknown event type: ' + event); + + if(!_hooks[event]) return; + $.each(_hooks[event], function(ind, callback) { + callback(data); + }); +} + + +window.addHook = function(event, callback) { + if(VALID_HOOKS.indexOf(event) === -1) throw('Unknown event type: ' + event); + if(typeof callback !== 'function') throw('Callback must be a function.'); + + if(!_hooks[event]) + _hooks[event] = [callback]; + else + _hooks[event].push(callback); +} + + // MAP DATA ////////////////////////////////////////////////////////// // these functions handle how and which entities are displayed on the @@ -242,8 +317,16 @@ window.handleDataResponse = function(data, textStatus, jqXHR) { // to be in the foreground, or they cannot be clicked. See // https://github.com/Leaflet/Leaflet/issues/185 var ppp = []; + var p2f = {}; $.each(m, function(qk, val) { $.each(val.deletedGameEntityGuids, function(ind, guid) { + if(getTypeByGuid(guid) === TYPE_FIELD && window.fields[guid] !== undefined) { + $.each(window.fields[guid].options.vertices, function(ind, vertex) { + if(window.portals[vertex.guid] === undefined) return true; + fieldArray = window.portals[vertex.guid].options.portalV2.linkedFields; + fieldArray.splice($.inArray(guid, fieldArray), 1); + }); + } window.removeByGuid(guid); }); @@ -265,17 +348,37 @@ window.handleDataResponse = function(data, textStatus, jqXHR) { ppp.push(ent); // delay portal render - } else if(ent[2].edge !== undefined) + } else if(ent[2].edge !== undefined) { renderLink(ent); - else if(ent[2].capturedRegion !== undefined) + } else if(ent[2].capturedRegion !== undefined) { + $.each(ent[2].capturedRegion, function(ind, vertex) { + if(p2f[vertex.guid] === undefined) + p2f[vertex.guid] = new Array(); + p2f[vertex.guid].push(ent[0]); + }); renderField(ent); - else + } else { throw('Unknown entity: ' + JSON.stringify(ent)); + } }); }); + $.each(ppp, function(ind, portal) { + if(portal[2].portalV2['linkedFields'] === undefined) { + portal[2].portalV2['linkedFields'] = []; + } + if(p2f[portal[0]] !== undefined) { + $.merge(p2f[portal[0]], portal[2].portalV2['linkedFields']); + portal[2].portalV2['linkedFields'] = uniqueArray(p2f[portal[0]]); + } + }); + $.each(ppp, function(ind, portal) { renderPortal(portal); }); - if(portals[selectedPortal]) portals[selectedPortal].bringToFront(); + if(portals[selectedPortal]) { + try { + portals[selectedPortal].bringToFront(); + } catch(e) { /* portal is now visible, catch Leaflet error */ } + } if(portalInUrlAvailable) { renderPortalDetails(urlPortal); @@ -294,13 +397,16 @@ window.cleanUp = function() { var minlvl = getMinPortalLevel(); for(var i = 0; i < portalsLayers.length; i++) { // i is also the portal level - portalsLayers[i].eachLayer(function(portal) { + portalsLayers[i].eachLayer(function(item) { + var itemGuid = item.options.guid; + // check if 'item' is a portal + if(getTypeByGuid(itemGuid) != TYPE_PORTAL) return true; // portal must be in bounds and have a high enough level. Also don’t // remove if it is selected. - if(portal.options.guid == window.selectedPortal || - (b.contains(portal.getLatLng()) && i >= minlvl)) return; + if(itemGuid == window.selectedPortal || + (b.contains(item.getLatLng()) && i >= minlvl)) return true; cnt[0]++; - portalsLayers[i].removeLayer(portal); + portalsLayers[i].removeLayer(item); }); } linksLayer.eachLayer(function(link) { @@ -316,31 +422,30 @@ window.cleanUp = function() { console.log('removed out-of-bounds: '+cnt[0]+' portals, '+cnt[1]+' links, '+cnt[2]+' fields'); } + // removes given entity from map window.removeByGuid = function(guid) { - // portals end in “.11” or “.12“, links in “.9", fields in “.b” - // .11 == portals - // .12 == portals - // .9 == links - // .b == fields - // .c == player/creator - // .d == chat messages - switch(guid.slice(33)) { - case '11': - case '12': + switch(getTypeByGuid(guid)) { + case TYPE_PORTAL: if(!window.portals[guid]) return; var p = window.portals[guid]; for(var i = 0; i < portalsLayers.length; i++) portalsLayers[i].removeLayer(p); break; - case '9': + case TYPE_LINK: if(!window.links[guid]) return; linksLayer.removeLayer(window.links[guid]); break; - case 'b': + case TYPE_FIELD: if(!window.fields[guid]) return; fieldsLayer.removeLayer(window.fields[guid]); break; + case TYPE_RESONATOR: + if(!window.resonators[guid]) return; + var r = window.resonators[guid]; + for(var i = 1; i < portalsLayers.length; i++) + portalsLayers[i].removeLayer(r); + break; default: console.warn('unknown GUID type: ' + guid); //window.debug.printStackTrace(); @@ -351,59 +456,148 @@ window.removeByGuid = function(guid) { // renders a portal on the map from the given entity window.renderPortal = function(ent) { - removeByGuid(ent[0]); - if(Object.keys(portals).length >= MAX_DRAWN_PORTALS && ent[0] != selectedPortal) - return; - - var latlng = [ent[2].locationE6.latE6/1E6, ent[2].locationE6.lngE6/1E6]; - // needs to be checked before, so the portal isn’t added to the - // details list and other places - //if(!getPaddedBounds().contains(latlng)) return; + return removeByGuid(ent[0]); // hide low level portals on low zooms var portalLevel = getPortalLevel(ent[2]); - if(portalLevel < getMinPortalLevel() && ent[0] != selectedPortal) return; - - // pre-load player names for high zoom levels - if(map.getZoom() >= PRECACHE_PLAYER_NAMES_ZOOM) { - if(ent[2].captured && ent[2].captured.capturingPlayerId) - getPlayerName(ent[2].captured.capturingPlayerId); - if(ent[2].resonatorArray && ent[2].resonatorArray.resonators) - $.each(ent[2].resonatorArray.resonators, function(ind, reso) { - if(reso) getPlayerName(reso.ownerGuid); - }); - } + if(portalLevel < getMinPortalLevel() && ent[0] != selectedPortal) + return removeByGuid(ent[0]); var team = getTeam(ent[2]); + // do nothing if portal did not change + var layerGroup = portalsLayers[parseInt(portalLevel)]; + var old = findEntityInLeaflet(layerGroup, window.portals, ent[0]); + if(old) { + var oo = old.options; + var u = oo.team !== team; + u = u || oo.level !== portalLevel; + // nothing for the portal changed, so don’t update. Let resonators + // manage themselves if they want to be updated. + if(!u) return renderResonators(ent); + removeByGuid(ent[0]); + } + + // there were changes, remove old portal + removeByGuid(ent[0]); + + var latlng = [ent[2].locationE6.latE6/1E6, ent[2].locationE6.lngE6/1E6]; + + // pre-loads player names for high zoom levels + loadPlayerNamesForPortal(ent[2]); + + + var lvWeight = Math.max(2, portalLevel / 1.5); + var lvRadius = Math.max(portalLevel + 3, 5); + var p = L.circleMarker(latlng, { - radius: 7, + radius: lvRadius, color: ent[0] == selectedPortal ? COLOR_SELECTED_PORTAL : COLORS[team], opacity: 1, - weight: 3, + weight: lvWeight, fillColor: COLORS[team], fillOpacity: 0.5, clickable: true, level: portalLevel, + team: team, details: ent[2], guid: ent[0]}); - p.on('remove', function() { delete window.portals[this.options.guid]; }); - p.on('add', function() { + p.on('remove', function() { + var portalGuid = this.options.guid + + // remove attached resonators, skip if + // all resonators have already removed by zooming + if(isResonatorsShow()) { + for(var i = 0; i <= 7; i++) + removeByGuid(portalResonatorGuid(portalGuid,i)); + } + delete window.portals[portalGuid]; + if(window.selectedPortal === portalGuid) { + window.unselectOldPortal(); + window.map.removeLayer(window.portalAccessIndicator); + window.portalAccessIndicator = null; + } + }); + + p.on('add', function() { + // enable for debugging + if(window.portals[this.options.guid]) throw('duplicate portal detected'); window.portals[this.options.guid] = this; // handles the case where a selected portal gets removed from the // map by hiding all portals with said level if(window.selectedPortal != this.options.guid) window.portalResetColor(this); }); + p.on('click', function() { window.renderPortalDetails(ent[0]); }); p.on('dblclick', function() { window.renderPortalDetails(ent[0]); window.map.setView(latlng, 17); }); + + window.renderResonators(ent); + + window.runHooks('portalAdded', {portal: p}); + // portalLevel contains a float, need to round down - p.addTo(portalsLayers[parseInt(portalLevel)]); + p.addTo(layerGroup); +} + +window.renderResonators = function(ent) { + var portalLevel = getPortalLevel(ent[2]); + if(portalLevel < getMinPortalLevel() && ent[0] != selectedPortal) return; + + if(!isResonatorsShow()) return; + + for(var i=0; i < ent[2].resonatorArray.resonators.length; i++) { + var rdata = ent[2].resonatorArray.resonators[i]; + + if(rdata == null) continue; + + if(window.resonators[portalResonatorGuid(ent[0],i)]) continue; + + // offset in meters + var dn = rdata.distanceToPortal*SLOT_TO_LAT[rdata.slot]; + var de = rdata.distanceToPortal*SLOT_TO_LNG[rdata.slot]; + + // Coordinate offset in radians + var dLat = dn/EARTH_RADIUS; + var dLon = de/(EARTH_RADIUS*Math.cos(Math.PI/180*(ent[2].locationE6.latE6/1E6))); + + // OffsetPosition, decimal degrees + var lat0 = ent[2].locationE6.latE6/1E6 + dLat * 180/Math.PI; + var lon0 = ent[2].locationE6.lngE6/1E6 + dLon * 180/Math.PI; + var Rlatlng = [lat0, lon0]; + var r = L.circleMarker(Rlatlng, { + radius: 3, + // #AAAAAA outline seems easier to see the fill opacity + color: '#AAAAAA', + opacity: 1, + weight: 1, + fillColor: COLORS_LVL[rdata.level], + fillOpacity: rdata.energyTotal/RESO_NRG[rdata.level], + clickable: false, + level: rdata.level, + details: rdata, + pDetails: ent[2], + guid: portalResonatorGuid(ent[0],i) }); + + r.on('remove', function() { delete window.resonators[this.options.guid]; }); + r.on('add', function() { window.resonators[this.options.guid] = this; }); + + r.addTo(portalsLayers[parseInt(portalLevel)]); + } +} + +// append portal guid with -resonator-[slot] to get guid for resonators +window.portalResonatorGuid = function(portalGuid, slot) { + return portalGuid + '-resonator-' + slot; +} + +window.isResonatorsShow = function() { + return map.getZoom() >= RESONATOR_DISPLAY_ZOOM_LEVEL; } window.portalResetColor = function(portal) { @@ -412,8 +606,12 @@ window.portalResetColor = function(portal) { // renders a link on the map from the given entity window.renderLink = function(ent) { - removeByGuid(ent[0]); - if(Object.keys(links).length >= MAX_DRAWN_LINKS) return; + if(Object.keys(links).length >= MAX_DRAWN_LINKS) + return removeByGuid(ent[0]); + + // assume that links never change. If they do, they will have a + // different ID. + if(findEntityInLeaflet(linksLayer, links, ent[0])) return; var team = getTeam(ent[2]); var edge = ent[2].edge; @@ -433,14 +631,23 @@ window.renderLink = function(ent) { if(!getPaddedBounds().intersects(poly.getBounds())) return; poly.on('remove', function() { delete window.links[this.options.guid]; }); - poly.on('add', function() { window.links[this.options.guid] = this; }); - poly.addTo(linksLayer).bringToBack(); + poly.on('add', function() { + // enable for debugging + if(window.links[this.options.guid]) throw('duplicate link detected'); + window.links[this.options.guid] = this; + this.bringToBack(); + }); + poly.addTo(linksLayer); } // renders a field on the map from a given entity window.renderField = function(ent) { - window.removeByGuid(ent[0]); - if(Object.keys(fields).length >= MAX_DRAWN_FIELDS) return; + if(Object.keys(fields).length >= MAX_DRAWN_FIELDS) + return window.removeByGuid(ent[0]); + + // assume that fields never change. If they do, they will have a + // different ID. + if(findEntityInLeaflet(fieldsLayer, fields, ent[0])) return; var team = getTeam(ent[2]); var reg = ent[2].capturedRegion; @@ -455,13 +662,40 @@ window.renderField = function(ent) { stroke: false, clickable: false, smoothFactor: 10, + vertices: ent[2].capturedRegion, + lastUpdate: ent[1], guid: ent[0]}); if(!getPaddedBounds().intersects(poly.getBounds())) return; poly.on('remove', function() { delete window.fields[this.options.guid]; }); - poly.on('add', function() { window.fields[this.options.guid] = this; }); - poly.addTo(fieldsLayer).bringToBack(); + poly.on('add', function() { + // enable for debugging + if(window.fields[this.options.guid]) console.warn('duplicate field detected'); + window.fields[this.options.guid] = this; + this.bringToBack(); + }); + poly.addTo(fieldsLayer); +} + + +// looks for the GUID in either the layerGroup or entityHash, depending +// on which is faster. Will either return the Leaflet entity or null, if +// it does not exist. +// For example, to find a field use the function like this: +// field = findEntityInLeaflet(fieldsLayer, fields, 'asdasdasd'); +window.findEntityInLeaflet = function(layerGroup, entityHash, guid) { + // fast way + if(map.hasLayer(layerGroup)) return entityHash[guid] || null; + + // slow way in case the layer is currently hidden + var ent = null; + layerGroup.eachLayer(function(entity) { + if(entity.options.guid !== guid) return true; + ent = entity; + return false; + }); + return ent; } @@ -517,7 +751,7 @@ window.renderUpdateStatus = function() { t += ' RENDER LIMIT ' if(window.failedRequestCount > 0) - t += ' ' + window.failedRequestCount + ' requests failed.' + t += ' ' + window.failedRequestCount + ' failed.' t += '
('; var minlvl = getMinPortalLevel(); @@ -677,6 +911,13 @@ window.rangeLinkClick = function() { window.reportPortalIssue = function(info) { var t = 'Redirecting you to a Google Help Page. Once there, click on “Contact Us” in the upper right corner.\n\nThe text box contains all necessary information. Press CTRL+C to copy it.'; + var d = window.portals[window.selectedPortal].options.details; + + var info = 'Your Nick: ' + PLAYER.nickname + ' ' + + 'Portal: ' + d.portalV2.descriptiveText.TITLE + ' ' + + 'Location: ' + d.portalV2.descriptiveText.ADDRESS + +' (lat ' + (d.locationE6.latE6/1E6) + '; lng ' + (d.locationE6.lngE6/1E6) + ')'; + //codename, approx addr, portalname if(prompt(t, info) !== null) location.href = 'https://support.google.com/ingress?hl=en'; @@ -718,8 +959,51 @@ window.scrollBottom = function(elm) { } window.zoomToAndShowPortal = function(guid, latlng) { - renderPortalDetails(guid); map.setView(latlng, 17); + // if the data is available, render it immediately. Otherwise defer + // until it becomes available. + if(window.portals[guid]) + renderPortalDetails(guid); + else + urlPortal = guid; +} + +// translates guids to entity types +window.getTypeByGuid = function(guid) { + // portals end in “.11” or “.12“, links in “.9", fields in “.b” + // .11 == portals + // .12 == portals + // .9 == links + // .b == fields + // .c == player/creator + // .d == chat messages + // + // others, not used in web: + // .5 == resources (burster/resonator) + // .6 == XM + // .4 == media items, maybe all droppped resources (?) + // resonator guid is [portal guid]-resonator-[slot] + switch(guid.slice(33)) { + case '11': + case '12': + return TYPE_PORTAL; + + case '9': + return TYPE_LINK; + + case 'b': + return TYPE_FIELD; + + case 'c': + return TYPE_PLAYER; + + case 'd': + return TYPE_CHAT; + + default: + if(guid.slice(-11,-2) == 'resonator') return TYPE_RESONATOR; + return TYPE_UNKNOWN; + } } String.prototype.capitalize = function() { @@ -733,6 +1017,24 @@ if (typeof String.prototype.startsWith !== 'function') { }; } +window.prettyEnergy = function(nrg) { + return nrg> 1000 ? Math.round(nrg/1000) + ' k': nrg; +} + +window.setPermaLink = function(elm) { + var c = map.getCenter(); + var lat = Math.round(c.lat*1E6); + var lng = Math.round(c.lng*1E6); + var qry = 'latE6='+lat+'&lngE6='+lng+'&z=' + map.getZoom(); + $(elm).attr('href', 'http://www.ingress.com/intel?' + qry); +} + +window.uniqueArray = function(arr) { + return $.grep(arr, function(v, i) { + return $.inArray(v, arr) === i; + }); +} + @@ -763,16 +1065,17 @@ window.setupLargeImagePreview = function() { window.setupStyles = function() { $('head').append(''); } @@ -829,7 +1132,22 @@ window.setupMap = function() { map.attributionControl.setPrefix(''); // listen for changes and store them in cookies map.on('moveend', window.storeMapPosition); - map.on('zoomend', window.storeMapPosition); + map.on('zoomend', function() { + window.storeMapPosition; + + // remove all resonators if zoom out to < RESONATOR_DISPLAY_ZOOM_LEVEL + if(isResonatorsShow()) return; + for(var i = 1; i < portalsLayers.length; i++) { + portalsLayers[i].eachLayer(function(item) { + var itemGuid = item.options.guid; + // check if 'item' is a resonator + if(getTypeByGuid(itemGuid) != TYPE_RESONATOR) return true; + portalsLayers[i].removeLayer(item); + }); + } + + console.log('Remove all resonators'); + }); $("[name='leaflet-base-layers']").change(function () { writeCookie('ingress.intelmap.type', $(this).parent().index()); }); @@ -890,6 +1208,24 @@ window.setupPlayerStat = function() { ); } +window.setupSidebarToggle = function() { + $('#sidebartoggle').on('click', function() { + var toggle = $('#sidebartoggle'); + var sidebar = $('#sidebar'); + if(sidebar.is(':visible')) { + sidebar.hide(); + $('.leaflet-right').css('margin-right','0'); + toggle.html(''); + toggle.css('right', '0'); + } else { + sidebar.show(); + $('.leaflet-right').css('margin-right', SIDEBAR_WIDTH+1+'px'); + toggle.html(''); + toggle.css('right', SIDEBAR_WIDTH+1+'px'); + } + }); +} + // BOOTING /////////////////////////////////////////////////////////// @@ -900,6 +1236,7 @@ function boot() { window.setupGeosearch(); window.setupRedeem(); window.setupLargeImagePreview(); + window.setupSidebarToggle(); window.updateGameScore(); window.setupPlayerStat(); window.chat.setup(); @@ -1397,7 +1734,7 @@ window.chat.renderMsg = function(msg, nick, time, team) { var t = ''; var s = 'style="color:'+COLORS[team]+'"'; var title = nick.length >= 8 ? 'title="'+nick+'" class="help"' : ''; - return '

'+t+''+nick+''+msg+'

'; + return '

'+t+' <'+nick+'> '+msg+'

'; } @@ -1410,14 +1747,16 @@ window.chat.getActive = function() { window.chat.toggle = function() { var c = $('#chat, #chatcontrols'); if(c.hasClass('expand')) { - $('#chatcontrols a:first').text('expand'); + $('#chatcontrols a:first').html(''); c.removeClass('expand'); var div = $('#chat > div:visible'); div.data('ignoreNextScroll', true); div.scrollTop(99999999); // scroll to bottom + $('.leaflet-control').css('margin-left', '13px'); } else { - $('#chatcontrols a:first').text('shrink'); + $('#chatcontrols a:first').html(''); c.addClass('expand'); + $('.leaflet-control').css('margin-left', '720px'); chat.needMoreMessages(); } } @@ -1635,12 +1974,12 @@ window.chat.postMsg = function() { // returns displayable text+link about portal range window.getRangeText = function(d) { var range = getPortalRange(d); - return 'range: ' + return ['range', + '' + (range > 1000 ? Math.round(range/1000) + ' km' : Math.round(range) + ' m') - + ''; + + '']; } // generates description text from details for portal @@ -1694,30 +2033,31 @@ window.getModDetails = function(d) { } window.getEnergyText = function(d) { - var nrg = getPortalEnergy(d); - return 'energy: ' + (nrg > 1000 ? Math.round(nrg/1000) +' k': nrg); + var currentNrg = getCurrentPortalEnergy(d); + var totalNrg = getTotalPortalEnergy(d); + var inf = currentNrg + ' / ' + totalNrg; + var fill = prettyEnergy(currentNrg) + ' / ' + prettyEnergy(totalNrg) + return ['energy', '' + fill + '']; } window.getAvgResoDistText = function(d) { var avgDist = Math.round(10*getAvgResoDist(d))/10; - return '⌀ res dist: ' + avgDist + ' m'; -} - -window.getReportIssueInfoText = function(d) { - return ('Your Nick: '+PLAYER.nickname+' ' - + 'Portal: '+d.portalV2.descriptiveText.TITLE+' ' - + 'Location: '+d.portalV2.descriptiveText.ADDRESS - +' (lat '+(d.locationE6.latE6/1E6)+'; lng '+(d.locationE6.lngE6/1E6)+')' - ).replace(/['"]/, ''); + return ['⌀ res dist', avgDist + ' m']; } window.getResonatorDetails = function(d) { - console.log('rendering reso details'); var resoDetails = ''; - var slotsFilled = 0; - $.each(d.resonatorArray.resonators, function(ind, reso) { + // octant=slot: 0=E, 1=NE, 2=N, 3=NW, 4=W, 5=SW, 6=S, SE=7 + // resos in the display should be ordered like this: + // N NE Since the view is displayed in columns, they + // NW E need to be ordered like this: N, NW, W, SW, NE, + // W SE E, SE, S, i.e. 2 3 4 5 1 0 7 6 + // SW S + $.each([2, 3, 4, 5, 1, 0, 7, 6], function(ind, slot) { + var isLeft = slot >= 2 && slot <= 5; + var reso = d.resonatorArray.resonators[slot]; if(!reso) { - resoDetails += renderResonatorDetails(slotsFilled++, 0); + resoDetails += renderResonatorDetails(slot, 0, 0, null, null, isLeft); return true; } @@ -1725,9 +2065,11 @@ window.getResonatorDetails = function(d) { var v = parseInt(reso.energyTotal); var nick = window.getPlayerName(reso.ownerGuid); var dist = reso.distanceToPortal; + // if array order and slot order drift apart, at least the octant + // naming will still be correct. + slot = parseInt(reso.slot); - slotsFilled++; - resoDetails += renderResonatorDetails(parseInt(reso.slot), l, v, dist, nick); + resoDetails += renderResonatorDetails(slot, l, v, dist, nick, isLeft); }); return resoDetails; } @@ -1736,15 +2078,15 @@ window.getResonatorDetails = function(d) { // not work with raw details-hash. Needs digested infos instead: // slot: which slot this resonator occupies. Starts with 0 (east) and // rotates clockwise. So, last one is 7 (southeast). -window.renderResonatorDetails = function(slot, level, nrg, dist, nick) { - if(level == 0) { - var meter = ''; +window.renderResonatorDetails = function(slot, level, nrg, dist, nick, isLeft) { + if(level === 0) { + var meter = ''; } else { var max = RESO_NRG[level]; var fillGrade = nrg/max*100; - var inf = 'energy:\t\t' + nrg + ' / ' + max + ' (' + Math.round(fillGrade) + '%)' + '\n' - + 'level:\t\t' + level +'\n' + var inf = 'energy:\t\t' + nrg + ' / ' + max + ' (' + Math.round(fillGrade) + '%)\n' + + 'level:\t\t' + level + '\n' + 'distance:\t' + dist + 'm\n' + 'owner:\t\t' + nick + '\n' + 'octant:\t' + OCTANTS[slot]; @@ -1757,12 +2099,41 @@ window.renderResonatorDetails = function(slot, level, nrg, dist, nick) { var fill = ''; - var meter = '' - + fill + lbar + ''; + var meter = '' + fill + lbar + ''; } - var cls = slot <= 3 ? 'left' : 'right'; + var cls = isLeft ? 'left' : 'right'; var text = ''+(nick||'')+''; - return (slot <= 3 ? text+meter : meter+text) + '
'; + return (isLeft ? text+meter : meter+text) + '
'; +} + +// calculate AP gain from destroying portal +// so far it counts only resonators + links +window.getDestroyAP = function(d) { + var resoCount = 0; + + $.each(d.resonatorArray.resonators, function(ind, reso) { + if(!reso) return true; + resoCount += 1; + }); + + var linkCount = d.portalV2.linkedEdges ? d.portalV2.linkedEdges.length : 0; + var fieldCount = d.portalV2.linkedFields ? d.portalV2.linkedFields.length : 0; + + var resoAp = resoCount * DESTROY_RESONATOR; + var linkAp = linkCount * DESTROY_LINK; + var fieldAp = fieldCount * DESTROY_FIELD; + var sum = resoAp + linkAp + fieldAp; + + function tt(text) { + var t = 'Destroy:\n'; + t += resoCount + '×\tResonators\t= ' + digits(resoAp) + '\n'; + t += linkCount + '×\tLinks\t\t= ' + digits(linkAp) + '\n'; + t += fieldCount + '×\tFields\t\t= ' + digits(fieldAp) + '\n'; + t += 'Sum: ' + digits(sum) + ' AP'; + return '' + digits(text) + ''; + } + + return [tt('AP Gain'), tt(sum)]; } @@ -1977,22 +2348,23 @@ window.renderPortalDetails = function(guid) { links[link.isOrigin ? 'outgoing' : 'incoming']++; }); function linkExpl(t) { return ''+t+''; } - var linksText = linkExpl('links')+':'+linkExpl(' ↳ ' + links.incoming+'  •  '+links.outgoing+' ↴'); + var linksText = [linkExpl('links'), linkExpl(' ↳ ' + links.incoming+'  •  '+links.outgoing+' ↴')]; var player = d.captured && d.captured.capturingPlayerId ? getPlayerName(d.captured.capturingPlayerId) : null; - var playerText = player ? 'owner: ' + player : null; + var playerText = player ? ['owner', player] : null; var time = d.captured ? unixTimeToString(d.captured.capturedTime) : null; - var sinceText = time ? 'since: ' + time : null; + var sinceText = time ? ['since', time] : null; + + var linkedFields = ['fields', d.portalV2.linkedFields.length]; // collect and html-ify random data - var randDetails = [playerText, sinceText, getRangeText(d), getEnergyText(d), linksText, getAvgResoDistText(d)]; + var randDetails = [playerText, sinceText, getRangeText(d), getEnergyText(d), linksText, getAvgResoDistText(d), linkedFields, getDestroyAP(d)]; randDetails = randDetails.map(function(detail) { if(!detail) return ''; - detail = detail.split(':'); - detail = ''; + detail = ''; return detail; }).join('\n'); @@ -2026,7 +2398,7 @@ window.renderPortalDetails = function(guid) { + '
'+getResonatorDetails(d)+'
' + '
' + '' - + '' + + '' + '
' ); } @@ -2188,6 +2560,21 @@ window.setPlayerName = function(guid, nick) { } +window.loadPlayerNamesForPortal = function(portal_details) { + if(map.getZoom() < PRECACHE_PLAYER_NAMES_ZOOM) return; + var e = portal_details; + + if(e.captured && e.captured.capturingPlayerId) + getPlayerName(e.captured.capturingPlayerId); + + if(!e.resonatorArray || !e.resonatorArray.resonators) return; + + $.each(e.resonatorArray.resonators, function(ind, reso) { + if(reso) getPlayerName(reso.ownerGuid); + }); +} + + // DEBUGGING TOOLS /////////////////////////////////////////////////// // meant to be used from browser debugger tools and the like. @@ -2273,7 +2660,21 @@ window.getPortalLevel = function(d) { return hasReso ? Math.max(1, lvl/8) : 0; } -window.getPortalEnergy = function(d) { +window.getTotalPortalEnergy = function(d) { + var nrg = 0; + $.each(d.resonatorArray.resonators, function(ind, reso) { + if(!reso) return true; + var level = parseInt(reso.level); + var max = RESO_NRG[level]; + nrg += max; + }); + return nrg; +} + +// For backwards compatibility +window.getPortalEnergy = window.getTotalPortalEnergy; + +window.getCurrentPortalEnergy = function(d) { var nrg = 0; $.each(d.resonatorArray.resonators, function(ind, reso) { if(!reso) return true;