diff --git a/build.py b/build.py index a7ace7e2..55ac2f0a 100755 --- a/build.py +++ b/build.py @@ -202,8 +202,8 @@ def copytree(src, dst, symlinks=False, ignore=None): # if we're building mobile too if buildMobile: - if buildMobile not in ['debug','release']: - raise Exception("Error: buildMobile must be 'debug' or 'release'") + if buildMobile not in ['debug','release','copyonly']: + raise Exception("Error: buildMobile must be 'debug' or 'release' or 'copyonly'") # compile the user location script fn = "user-location.user.js" @@ -232,13 +232,14 @@ if buildMobile: copytree(os.path.join(outDir,"plugins"), "mobile/assets/plugins") - # now launch 'ant' to build the mobile project - retcode = os.system("ant -buildfile mobile/build.xml %s" % buildMobile) + if buildMobile != 'copyonly': + # now launch 'ant' to build the mobile project + retcode = os.system("ant -buildfile mobile/build.xml %s" % buildMobile) - if retcode != 0: - print ("Error: mobile app failed to build. ant returned %d" % retcode) - else: - shutil.copy("mobile/bin/IITC_Mobile-%s.apk" % buildMobile, os.path.join(outDir,"IITC_Mobile-%s.apk" % buildMobile) ) + if retcode != 0: + print ("Error: mobile app failed to build. ant returned %d" % retcode) + else: + shutil.copy("mobile/bin/IITC_Mobile-%s.apk" % buildMobile, os.path.join(outDir,"IITC_Mobile-%s.apk" % buildMobile) ) # vim: ai si ts=4 sw=4 sts=4 et diff --git a/code/boot.js b/code/boot.js index 29b052cc..c08acf8b 100644 --- a/code/boot.js +++ b/code/boot.js @@ -160,6 +160,7 @@ window.setupMap = function() { )); var addLayers = {}; + var hiddenLayer = []; portalsLayers = []; for(var i = 0; i <= 8; i++) { @@ -167,15 +168,21 @@ window.setupMap = function() { map.addLayer(portalsLayers[i]); var t = (i === 0 ? 'Unclaimed' : 'Level ' + i) + ' Portals'; addLayers[t] = portalsLayers[i]; + // Store it in hiddenLayer to remove later + if(!isLayerGroupDisplayed(t)) hiddenLayer.push(portalsLayers[i]); } fieldsLayer = L.layerGroup([]); map.addLayer(fieldsLayer, true); addLayers['Fields'] = fieldsLayer; + // Store it in hiddenLayer to remove later + if(!isLayerGroupDisplayed('Fields')) hiddenLayer.push(fieldsLayer); linksLayer = L.layerGroup([]); map.addLayer(linksLayer, true); addLayers['Links'] = linksLayer; + // Store it in hiddenLayer to remove later + if(!isLayerGroupDisplayed('Links')) hiddenLayer.push(linksLayer); window.layerChooser = new L.Control.Layers({ 'MapQuest OSM': views[0], @@ -185,6 +192,10 @@ window.setupMap = function() { 'Google Hybrid': views[4], 'Google Terrain': views[5] }, addLayers); + // Remove the hidden layer after layerChooser built, to avoid messing up ordering of layers + $.each(hiddenLayer, function(ind, layer){ + map.removeLayer(layer); + }); map.addControl(window.layerChooser); @@ -396,6 +407,11 @@ function boot() { window.setupBackButton(); // read here ONCE, so the URL is only evaluated one time after the // necessary data has been loaded. + urlPortalLL = getURLParam('pll'); + if(urlPortalLL) { + urlPortalLL = urlPortalLL.split(","); + urlPortalLL = [parseFloat(urlPortalLL[0]) || 0.0, parseFloat(urlPortalLL[1]) || 0.0]; + } urlPortal = getURLParam('pguid'); // load only once diff --git a/code/chat.js b/code/chat.js index df42dd2a..69b36691 100644 --- a/code/chat.js +++ b/code/chat.js @@ -1,678 +1,678 @@ -window.chat = function() {}; - -window.chat.handleTabCompletion = function() { - var el = $('#chatinput input'); - var curPos = el.get(0).selectionStart; - var text = el.val(); - var word = text.slice(0, curPos).replace(/.*\b([a-z0-9-_])/, '$1').toLowerCase(); - - var list = $('#chat > div:visible mark'); - list = list.map(function(ind, mark) { return $(mark).text(); } ); - list = uniqueArray(list); - - var nick = null; - for(var i = 0; i < list.length; i++) { - if(!list[i].toLowerCase().startsWith(word)) continue; - if(nick && nick !== list[i]) { - console.log('More than one nick matches, aborting. ('+list[i]+' vs '+nick+')'); - return; - } - nick = list[i]; - } - if(!nick) { - console.log('No matches for ' + word); - return; - } - - var posStart = curPos - word.length; - var newText = text.substring(0, posStart); - var atPresent = text.substring(posStart-1, posStart) === '@'; - newText += (atPresent ? '' : '@') + nick + ' '; - newText += text.substring(curPos); - el.val(newText); -} - -// -// clear management -// - - -window.chat._oldBBox = null; -window.chat.genPostData = function(isFaction, storageHash, getOlderMsgs) { - if(typeof isFaction !== 'boolean') throw('Need to know if public or faction chat.'); - - chat._localRangeCircle.setLatLng(map.getCenter()); - var b = map.getBounds().extend(chat._localRangeCircle.getBounds()); - var ne = b.getNorthEast(); - var sw = b.getSouthWest(); - - // round bounds in order to ignore rounding errors - var bbs = $.map([ne.lat, ne.lng, sw.lat, sw.lng], function(x) { return Math.round(x*1E4) }).join(); - if(chat._oldBBox && chat._oldBBox !== bbs) { - $('#chat > div').data('needsClearing', true); - console.log('Bounding Box changed, chat will be cleared (old: '+chat._oldBBox+' ; new: '+bbs+' )'); - // need to reset these flags now because clearing will only occur - // after the request is finished – i.e. there would be one almost - // useless request. - chat._faction.data = {}; - chat._faction.oldestTimestamp = -1; - chat._faction.newestTimestamp = -1; - - chat._public.data = {}; - chat._public.oldestTimestamp = -1; - chat._public.newestTimestamp = -1; - } - chat._oldBBox = bbs; - - var ne = b.getNorthEast(); - var sw = b.getSouthWest(); - var data = { - desiredNumItems: isFaction ? CHAT_FACTION_ITEMS : CHAT_PUBLIC_ITEMS , - minLatE6: Math.round(sw.lat*1E6), - minLngE6: Math.round(sw.lng*1E6), - maxLatE6: Math.round(ne.lat*1E6), - maxLngE6: Math.round(ne.lng*1E6), - minTimestampMs: -1, - maxTimestampMs: -1, - factionOnly: isFaction - } - - if(getOlderMsgs) { - // ask for older chat when scrolling up - data = $.extend(data, {maxTimestampMs: storageHash.oldestTimestamp}); - } else { - // ask for newer chat - var min = storageHash.newestTimestamp; - // the inital request will have both timestamp values set to -1, - // thus we receive the newest desiredNumItems. After that, we will - // only receive messages with a timestamp greater or equal to min - // above. - // After resuming from idle, there might be more new messages than - // desiredNumItems. So on the first request, we are not really up to - // date. We will eventually catch up, as long as there are less new - // messages than desiredNumItems per each refresh cycle. - // A proper solution would be to query until no more new results are - // returned. Another way would be to set desiredNumItems to a very - // large number so we really get all new messages since the last - // request. Setting desiredNumItems to -1 does unfortunately not - // work. - // Currently this edge case is not handled. Let’s see if this is a - // problem in crowded areas. - $.extend(data, {minTimestampMs: min}); - } - return data; -} - - - -// -// faction -// - -window.chat._requestFactionRunning = false; -window.chat.requestFaction = function(getOlderMsgs, isRetry) { - if(chat._requestFactionRunning && !isRetry) return; - if(isIdle()) return renderUpdateStatus(); - chat._requestFactionRunning = true; - - var d = chat.genPostData(true, chat._faction, getOlderMsgs); - var r = window.postAjax( - 'getPaginatedPlextsV2', - d, - chat.handleFaction, - isRetry - ? function() { window.chat._requestFactionRunning = false; } - : function() { window.chat.requestFaction(getOlderMsgs, true) } - ); - - requests.add(r); -} - - -window.chat._faction = {data:{}, oldestTimestamp:-1, newestTimestamp:-1}; -window.chat.handleFaction = function(data, textStatus, jqXHR) { - chat._requestFactionRunning = false; - - if(!data || !data.result) { - window.failedRequestCount++; - return console.warn('faction chat error. Waiting for next auto-refresh.'); - } - - if(data.result.length === 0) return; - - var old = chat._faction.oldestTimestamp; - chat.writeDataToHash(data, chat._faction, false); - var oldMsgsWereAdded = old !== chat._faction.oldestTimestamp; - - runHooks('factionChatDataAvailable', {raw: data, processed: chat._faction.data}); - - window.chat.renderFaction(oldMsgsWereAdded); - - if(data.result.length >= CHAT_FACTION_ITEMS) chat.needMoreMessages(); -} - -window.chat.renderFaction = function(oldMsgsWereAdded) { - chat.renderData(chat._faction.data, 'chatfaction', oldMsgsWereAdded); -} - - -// -// public -// - -window.chat._requestPublicRunning = false; -window.chat.requestPublic = function(getOlderMsgs, isRetry) { - if(chat._requestPublicRunning && !isRetry) return; - if(isIdle()) return renderUpdateStatus(); - chat._requestPublicRunning = true; - - var d = chat.genPostData(false, chat._public, getOlderMsgs); - var r = window.postAjax( - 'getPaginatedPlextsV2', - d, - chat.handlePublic, - isRetry - ? function() { window.chat._requestPublicRunning = false; } - : function() { window.chat.requestPublic(getOlderMsgs, true) } - ); - - requests.add(r); -} - -window.chat._public = {data:{}, oldestTimestamp:-1, newestTimestamp:-1}; -window.chat.handlePublic = function(data, textStatus, jqXHR) { - chat._requestPublicRunning = false; - - if(!data || !data.result) { - window.failedRequestCount++; - return console.warn('public chat error. Waiting for next auto-refresh.'); - } - - if(data.result.length === 0) return; - - var old = chat._public.oldestTimestamp; - chat.writeDataToHash(data, chat._public, true); - var oldMsgsWereAdded = old !== chat._public.oldestTimestamp; - - runHooks('publicChatDataAvailable', {raw: data, processed: chat._public.data}); - - switch(chat.getActive()) { - case 'public': window.chat.renderPublic(oldMsgsWereAdded); break; - case 'compact': window.chat.renderCompact(oldMsgsWereAdded); break; - case 'full': window.chat.renderFull(oldMsgsWereAdded); break; - } - - if(data.result.length >= CHAT_PUBLIC_ITEMS) chat.needMoreMessages(); -} - -window.chat.renderPublic = function(oldMsgsWereAdded) { - // only keep player data - var data = $.map(chat._public.data, function(entry) { - if(!entry[1]) return [entry]; - }); - chat.renderData(data, 'chatpublic', oldMsgsWereAdded); -} - -window.chat.renderCompact = function(oldMsgsWereAdded) { - var data = {}; - $.each(chat._public.data, function(guid, entry) { - // skip player msgs - if(!entry[1]) return true; - var pguid = entry[3]; - // ignore if player has newer data - if(data[pguid] && data[pguid][0] > entry[0]) return true; - data[pguid] = entry; - }); - // data keys are now player guids instead of message guids. However, - // it is all the same to renderData. - chat.renderData(data, 'chatcompact', oldMsgsWereAdded); -} - -window.chat.renderFull = function(oldMsgsWereAdded) { - // only keep automatically generated data - var data = $.map(chat._public.data, function(entry) { - if(entry[1]) return [entry]; - }); - chat.renderData(data, 'chatfull', oldMsgsWereAdded); -} - - -// -// common -// - -window.chat.nicknameClicked = function(event, nickname) { - var hookData = { event: event, nickname: nickname }; - - if (window.runHooks('nicknameClicked', hookData)) { - window.chat.addNickname('@' + nickname); - } -} - -window.chat.writeDataToHash = function(newData, storageHash, isPublicChannel) { - $.each(newData.result, function(ind, json) { - // avoid duplicates - if(json[0] in storageHash.data) return true; - - var isSecureMessage = false; - var msgToPlayer = false; - - var time = json[1]; - var team = json[2].plext.team === 'ALIENS' ? TEAM_ENL : TEAM_RES; - var auto = json[2].plext.plextType !== 'PLAYER_GENERATED'; - var systemNarrowcast = json[2].plext.plextType === 'SYSTEM_NARROWCAST'; - - //track oldest + newest timestamps - if (storageHash.oldestTimestamp === -1 || storageHash.oldestTimestamp > time) storageHash.oldestTimestamp = time; - if (storageHash.newestTimestamp === -1 || storageHash.newestTimestamp < time) storageHash.newestTimestamp = time; - - //remove "Your X on Y was destroyed by Z" from the faction channel - if (systemNarrowcast && !isPublicChannel) return true; - - var msg = '', nick = '', pguid; - $.each(json[2].plext.markup, function(ind, markup) { - switch(markup[0]) { - case 'SENDER': // user generated messages - nick = markup[1].plain.slice(0, -2); // cut “: ” at end - pguid = markup[1].guid; - break; - - case 'PLAYER': // automatically generated messages - pguid = markup[1].guid; - nick = markup[1].plain; - team = markup[1].team === 'ALIENS' ? TEAM_ENL : TEAM_RES; - if(ind > 0) msg += nick; // don’t repeat nick directly - break; - - case 'TEXT': - msg += $('
').text(markup[1].plain).html().autoLink(); - break; - - case 'AT_PLAYER': - var thisToPlayer = (markup[1].plain == ('@'+window.PLAYER.nickname)); - var spanClass = thisToPlayer ? "pl_nudge_me" : (markup[1].team + " pl_nudge_player"); - var atPlayerName = markup[1].plain.replace(/^@/, ""); - msg += $('
').html($('') - .attr('class', spanClass) - .attr('onclick',"window.chat.nicknameClicked(event, '"+atPlayerName+"')") - .text(markup[1].plain)).html(); - msgToPlayer = msgToPlayer || thisToPlayer; - break; - - case 'PORTAL': - var latlng = [markup[1].latE6/1E6, markup[1].lngE6/1E6]; - var perma = '/intel?latE6='+markup[1].latE6+'&lngE6='+markup[1].lngE6+'&z=17&pguid='+markup[1].guid; - var js = 'window.zoomToAndShowPortal(\''+markup[1].guid+'\', ['+latlng[0]+', '+latlng[1]+']);return false'; - - msg += '' - + window.chat.getChatPortalName(markup[1]) - + ''; - break; - - case 'SECURE': - //NOTE: we won't add the '[secure]' string here - it'll be handled below instead - isSecureMessage = true; - break; - - default: - //handle unknown types by outputting the plain text version, marked with it's type - msg += $('
').text(markup[0]+':<'+markup[1].plain+'>').html(); - break; - } - }); - - - //skip secure messages on the public channel - if (isPublicChannel && isSecureMessage) return true; - - //skip public messages (e.g. @player mentions) on the secure channel - if ((!isPublicChannel) && (!isSecureMessage)) return true; - - - //NOTE: these two are currently redundant with the above two tests - but code can change... - //from the server, private channel messages are flagged with a SECURE string '[secure] ', and appear in - //both the public and private channels - //we don't include this '[secure]' text above, as it's redundant in the faction-only channel - //let's add it here though if we have a secure message in the public channel, or the reverse if a non-secure in the faction one - if (isPublicChannel && isSecureMessage) msg = '[secure] ' + msg; - //and, add the reverse - a 'public' marker to messages in the private channel - if ((!isPublicChannel) && (!isSecureMessage)) msg = '[public] ' + msg; - - - // format: timestamp, autogenerated, HTML message, player guid - storageHash.data[json[0]] = [json[1], auto, chat.renderMsg(msg, nick, time, team, msgToPlayer, systemNarrowcast), pguid]; - - window.setPlayerName(pguid, nick); // free nick name resolves - }); -} - -// Override portal names that are used over and over, such as 'US Post Office' -window.chat.getChatPortalName = function(markup) { - var name = markup.name; - if(name === 'US Post Office') { - var address = markup.address.split(','); - name = 'USPS: ' + address[0]; - } - return name; -} - -// renders data from the data-hash to the element defined by the given -// ID. Set 3rd argument to true if it is likely that old data has been -// added. Latter is only required for scrolling. -window.chat.renderData = function(data, element, likelyWereOldMsgs) { - var elm = $('#'+element); - if(elm.is(':hidden')) return; - - // discard guids and sort old to new - var vals = $.map(data, function(v, k) { return [v]; }); - vals = vals.sort(function(a, b) { return a[0]-b[0]; }); - - // render to string with date separators inserted - var msgs = ''; - var prevTime = null; - $.each(vals, function(ind, msg) { - var nextTime = new Date(msg[0]).toLocaleDateString(); - if(prevTime && prevTime !== nextTime) - msgs += chat.renderDivider(nextTime); - msgs += msg[2]; - prevTime = nextTime; - }); - - var scrollBefore = scrollBottom(elm); - elm.html('' + msgs + '
'); - chat.keepScrollPosition(elm, scrollBefore, likelyWereOldMsgs); -} - - -window.chat.renderDivider = function(text) { - var d = ' ──────────────────────────────────────────────────────────────────────────'; - return '─ ' + text + d + ''; -} - - -window.chat.renderMsg = function(msg, nick, time, team, msgToPlayer, systemNarrowcast) { - var ta = unixTimeToHHmm(time); - var tb = unixTimeToString(time, true); - // help cursor via “#chat time” - var t = ''; - if ( msgToPlayer ) - { - t = '
' + t + '
'; - } - if (systemNarrowcast) - { - msg = '
' + msg + '
'; - } - var color = COLORS[team]; - if (nick === window.PLAYER.nickname) color = '#fd6'; //highlight things said/done by the player in a unique colour (similar to @player mentions from others in the chat text itself) - var s = 'style="cursor:pointer; color:'+color+'"'; - var title = nick.length >= 8 ? 'title="'+nick+'" class="help"' : ''; - var i = ['<', '>']; - return ''+t+''+i[0]+''+ nick+''+i[1]+''+msg+''; -} - -window.chat.addNickname= function(nick){ - var c = document.getElementById("chattext"); - c.value = [c.value.trim(), nick].join(" ").trim() + " "; - c.focus() -} - - - - -window.chat.getActive = function() { - return $('#chatcontrols .active').text(); -} - - -window.chat.toggle = function() { - var c = $('#chat, #chatcontrols'); - if(c.hasClass('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').html(''); - c.addClass('expand'); - $('.leaflet-control').css('margin-left', '720px'); - chat.needMoreMessages(); - } -} - - -window.chat.request = function() { - console.log('refreshing chat'); - chat.requestFaction(false); - chat.requestPublic(false); -} - - -// checks if there are enough messages in the selected chat tab and -// loads more if not. -window.chat.needMoreMessages = function() { - var activeTab = chat.getActive(); - if(activeTab === 'debug') return; - - var activeChat = $('#chat > :visible'); - if(activeChat.length === 0) return; - - var hasScrollbar = scrollBottom(activeChat) !== 0 || activeChat.scrollTop() !== 0; - var nearTop = activeChat.scrollTop() <= CHAT_REQUEST_SCROLL_TOP; - if(hasScrollbar && !nearTop) return; - - console.log('No scrollbar or near top in active chat. Requesting more data.'); - - if(activeTab === 'faction') - chat.requestFaction(true); - else - chat.requestPublic(true); -} - - -window.chat.chooser = function(event) { - var t = $(event.target); - var tt = t.text(); - - var mark = $('#chatinput mark'); - var input = $('#chatinput input'); - - $('#chatcontrols .active').removeClass('active'); - t.addClass('active'); - - $('#chat > div').hide(); - - var elm; - - switch(tt) { - case 'faction': - input.css('color', ''); - mark.css('color', ''); - mark.text('tell faction:'); - break; - - case 'public': - input.css('cssText', 'color: #f66 !important'); - mark.css('cssText', 'color: #f66 !important'); - mark.text('broadcast:'); - break; - - case 'compact': - case 'full': - mark.css('cssText', 'color: #bbb !important'); - input.css('cssText', 'color: #bbb !important'); - mark.text('tell Jarvis:'); - break; - - default: - throw('chat.chooser was asked to handle unknown button: ' + tt); - } - - var elm = $('#chat' + tt); - elm.show(); - eval('chat.render' + tt.capitalize() + '(false);'); - if(elm.data('needsScrollTop')) { - elm.data('ignoreNextScroll', true); - elm.scrollTop(elm.data('needsScrollTop')); - elm.data('needsScrollTop', null); - } - - chat.needMoreMessages(); -} - - -// contains the logic to keep the correct scroll position. -window.chat.keepScrollPosition = function(box, scrollBefore, isOldMsgs) { - // If scrolled down completely, keep it that way so new messages can - // be seen easily. If scrolled up, only need to fix scroll position - // when old messages are added. New messages added at the bottom don’t - // change the view and enabling this would make the chat scroll down - // for every added message, even if the user wants to read old stuff. - - if(box.is(':hidden') && !isOldMsgs) { - box.data('needsScrollTop', 99999999); - return; - } - - if(scrollBefore === 0 || isOldMsgs) { - box.data('ignoreNextScroll', true); - box.scrollTop(box.scrollTop() + (scrollBottom(box)-scrollBefore)); - } -} - - - - -// -// setup -// - -window.chat.setup = function() { - window.chat._localRangeCircle = L.circle(map.getCenter(), CHAT_MIN_RANGE*1000); - - $('#chatcontrols, #chat, #chatinput').show(); - - $('#chatcontrols a:first').click(window.chat.toggle); - $('#chatcontrols a').each(function(ind, elm) { - if($.inArray($(elm).text(), ['full', 'compact', 'public', 'faction']) !== -1) - $(elm).click(window.chat.chooser); - }); - - - $('#chatinput').click(function() { - $('#chatinput input').focus(); - }); - - window.chat.setupTime(); - window.chat.setupPosting(); - - $('#chatfaction').scroll(function() { - var t = $(this); - if(t.data('ignoreNextScroll')) return t.data('ignoreNextScroll', false); - if(t.scrollTop() < CHAT_REQUEST_SCROLL_TOP) chat.requestFaction(true); - if(scrollBottom(t) === 0) chat.requestFaction(false); - }); - - $('#chatpublic, #chatfull, #chatcompact').scroll(function() { - var t = $(this); - if(t.data('ignoreNextScroll')) return t.data('ignoreNextScroll', false); - if(t.scrollTop() < CHAT_REQUEST_SCROLL_TOP) chat.requestPublic(true); - if(scrollBottom(t) === 0) chat.requestPublic(false); - }); - - chat.request(); - window.addResumeFunction(chat.request); - window.requests.addRefreshFunction(chat.request); - - var cls = PLAYER.team === 'ALIENS' ? 'enl' : 'res'; - $('#chatinput mark').addClass(cls); - - $(window).on('click', '.nickname', function(event) { - window.chat.nicknameClicked(event, $(this).text()); - }); -} - - -window.chat.setupTime = function() { - var inputTime = $('#chatinput time'); - var updateTime = function() { - if(window.isIdle()) return; - var d = new Date(); - var h = d.getHours() + ''; if(h.length === 1) h = '0' + h; - var m = d.getMinutes() + ''; if(m.length === 1) m = '0' + m; - inputTime.text(h+':'+m); - // update ON the minute (1ms after) - setTimeout(updateTime, (60 - d.getSeconds()) * 1000 + 1); - }; - updateTime(); - window.addResumeFunction(updateTime); -} - - -// -// posting -// - - -window.chat.setupPosting = function() { - if (!isSmartphone()) { - $('#chatinput input').keydown(function(event) { - try { - var kc = (event.keyCode ? event.keyCode : event.which); - if(kc === 13) { // enter - chat.postMsg(); - event.preventDefault(); - } else if (kc === 9) { // tab - event.preventDefault(); - window.chat.handleTabCompletion(); - } - } catch(error) { - console.log(error); - debug.printStackTrace(); - } - }); - } - - $('#chatinput').submit(function(event) { - event.preventDefault(); - chat.postMsg(); - }); -} - - -window.chat.postMsg = function() { - var c = chat.getActive(); - if(c === 'full' || c === 'compact') - return alert('Jarvis: A strange game. The only winning move is not to play. How about a nice game of chess?'); - - var msg = $.trim($('#chatinput input').val()); - if(!msg || msg === '') return; - - if(c === 'debug') return new Function (msg)(); - - var publik = c === 'public'; - var latlng = map.getCenter(); - - var data = {message: msg, - latE6: Math.round(latlng.lat*1E6), - lngE6: Math.round(latlng.lng*1E6), - factionOnly: !publik}; - - var errMsg = 'Your message could not be delivered. You can copy&' + - 'paste it here and try again if you want:\n\n' + msg; - - window.postAjax('sendPlext', data, - function(response) { - if(response.error) alert(errMsg); - if(publik) chat.requestPublic(false); else chat.requestFaction(false); }, - function() { - alert(errMsg); - } - ); - - $('#chatinput input').val(''); -} +window.chat = function() {}; + +window.chat.handleTabCompletion = function() { + var el = $('#chatinput input'); + var curPos = el.get(0).selectionStart; + var text = el.val(); + var word = text.slice(0, curPos).replace(/.*\b([a-z0-9-_])/, '$1').toLowerCase(); + + var list = $('#chat > div:visible mark'); + list = list.map(function(ind, mark) { return $(mark).text(); } ); + list = uniqueArray(list); + + var nick = null; + for(var i = 0; i < list.length; i++) { + if(!list[i].toLowerCase().startsWith(word)) continue; + if(nick && nick !== list[i]) { + console.log('More than one nick matches, aborting. ('+list[i]+' vs '+nick+')'); + return; + } + nick = list[i]; + } + if(!nick) { + console.log('No matches for ' + word); + return; + } + + var posStart = curPos - word.length; + var newText = text.substring(0, posStart); + var atPresent = text.substring(posStart-1, posStart) === '@'; + newText += (atPresent ? '' : '@') + nick + ' '; + newText += text.substring(curPos); + el.val(newText); +} + +// +// clear management +// + + +window.chat._oldBBox = null; +window.chat.genPostData = function(isFaction, storageHash, getOlderMsgs) { + if(typeof isFaction !== 'boolean') throw('Need to know if public or faction chat.'); + + chat._localRangeCircle.setLatLng(map.getCenter()); + var b = map.getBounds().extend(chat._localRangeCircle.getBounds()); + var ne = b.getNorthEast(); + var sw = b.getSouthWest(); + + // round bounds in order to ignore rounding errors + var bbs = $.map([ne.lat, ne.lng, sw.lat, sw.lng], function(x) { return Math.round(x*1E4) }).join(); + if(chat._oldBBox && chat._oldBBox !== bbs) { + $('#chat > div').data('needsClearing', true); + console.log('Bounding Box changed, chat will be cleared (old: '+chat._oldBBox+' ; new: '+bbs+' )'); + // need to reset these flags now because clearing will only occur + // after the request is finished – i.e. there would be one almost + // useless request. + chat._faction.data = {}; + chat._faction.oldestTimestamp = -1; + chat._faction.newestTimestamp = -1; + + chat._public.data = {}; + chat._public.oldestTimestamp = -1; + chat._public.newestTimestamp = -1; + } + chat._oldBBox = bbs; + + var ne = b.getNorthEast(); + var sw = b.getSouthWest(); + var data = { + desiredNumItems: isFaction ? CHAT_FACTION_ITEMS : CHAT_PUBLIC_ITEMS , + minLatE6: Math.round(sw.lat*1E6), + minLngE6: Math.round(sw.lng*1E6), + maxLatE6: Math.round(ne.lat*1E6), + maxLngE6: Math.round(ne.lng*1E6), + minTimestampMs: -1, + maxTimestampMs: -1, + factionOnly: isFaction + } + + if(getOlderMsgs) { + // ask for older chat when scrolling up + data = $.extend(data, {maxTimestampMs: storageHash.oldestTimestamp}); + } else { + // ask for newer chat + var min = storageHash.newestTimestamp; + // the inital request will have both timestamp values set to -1, + // thus we receive the newest desiredNumItems. After that, we will + // only receive messages with a timestamp greater or equal to min + // above. + // After resuming from idle, there might be more new messages than + // desiredNumItems. So on the first request, we are not really up to + // date. We will eventually catch up, as long as there are less new + // messages than desiredNumItems per each refresh cycle. + // A proper solution would be to query until no more new results are + // returned. Another way would be to set desiredNumItems to a very + // large number so we really get all new messages since the last + // request. Setting desiredNumItems to -1 does unfortunately not + // work. + // Currently this edge case is not handled. Let’s see if this is a + // problem in crowded areas. + $.extend(data, {minTimestampMs: min}); + } + return data; +} + + + +// +// faction +// + +window.chat._requestFactionRunning = false; +window.chat.requestFaction = function(getOlderMsgs, isRetry) { + if(chat._requestFactionRunning && !isRetry) return; + if(isIdle()) return renderUpdateStatus(); + chat._requestFactionRunning = true; + + var d = chat.genPostData(true, chat._faction, getOlderMsgs); + var r = window.postAjax( + 'getPaginatedPlextsV2', + d, + chat.handleFaction, + isRetry + ? function() { window.chat._requestFactionRunning = false; } + : function() { window.chat.requestFaction(getOlderMsgs, true) } + ); + + requests.add(r); +} + + +window.chat._faction = {data:{}, oldestTimestamp:-1, newestTimestamp:-1}; +window.chat.handleFaction = function(data, textStatus, jqXHR) { + chat._requestFactionRunning = false; + + if(!data || !data.result) { + window.failedRequestCount++; + return console.warn('faction chat error. Waiting for next auto-refresh.'); + } + + if(data.result.length === 0) return; + + var old = chat._faction.oldestTimestamp; + chat.writeDataToHash(data, chat._faction, false); + var oldMsgsWereAdded = old !== chat._faction.oldestTimestamp; + + runHooks('factionChatDataAvailable', {raw: data, processed: chat._faction.data}); + + window.chat.renderFaction(oldMsgsWereAdded); + + if(data.result.length >= CHAT_FACTION_ITEMS) chat.needMoreMessages(); +} + +window.chat.renderFaction = function(oldMsgsWereAdded) { + chat.renderData(chat._faction.data, 'chatfaction', oldMsgsWereAdded); +} + + +// +// public +// + +window.chat._requestPublicRunning = false; +window.chat.requestPublic = function(getOlderMsgs, isRetry) { + if(chat._requestPublicRunning && !isRetry) return; + if(isIdle()) return renderUpdateStatus(); + chat._requestPublicRunning = true; + + var d = chat.genPostData(false, chat._public, getOlderMsgs); + var r = window.postAjax( + 'getPaginatedPlextsV2', + d, + chat.handlePublic, + isRetry + ? function() { window.chat._requestPublicRunning = false; } + : function() { window.chat.requestPublic(getOlderMsgs, true) } + ); + + requests.add(r); +} + +window.chat._public = {data:{}, oldestTimestamp:-1, newestTimestamp:-1}; +window.chat.handlePublic = function(data, textStatus, jqXHR) { + chat._requestPublicRunning = false; + + if(!data || !data.result) { + window.failedRequestCount++; + return console.warn('public chat error. Waiting for next auto-refresh.'); + } + + if(data.result.length === 0) return; + + var old = chat._public.oldestTimestamp; + chat.writeDataToHash(data, chat._public, true); + var oldMsgsWereAdded = old !== chat._public.oldestTimestamp; + + runHooks('publicChatDataAvailable', {raw: data, processed: chat._public.data}); + + switch(chat.getActive()) { + case 'public': window.chat.renderPublic(oldMsgsWereAdded); break; + case 'compact': window.chat.renderCompact(oldMsgsWereAdded); break; + case 'full': window.chat.renderFull(oldMsgsWereAdded); break; + } + + if(data.result.length >= CHAT_PUBLIC_ITEMS) chat.needMoreMessages(); +} + +window.chat.renderPublic = function(oldMsgsWereAdded) { + // only keep player data + var data = $.map(chat._public.data, function(entry) { + if(!entry[1]) return [entry]; + }); + chat.renderData(data, 'chatpublic', oldMsgsWereAdded); +} + +window.chat.renderCompact = function(oldMsgsWereAdded) { + var data = {}; + $.each(chat._public.data, function(guid, entry) { + // skip player msgs + if(!entry[1]) return true; + var pguid = entry[3]; + // ignore if player has newer data + if(data[pguid] && data[pguid][0] > entry[0]) return true; + data[pguid] = entry; + }); + // data keys are now player guids instead of message guids. However, + // it is all the same to renderData. + chat.renderData(data, 'chatcompact', oldMsgsWereAdded); +} + +window.chat.renderFull = function(oldMsgsWereAdded) { + // only keep automatically generated data + var data = $.map(chat._public.data, function(entry) { + if(entry[1]) return [entry]; + }); + chat.renderData(data, 'chatfull', oldMsgsWereAdded); +} + + +// +// common +// + +window.chat.nicknameClicked = function(event, nickname) { + var hookData = { event: event, nickname: nickname }; + + if (window.runHooks('nicknameClicked', hookData)) { + window.chat.addNickname('@' + nickname); + } +} + +window.chat.writeDataToHash = function(newData, storageHash, isPublicChannel) { + $.each(newData.result, function(ind, json) { + // avoid duplicates + if(json[0] in storageHash.data) return true; + + var isSecureMessage = false; + var msgToPlayer = false; + + var time = json[1]; + var team = json[2].plext.team === 'ALIENS' ? TEAM_ENL : TEAM_RES; + var auto = json[2].plext.plextType !== 'PLAYER_GENERATED'; + var systemNarrowcast = json[2].plext.plextType === 'SYSTEM_NARROWCAST'; + + //track oldest + newest timestamps + if (storageHash.oldestTimestamp === -1 || storageHash.oldestTimestamp > time) storageHash.oldestTimestamp = time; + if (storageHash.newestTimestamp === -1 || storageHash.newestTimestamp < time) storageHash.newestTimestamp = time; + + //remove "Your X on Y was destroyed by Z" from the faction channel + if (systemNarrowcast && !isPublicChannel) return true; + + var msg = '', nick = '', pguid; + $.each(json[2].plext.markup, function(ind, markup) { + switch(markup[0]) { + case 'SENDER': // user generated messages + nick = markup[1].plain.slice(0, -2); // cut “: ” at end + pguid = markup[1].guid; + break; + + case 'PLAYER': // automatically generated messages + pguid = markup[1].guid; + nick = markup[1].plain; + team = markup[1].team === 'ALIENS' ? TEAM_ENL : TEAM_RES; + if(ind > 0) msg += nick; // don’t repeat nick directly + break; + + case 'TEXT': + msg += $('
').text(markup[1].plain).html().autoLink(); + break; + + case 'AT_PLAYER': + var thisToPlayer = (markup[1].plain == ('@'+window.PLAYER.nickname)); + var spanClass = thisToPlayer ? "pl_nudge_me" : (markup[1].team + " pl_nudge_player"); + var atPlayerName = markup[1].plain.replace(/^@/, ""); + msg += $('
').html($('') + .attr('class', spanClass) + .attr('onclick',"window.chat.nicknameClicked(event, '"+atPlayerName+"')") + .text(markup[1].plain)).html(); + msgToPlayer = msgToPlayer || thisToPlayer; + break; + + case 'PORTAL': + var latlng = [markup[1].latE6/1E6, markup[1].lngE6/1E6]; + var perma = '/intel?ll='+latlng[0]+','+latlng[1]+'&z=17&pll='+latlng[0]+','+latlng[1]; + var js = 'window.zoomToAndShowPortal(\''+markup[1].guid+'\', ['+latlng[0]+', '+latlng[1]+']);return false'; + + msg += '' + + window.chat.getChatPortalName(markup[1]) + + ''; + break; + + case 'SECURE': + //NOTE: we won't add the '[secure]' string here - it'll be handled below instead + isSecureMessage = true; + break; + + default: + //handle unknown types by outputting the plain text version, marked with it's type + msg += $('
').text(markup[0]+':<'+markup[1].plain+'>').html(); + break; + } + }); + + + //skip secure messages on the public channel + if (isPublicChannel && isSecureMessage) return true; + + //skip public messages (e.g. @player mentions) on the secure channel + if ((!isPublicChannel) && (!isSecureMessage)) return true; + + + //NOTE: these two are currently redundant with the above two tests - but code can change... + //from the server, private channel messages are flagged with a SECURE string '[secure] ', and appear in + //both the public and private channels + //we don't include this '[secure]' text above, as it's redundant in the faction-only channel + //let's add it here though if we have a secure message in the public channel, or the reverse if a non-secure in the faction one + if (isPublicChannel && isSecureMessage) msg = '[secure] ' + msg; + //and, add the reverse - a 'public' marker to messages in the private channel + if ((!isPublicChannel) && (!isSecureMessage)) msg = '[public] ' + msg; + + + // format: timestamp, autogenerated, HTML message, player guid + storageHash.data[json[0]] = [json[1], auto, chat.renderMsg(msg, nick, time, team, msgToPlayer, systemNarrowcast), pguid]; + + window.setPlayerName(pguid, nick); // free nick name resolves + }); +} + +// Override portal names that are used over and over, such as 'US Post Office' +window.chat.getChatPortalName = function(markup) { + var name = markup.name; + if(name === 'US Post Office') { + var address = markup.address.split(','); + name = 'USPS: ' + address[0]; + } + return name; +} + +// renders data from the data-hash to the element defined by the given +// ID. Set 3rd argument to true if it is likely that old data has been +// added. Latter is only required for scrolling. +window.chat.renderData = function(data, element, likelyWereOldMsgs) { + var elm = $('#'+element); + if(elm.is(':hidden')) return; + + // discard guids and sort old to new + var vals = $.map(data, function(v, k) { return [v]; }); + vals = vals.sort(function(a, b) { return a[0]-b[0]; }); + + // render to string with date separators inserted + var msgs = ''; + var prevTime = null; + $.each(vals, function(ind, msg) { + var nextTime = new Date(msg[0]).toLocaleDateString(); + if(prevTime && prevTime !== nextTime) + msgs += chat.renderDivider(nextTime); + msgs += msg[2]; + prevTime = nextTime; + }); + + var scrollBefore = scrollBottom(elm); + elm.html('' + msgs + '
'); + chat.keepScrollPosition(elm, scrollBefore, likelyWereOldMsgs); +} + + +window.chat.renderDivider = function(text) { + var d = ' ──────────────────────────────────────────────────────────────────────────'; + return '─ ' + text + d + ''; +} + + +window.chat.renderMsg = function(msg, nick, time, team, msgToPlayer, systemNarrowcast) { + var ta = unixTimeToHHmm(time); + var tb = unixTimeToString(time, true); + // help cursor via “#chat time” + var t = ''; + if ( msgToPlayer ) + { + t = '
' + t + '
'; + } + if (systemNarrowcast) + { + msg = '
' + msg + '
'; + } + var color = COLORS[team]; + if (nick === window.PLAYER.nickname) color = '#fd6'; //highlight things said/done by the player in a unique colour (similar to @player mentions from others in the chat text itself) + var s = 'style="cursor:pointer; color:'+color+'"'; + var title = nick.length >= 8 ? 'title="'+nick+'" class="help"' : ''; + var i = ['<', '>']; + return ''+t+''+i[0]+''+ nick+''+i[1]+''+msg+''; +} + +window.chat.addNickname= function(nick){ + var c = document.getElementById("chattext"); + c.value = [c.value.trim(), nick].join(" ").trim() + " "; + c.focus() +} + + + + +window.chat.getActive = function() { + return $('#chatcontrols .active').text(); +} + + +window.chat.toggle = function() { + var c = $('#chat, #chatcontrols'); + if(c.hasClass('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').html(''); + c.addClass('expand'); + $('.leaflet-control').css('margin-left', '720px'); + chat.needMoreMessages(); + } +} + + +window.chat.request = function() { + console.log('refreshing chat'); + chat.requestFaction(false); + chat.requestPublic(false); +} + + +// checks if there are enough messages in the selected chat tab and +// loads more if not. +window.chat.needMoreMessages = function() { + var activeTab = chat.getActive(); + if(activeTab === 'debug') return; + + var activeChat = $('#chat > :visible'); + if(activeChat.length === 0) return; + + var hasScrollbar = scrollBottom(activeChat) !== 0 || activeChat.scrollTop() !== 0; + var nearTop = activeChat.scrollTop() <= CHAT_REQUEST_SCROLL_TOP; + if(hasScrollbar && !nearTop) return; + + console.log('No scrollbar or near top in active chat. Requesting more data.'); + + if(activeTab === 'faction') + chat.requestFaction(true); + else + chat.requestPublic(true); +} + + +window.chat.chooser = function(event) { + var t = $(event.target); + var tt = t.text(); + + var mark = $('#chatinput mark'); + var input = $('#chatinput input'); + + $('#chatcontrols .active').removeClass('active'); + t.addClass('active'); + + $('#chat > div').hide(); + + var elm; + + switch(tt) { + case 'faction': + input.css('color', ''); + mark.css('color', ''); + mark.text('tell faction:'); + break; + + case 'public': + input.css('cssText', 'color: #f66 !important'); + mark.css('cssText', 'color: #f66 !important'); + mark.text('broadcast:'); + break; + + case 'compact': + case 'full': + mark.css('cssText', 'color: #bbb !important'); + input.css('cssText', 'color: #bbb !important'); + mark.text('tell Jarvis:'); + break; + + default: + throw('chat.chooser was asked to handle unknown button: ' + tt); + } + + var elm = $('#chat' + tt); + elm.show(); + eval('chat.render' + tt.capitalize() + '(false);'); + if(elm.data('needsScrollTop')) { + elm.data('ignoreNextScroll', true); + elm.scrollTop(elm.data('needsScrollTop')); + elm.data('needsScrollTop', null); + } + + chat.needMoreMessages(); +} + + +// contains the logic to keep the correct scroll position. +window.chat.keepScrollPosition = function(box, scrollBefore, isOldMsgs) { + // If scrolled down completely, keep it that way so new messages can + // be seen easily. If scrolled up, only need to fix scroll position + // when old messages are added. New messages added at the bottom don’t + // change the view and enabling this would make the chat scroll down + // for every added message, even if the user wants to read old stuff. + + if(box.is(':hidden') && !isOldMsgs) { + box.data('needsScrollTop', 99999999); + return; + } + + if(scrollBefore === 0 || isOldMsgs) { + box.data('ignoreNextScroll', true); + box.scrollTop(box.scrollTop() + (scrollBottom(box)-scrollBefore)); + } +} + + + + +// +// setup +// + +window.chat.setup = function() { + window.chat._localRangeCircle = L.circle(map.getCenter(), CHAT_MIN_RANGE*1000); + + $('#chatcontrols, #chat, #chatinput').show(); + + $('#chatcontrols a:first').click(window.chat.toggle); + $('#chatcontrols a').each(function(ind, elm) { + if($.inArray($(elm).text(), ['full', 'compact', 'public', 'faction']) !== -1) + $(elm).click(window.chat.chooser); + }); + + + $('#chatinput').click(function() { + $('#chatinput input').focus(); + }); + + window.chat.setupTime(); + window.chat.setupPosting(); + + $('#chatfaction').scroll(function() { + var t = $(this); + if(t.data('ignoreNextScroll')) return t.data('ignoreNextScroll', false); + if(t.scrollTop() < CHAT_REQUEST_SCROLL_TOP) chat.requestFaction(true); + if(scrollBottom(t) === 0) chat.requestFaction(false); + }); + + $('#chatpublic, #chatfull, #chatcompact').scroll(function() { + var t = $(this); + if(t.data('ignoreNextScroll')) return t.data('ignoreNextScroll', false); + if(t.scrollTop() < CHAT_REQUEST_SCROLL_TOP) chat.requestPublic(true); + if(scrollBottom(t) === 0) chat.requestPublic(false); + }); + + chat.request(); + window.addResumeFunction(chat.request); + window.requests.addRefreshFunction(chat.request); + + var cls = PLAYER.team === 'ALIENS' ? 'enl' : 'res'; + $('#chatinput mark').addClass(cls); + + $(window).on('click', '.nickname', function(event) { + window.chat.nicknameClicked(event, $(this).text()); + }); +} + + +window.chat.setupTime = function() { + var inputTime = $('#chatinput time'); + var updateTime = function() { + if(window.isIdle()) return; + var d = new Date(); + var h = d.getHours() + ''; if(h.length === 1) h = '0' + h; + var m = d.getMinutes() + ''; if(m.length === 1) m = '0' + m; + inputTime.text(h+':'+m); + // update ON the minute (1ms after) + setTimeout(updateTime, (60 - d.getSeconds()) * 1000 + 1); + }; + updateTime(); + window.addResumeFunction(updateTime); +} + + +// +// posting +// + + +window.chat.setupPosting = function() { + if (!isSmartphone()) { + $('#chatinput input').keydown(function(event) { + try { + var kc = (event.keyCode ? event.keyCode : event.which); + if(kc === 13) { // enter + chat.postMsg(); + event.preventDefault(); + } else if (kc === 9) { // tab + event.preventDefault(); + window.chat.handleTabCompletion(); + } + } catch(error) { + console.log(error); + debug.printStackTrace(); + } + }); + } + + $('#chatinput').submit(function(event) { + event.preventDefault(); + chat.postMsg(); + }); +} + + +window.chat.postMsg = function() { + var c = chat.getActive(); + if(c === 'full' || c === 'compact') + return alert('Jarvis: A strange game. The only winning move is not to play. How about a nice game of chess?'); + + var msg = $.trim($('#chatinput input').val()); + if(!msg || msg === '') return; + + if(c === 'debug') return new Function (msg)(); + + var publik = c === 'public'; + var latlng = map.getCenter(); + + var data = {message: msg, + latE6: Math.round(latlng.lat*1E6), + lngE6: Math.round(latlng.lng*1E6), + factionOnly: !publik}; + + var errMsg = 'Your message could not be delivered. You can copy&' + + 'paste it here and try again if you want:\n\n' + msg; + + window.postAjax('sendPlext', data, + function(response) { + if(response.error) alert(errMsg); + if(publik) chat.requestPublic(false); else chat.requestFaction(false); }, + function() { + alert(errMsg); + } + ); + + $('#chatinput input').val(''); +} diff --git a/code/map_data.js b/code/map_data.js index 419534f0..2c956a95 100644 --- a/code/map_data.js +++ b/code/map_data.js @@ -99,6 +99,7 @@ window.handleDataResponse = function(data, textStatus, jqXHR) { if(!window.getPaddedBounds().contains(latlng) && selectedPortal !== ent[0] && urlPortal !== ent[0] + && !(urlPortalLL && urlPortalLL[0] === latlng[0] && urlPortalLL[1] === latlng[1]) ) return; if('imageByUrl' in ent[2] && 'imageUrl' in ent[2].imageByUrl) { @@ -175,11 +176,15 @@ window.handlePortalsRender = function(portals) { runHooks('portalDataLoaded', {portals : portals}); $.each(portals, function(ind, portal) { //~ if(selectedPortal === portal[0]) portalUpdateAvailable = true; - if(urlPortal && portal[0] === urlPortal) portalInUrlAvailable = true; + if(urlPortalLL && urlPortalLL[0] === portal[2].locationE6.latE6/1E6 && urlPortalLL[1] === portal[2].locationE6.lngE6/1E6) { + urlPortal = portal[0]; + portalInUrlAvailable = true; + urlPortalLL = null; + } if(window.portals[portal[0]]) { highlightPortal(window.portals[portal[0]]); } - renderPortal(portal); + renderPortal(portal); }); // restore selected portal if still available diff --git a/code/portal_detail_display.js b/code/portal_detail_display.js index 636230ed..205b80c5 100644 --- a/code/portal_detail_display.js +++ b/code/portal_detail_display.js @@ -45,11 +45,11 @@ window.renderPortalDetails = function(guid) { setPortalIndicators(d); var img = d.imageByUrl.imageUrl; - var lat = d.locationE6.latE6; - var lng = d.locationE6.lngE6; - var perma = '/intel?latE6='+lat+'&lngE6='+lng+'&z=17&pguid='+guid; + var lat = d.locationE6.latE6/1E6; + var lng = d.locationE6.lngE6/1E6; + var perma = '/intel?ll='+lat+','+lng+'&z=17&pll='+lat+','+lng; var imgTitle = 'title="'+getPortalDescriptionFromDetails(d)+'\n\nClick to show full image."'; - var poslinks = 'window.showPortalPosLinks('+lat/1E6+','+lng/1E6+',\'' + d.portalV2.descriptiveText.TITLE + '\')'; + var poslinks = 'window.showPortalPosLinks('+lat+','+lng+',\'' + d.portalV2.descriptiveText.TITLE + '\')'; var postcard = 'Send in a postcard. Will put it online after receiving. Address:\\n\\nStefan Breunig\\nINF 305 – R045\\n69120 Heidelberg\\nGermany'; $('#portaldetails') diff --git a/code/utils_misc.js b/code/utils_misc.js index e4d0f5c5..dede2f01 100644 --- a/code/utils_misc.js +++ b/code/utils_misc.js @@ -288,9 +288,9 @@ window.prettyEnergy = function(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 lat = Math.round(c.lat*1E6)/1E6; + var lng = Math.round(c.lng*1E6)/1E6; + var qry = 'll='+lat+','+lng+'&z=' + map.getZoom(); $(elm).attr('href', '/intel?' + qry); }