// MAP DATA ////////////////////////////////////////////////////////// // these functions handle how and which entities are displayed on the // map. They also keep them up to date, unless interrupted by user // action. // requests map data for current viewport. For details on how this // works, refer to the description in “MAP DATA REQUEST CALCULATORS” window.requestData = function() { console.log('refreshing data'); requests.abort(); cleanUp(); var magic = convertCenterLat(map.getCenter().lat); var R = calculateR(magic); var bounds = map.getBounds(); // convert to point values topRight = convertLatLngToPoint(bounds.getNorthEast(), magic, R); bottomLeft = convertLatLngToPoint(bounds.getSouthWest() , magic, R); // how many quadrants intersect the current view? quadsX = Math.abs(bottomLeft.x - topRight.x); quadsY = Math.abs(bottomLeft.y - topRight.y); // will group requests by second-last quad-key quadrant tiles = {}; // walk in x-direction, starts right goes left for(var i = 0; i <= quadsX; i++) { var x = Math.abs(topRight.x - i); var qk = pointToQuadKey(x, topRight.y); var bnds = convertPointToLatLng(x, topRight.y, magic, R); if(!tiles[qk.slice(0, -1)]) tiles[qk.slice(0, -1)] = []; tiles[qk.slice(0, -1)].push(generateBoundsParams(qk, bnds)); // walk in y-direction, starts top, goes down for(var j = 1; j <= quadsY; j++) { var qk = pointToQuadKey(x, topRight.y + j); var bnds = convertPointToLatLng(x, topRight.y + j, magic, R); if(!tiles[qk.slice(0, -1)]) tiles[qk.slice(0, -1)] = []; tiles[qk.slice(0, -1)].push(generateBoundsParams(qk, bnds)); } } // finally send ajax requests $.each(tiles, function(ind, tls) { data = { minLevelOfDetail: -1 }; data.boundsParamsList = tls; window.requests.add(window.postAjax('getThinnedEntitiesV2', data, window.handleDataResponse)); }); } // works on map data response and ensures entities are drawn/updated. window.handleDataResponse = function(data, textStatus, jqXHR) { // remove from active ajax queries list if(!data || !data.result) { window.failedRequestCount++; console.warn(data); return; } var portalUpdateAvailable = false; var portalInUrlAvailable = false; var m = data.result.map; // defer rendering of portals because there is no z-index in SVG. // this means that what’s rendered last ends up on top. While the // portals can be brought to front, this costs extra time. They need // 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.details.portalV2.linkedFields; fieldArray.splice($.inArray(guid, fieldArray), 1); }); } window.removeByGuid(guid); }); $.each(val.gameEntities, function(ind, ent) { // ent = [GUID, id(?), details] // format for links: { controllingTeam, creator, edge } // format for portals: { controllingTeam, turret } if(ent[2].turret !== undefined) { 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] ) return; ppp.push(ent); // delay portal render } else if(ent[2].edge !== undefined) { renderLink(ent); } 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 { 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]) { try { portals[selectedPortal].bringToFront(); } catch(e) { /* portal is now visible, catch Leaflet error */ } } if(portalInUrlAvailable) { renderPortalDetails(urlPortal); urlPortal = null; // select it only once } if(portalUpdateAvailable) renderPortalDetails(selectedPortal); resolvePlayerNames(); } // removes entities that are still handled by Leaflet, although they // do not intersect the current viewport. window.cleanUp = function() { var cnt = [0,0,0]; var b = getPaddedBounds(); var minlvl = getMinPortalLevel(); for(var i = 0; i < portalsLayers.length; i++) { // i is also the portal level 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(itemGuid == window.selectedPortal || (b.contains(item.getLatLng()) && i >= minlvl)) return true; cnt[0]++; portalsLayers[i].removeLayer(item); }); } linksLayer.eachLayer(function(link) { if(b.intersects(link.getBounds())) return; cnt[1]++; linksLayer.removeLayer(link); }); fieldsLayer.eachLayer(function(field) { if(b.intersects(field.getBounds())) return; cnt[2]++; fieldsLayer.removeLayer(field); }); console.log('removed out-of-bounds: '+cnt[0]+' portals, '+cnt[1]+' links, '+cnt[2]+' fields'); } // removes given entity from map window.removeByGuid = function(guid) { 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 TYPE_LINK: if(!window.links[guid]) return; linksLayer.removeLayer(window.links[guid]); break; 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(); } } // 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) return removeByGuid(ent[0]); // hide low level portals on low zooms var portalLevel = getPortalLevel(ent[2]); 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 changed that requires re-rendering the portal. if(!u) { // let resos handle themselves if they need to be redrawn renderResonators(ent, old); // update stored details for portal details in sidebar. old.options.details = ent[2]; return; } } // there were changes, remove old portal. 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 loadPlayerNamesForPortal(ent[2]); var lvWeight = Math.max(2, portalLevel / 1.5); var lvRadius = Math.max(portalLevel + 3, 5); var p = L.circleMarker(latlng, { 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], fillOpacity: 0.5, clickable: true, level: portalLevel, team: team, details: ent[2], guid: ent[0]}); 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, null); window.runHooks('portalAdded', {portal: p}); p.addTo(layerGroup); } window.renderResonators = function(ent, portalLayer) { if(!isResonatorsShow()) return; var portalLevel = getPortalLevel(ent[2]); if(portalLevel < getMinPortalLevel() && ent[0] !== selectedPortal) return; var portalLatLng = [ent[2].locationE6.latE6/1E6, ent[2].locationE6.lngE6/1E6]; var layerGroup = portalsLayers[parseInt(portalLevel)]; var reRendered = false; $.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)) 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) { return true; } // 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 resoGuid = portalResonatorGuid(ent[0], i); // the resonator var resoStyle = ent[0] === selectedPortal ? OPTIONS_RESONATOR_SELECTED : OPTIONS_RESONATOR_NON_SELECTED; var resoProperty = $.extend({ opacity: 1, fillColor: COLORS_LVL[rdata.level], fillOpacity: rdata.energyTotal/RESO_NRG[rdata.level], clickable: false, guid: resoGuid }, resoStyle); var reso = L.circleMarker(Rlatlng, resoProperty); // line connecting reso to portal var connStyle = ent[0] === selectedPortal ? OPTIONS_RESONATOR_LINE_SELECTED : OPTIONS_RESONATOR_LINE_NON_SELECTED; var connProperty = $.extend({ color: '#FFA000', 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 }, connStyle); var conn = L.polyline([portalLatLng, Rlatlng], connProperty); // put both in one group, so they can be handled by the same logic. var r = L.layerGroup([reso, conn]); r.options = { level: rdata.level, details: rdata, pDetails: ent[2], guid: resoGuid }; // However, LayerGroups (and FeatureGroups) don’t fire add/remove // events, thus this listener will be attached to the resonator. It // 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() { 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(); } // 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.isSameResonator = function(oldRes, newRes) { if(!oldRes && !newRes) return true; if(typeof oldRes !== typeof newRes) return false; if(oldRes.level !== newRes.level) return false; if(oldRes.energyTotal !== newRes.energyTotal) return false; if(oldRes.distanceToPortal !== newRes.distanceToPortal) return false; return true; } window.portalResetColor = function(portal) { 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 window.renderLink = function(ent) { 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; var latlngs = [ [edge.originPortalLocation.latE6/1E6, edge.originPortalLocation.lngE6/1E6], [edge.destinationPortalLocation.latE6/1E6, edge.destinationPortalLocation.lngE6/1E6] ]; var poly = L.polyline(latlngs, { color: COLORS[team], opacity: 1, weight:2, clickable: false, guid: ent[0], smoothFactor: 10 }); if(!getPaddedBounds().intersects(poly.getBounds())) return; poly.on('remove', function() { delete window.links[this.options.guid]; }); 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) { 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; var latlngs = [ [reg.vertexA.location.latE6/1E6, reg.vertexA.location.lngE6/1E6], [reg.vertexB.location.latE6/1E6, reg.vertexB.location.lngE6/1E6], [reg.vertexC.location.latE6/1E6, reg.vertexC.location.lngE6/1E6] ]; var poly = L.polygon(latlngs, { fillColor: COLORS[team], fillOpacity: 0.25, 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() { // 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; }