/* global IITC, L, log -- eslint */
/**
* Manages rendering of map data (portals, links, fields) into Leaflet.
* @class Render
*/
window.Render = function () {
this.portalMarkerScale = undefined;
};
/**
* Initiates a render pass. It's called at the start of making a batch of data requests to the servers.
*
* @function
* @memberof Render
* @param {L.LatLngBounds} bounds - The bounds within which the render pass will occur.
*/
window.Render.prototype.startRenderPass = function (bounds) {
this.deletedGuid = {}; // object - represents the set of all deleted game entity GUIDs seen in a render pass
this.seenPortalsGuid = {};
this.seenLinksGuid = {};
this.seenFieldsGuid = {};
// we pad the bounds used for clearing a litle bit, as entities are sometimes returned outside of their specified tile boundaries
// this will just avoid a few entity removals at start of render when they'll just be added again
var paddedBounds = bounds.pad(0.1);
this.clearPortalsOutsideBounds(paddedBounds);
this.clearLinksOutsideBounds(paddedBounds);
this.clearFieldsOutsideBounds(paddedBounds);
this.rescalePortalMarkers();
};
/**
* Clears portals outside the specified bounds.
*
* @function
* @memberof Render
* @param {L.LatLngBounds} bounds - The bounds to check against.
*/
window.Render.prototype.clearPortalsOutsideBounds = function (bounds) {
for (var guid in window.portals) {
var p = window.portals[guid];
// clear portals outside visible bounds - unless it's the selected portal, or it's relevant to artifacts
if (!bounds.contains(p.getLatLng()) && guid !== window.selectedPortal && !window.artifact.isInterestingPortal(guid)) {
this.deletePortalEntity(guid);
}
}
};
/**
* Clears links that are outside the specified bounds.
*
* @function
* @memberof Render
* @param {L.LatLngBounds} bounds - The bounds to check against for link removal.
*/
window.Render.prototype.clearLinksOutsideBounds = function (bounds) {
for (var guid in window.links) {
var l = window.links[guid];
// NOTE: our geodesic lines can have lots of intermediate points. the bounds calculation hasn't been optimised for this
// so can be particularly slow. a simple bounds check based on start+end point will be good enough for this check
var lls = l.getLatLngs();
var linkBounds = L.latLngBounds(lls);
if (!bounds.intersects(linkBounds)) {
this.deleteLinkEntity(guid);
}
}
};
/**
* Clears fields that are outside the specified bounds.
*
* @function
* @memberof Render
* @param {L.LatLngBounds} bounds - The bounds to check against for field removal.
*/
window.Render.prototype.clearFieldsOutsideBounds = function (bounds) {
for (var guid in window.fields) {
var f = window.fields[guid];
// NOTE: our geodesic polys can have lots of intermediate points. the bounds calculation hasn't been optimised for this
// so can be particularly slow. a simple bounds check based on corner points will be good enough for this check
var lls = f.getLatLngs();
var fieldBounds = L.latLngBounds([lls[0], lls[1]]).extend(lls[2]);
if (!bounds.intersects(fieldBounds)) {
this.deleteFieldEntity(guid);
}
}
};
/**
* Processes tile data including deleted entity GUIDs and game entities.
*
* @function
* @memberof Render
* @param {Object} tiledata - Data for a specific map tile.
*/
window.Render.prototype.processTileData = function (tiledata) {
this.processDeletedGameEntityGuids(tiledata.deletedGameEntityGuids || []);
this.processGameEntities(tiledata.gameEntities || []);
};
/**
* Processes deleted game entity GUIDs and removes them from the map.
*
* @function
* @memberof Render
* @param {Array} deleted - Array of deleted game entity GUIDs.
*/
window.Render.prototype.processDeletedGameEntityGuids = function (deleted) {
for (var i in deleted) {
var guid = deleted[i];
if (!(guid in this.deletedGuid)) {
this.deletedGuid[guid] = true; // flag this guid as having being processed
if (guid === window.selectedPortal) {
// the rare case of the selected portal being deleted. clear the details tab and deselect it
window.renderPortalDetails(null);
}
this.deleteEntity(guid);
}
}
};
/**
* Processes game entities (fields, links, portals) and creates them on the map.
*
* @function
* @memberof Render
* @param {Array} entities - Array of game entities.
* @param {string} details - Details for the {@link window.decodeArray.portal} function.
*/
window.Render.prototype.processGameEntities = function (entities, details) {
// details expected in decodeArray.portal
// we loop through the entities three times - for fields, links and portals separately
// this is a reasonably efficient work-around for leafletjs limitations on svg render order
for (const i in entities) {
const ent = entities[i];
if (ent[2][0] === 'r' && !(ent[0] in this.deletedGuid)) {
this.createFieldEntity(ent);
}
}
for (const i in entities) {
const ent = entities[i];
if (ent[2][0] === 'e' && !(ent[0] in this.deletedGuid)) {
this.createLinkEntity(ent);
}
}
for (const i in entities) {
const ent = entities[i];
if (ent[2][0] === 'p' && !(ent[0] in this.deletedGuid)) {
this.createPortalEntity(ent, details);
}
}
};
/**
* Ends a render pass. This includes cleanup and processing of any remaining data.
* Called when the render is considered complete.
*
* @function
* @memberof Render
*/
window.Render.prototype.endRenderPass = function () {
var countp = 0,
countl = 0,
countf = 0;
// check to see if there are any entities we haven't seen. if so, delete them
for (const guid in window.portals) {
// special case for selected portal - it's kept even if not seen
// artifact (e.g. jarvis shard) portals are also kept - but they're always 'seen'
if (!(guid in this.seenPortalsGuid) && guid !== window.selectedPortal) {
this.deletePortalEntity(guid);
countp++;
}
}
for (const guid in window.links) {
if (!(guid in this.seenLinksGuid)) {
this.deleteLinkEntity(guid);
countl++;
}
}
for (const guid in window.fields) {
if (!(guid in this.seenFieldsGuid)) {
this.deleteFieldEntity(guid);
countf++;
}
}
log.log('Render: end cleanup: removed ' + countp + ' portals, ' + countl + ' links, ' + countf + ' fields');
// reorder portals to be after links/fields
this.bringPortalsToFront();
// re-select the selected portal, to re-render the side-bar. ensures that any data calculated from the map data is up to date
if (window.selectedPortal) {
window.renderPortalDetails(window.selectedPortal);
}
};
/**
* Brings portal markers to the front of the map view, ensuring they are rendered above links and fields.
*
* @function
* @memberof Render
*/
window.Render.prototype.bringPortalsToFront = function () {
for (var guid in window.portals) {
window.portals[guid].bringToFront();
}
// artifact portals are always brought to the front, above all others
$.each(window.artifact.getInterestingPortals(), function (i, guid) {
if (window.portals[guid] && window.portals[guid]._map) {
window.portals[guid].bringToFront();
}
});
};
/**
* Deletes an entity (portal, link, or field) from the map based on its GUID.
*
* @function
* @memberof Render
* @param {string} guid - The globally unique identifier of the entity to delete.
*/
window.Render.prototype.deleteEntity = function (guid) {
this.deletePortalEntity(guid);
this.deleteLinkEntity(guid);
this.deleteFieldEntity(guid);
};
/**
* Deletes a portal entity from the map based on its GUID.
*
* @function
* @memberof Render
* @param {string} guid - The globally unique identifier of the portal to delete.
*/
window.Render.prototype.deletePortalEntity = function (guid) {
if (guid in window.portals) {
var p = window.portals[guid];
window.ornaments.removePortal(p);
this.removePortalFromMapLayer(p);
delete window.portals[guid];
window.runHooks('portalRemoved', { portal: p, data: p.options.data });
}
};
/**
* Deletes a link entity from the map based on its GUID.
*
* @function
* @memberof Render
* @param {string} guid - The globally unique identifier of the link to delete.
*/
window.Render.prototype.deleteLinkEntity = function (guid) {
if (guid in window.links) {
var l = window.links[guid];
l.remove();
delete window.links[guid];
window.runHooks('linkRemoved', { link: l, data: l.options.data });
}
};
/**
* Deletes a field entity from the map based on its GUID.
*
* @function
* @memberof Render
* @param {string} guid - The globally unique identifier of the field to delete.
*/
window.Render.prototype.deleteFieldEntity = function (guid) {
if (guid in window.fields) {
var f = window.fields[guid];
f.remove();
delete window.fields[guid];
window.runHooks('fieldRemoved', { field: f, data: f.options.data });
}
};
/**
* Creates a placeholder portal entity. This is used when the portal is not fully loaded,
* but its existence is known from links/fields.
*
* @function
* @memberof Render
* @param {string} guid - The globally unique identifier of the portal.
* @param {number} latE6 - The latitude of the portal in E6 format.
* @param {number} lngE6 - The longitude of the portal in E6 format.
* @param {string} team - The team faction of the portal.
* @param {number} [timestamp] - The timestamp of the portal data.
*/
window.Render.prototype.createPlaceholderPortalEntity = function (guid, latE6, lngE6, team, timestamp) {
// intel no longer returns portals at anything but the closest zoom
// stock intel creates 'placeholder' portals from the data in links/fields - IITC needs to do the same
// we only have the portal guid, lat/lng coords, and the faction - no other data
// having the guid, at least, allows the portal details to be loaded once it's selected. however,
// no highlighters, portal level numbers, portal names, useful counts of portals, etc are possible
// zero will mean any other source of portal data will have a higher timestamp
timestamp = timestamp || 0;
var ent = [
guid, // ent[0] = guid
-1, // ent[1] = timestamp - zero will mean any other source of portal data will have a higher timestamp
// ent[2] = an array with the entity data
[
'p', // 0 - a portal
team, // 1 - team
latE6, // 2 - lat
lngE6, // 3 - lng
],
];
// placeholder portals don't have a useful timestamp value - so the standard code that checks for updated
// portal details doesn't apply
// so, check that the basic details are valid and delete the existing portal if out of date
var portalMoved = false;
if (guid in window.portals) {
var p = window.portals[guid];
portalMoved = latE6 !== p.options.data.latE6 || lngE6 !== p.options.data.lngE6;
if (team !== p.options.data.team && p.options.timestamp < timestamp) {
// team - delete existing portal
this.deletePortalEntity(guid);
}
}
if (!portalMoved) {
this.createPortalEntity(ent, 'core'); // placeholder
}
};
/**
* Creates a portal entity from the provided game entity data.
* If the portal already exists and the new data is more recent, it replaces the existing data.
*
* @function
* @memberof Render
* @param {Array} ent - An array representing the game entity.
* @param {string} details - Detail level expected in {@link window.decodeArray.portal} (e.g., 'core', 'summary').
*/
window.Render.prototype.createPortalEntity = function (ent, details) {
this.seenPortalsGuid[ent[0]] = true; // flag we've seen it
var previousData = undefined;
var data = window.decodeArray.portal(ent[2], details);
// check if entity already exists
if (ent[0] in window.portals) {
// yes. now check to see if the entity data we have is newer than that in place
var p = window.portals[ent[0]];
if (!data.history || p.options.data.history === data.history)
if (p.options.timestamp >= ent[1]) {
return; // this data is identical or older - abort processing
}
// the data we have is newer. many data changes require re-rendering of the portal
// (e.g. level changed, so size is different, or stats changed so highlighter is different)
// so to keep things simple we'll always re-create the entity in this case
// remember the old details, for the callback
previousData = p.options.data;
// preserve history
if (!data.history) {
data.history = previousData.history;
}
this.deletePortalEntity(ent[0]);
}
var portalLevel = parseInt(data.level) || 0;
var team = window.teamStringToId(data.team);
// the data returns unclaimed portals as level 1 - but IITC wants them treated as level 0
if (team === window.TEAM_NONE) portalLevel = 0;
var latlng = L.latLng(data.latE6 / 1e6, data.lngE6 / 1e6);
var dataOptions = {
level: portalLevel,
team: team,
ent: ent, // LEGACY - TO BE REMOVED AT SOME POINT! use .guid, .timestamp and .data instead
guid: ent[0],
timestamp: ent[1],
data: data,
};
window.pushPortalGuidPositionCache(ent[0], data.latE6, data.lngE6);
var marker = window.createMarker(latlng, dataOptions);
function handler_portal_click(e) {
window.renderPortalDetails(e.target.options.guid);
}
function handler_portal_dblclick(e) {
window.renderPortalDetails(e.target.options.guid);
window.map.setView(e.target.getLatLng(), window.DEFAULT_ZOOM);
}
function handler_portal_contextmenu(e) {
window.renderPortalDetails(e.target.options.guid);
if (window.isSmartphone()) {
window.show('info');
} else if (!$('#scrollwrapper').is(':visible')) {
$('#sidebartoggle').click();
}
}
marker.on('click', handler_portal_click);
marker.on('dblclick', handler_portal_dblclick);
marker.on('contextmenu', handler_portal_contextmenu);
window.runHooks('portalAdded', { portal: marker, previousData: previousData });
window.portals[ent[0]] = marker;
// check for URL links to portal, and select it if this is the one
if (window.urlPortalLL && window.urlPortalLL[0] === marker.getLatLng().lat && window.urlPortalLL[1] === marker.getLatLng().lng) {
// URL-passed portal found via pll parameter - set the guid-based parameter
log.log('urlPortalLL ' + window.urlPortalLL[0] + ',' + window.urlPortalLL[1] + ' matches portal GUID ' + ent[0]);
window.urlPortal = ent[0];
window.urlPortalLL = undefined; // clear the URL parameter so it's not matched again
}
if (window.urlPortal === ent[0]) {
// URL-passed portal found via guid parameter - set it as the selected portal
log.log('urlPortal GUID ' + window.urlPortal + ' found - selecting...');
window.selectedPortal = ent[0];
window.urlPortal = undefined; // clear the URL parameter so it's not matched again
}
// (re-)select the portal, to refresh the sidebar on any changes
if (ent[0] === window.selectedPortal) {
log.log('portal guid ' + ent[0] + ' is the selected portal - re-rendering portal details');
window.renderPortalDetails(window.selectedPortal);
}
window.ornaments.addPortal(marker);
// TODO? postpone adding to the map layer
this.addPortalToMapLayer(marker);
};
/**
* Creates a field entity from the provided game entity data.
*
* @function
* @memberof Render
* @param {Array} ent - An array representing the game entity.
*/
window.Render.prototype.createFieldEntity = function (ent) {
this.seenFieldsGuid[ent[0]] = true; // flag we've seen it
var data = {
// type: ent[2][0],
timestamp: ent[1],
team: ent[2][1],
points: ent[2][2].map(function (arr) {
return { guid: arr[0], latE6: arr[1], lngE6: arr[2] };
}),
};
// create placeholder portals for field corners. we already do links, but there are the odd case where this is useful
for (var i = 0; i < 3; i++) {
var p = data.points[i];
this.createPlaceholderPortalEntity(p.guid, p.latE6, p.lngE6, data.team, data.timestamp);
}
// check if entity already exists
if (ent[0] in window.fields) {
// yes. in theory, we should never get updated data for an existing field. they're created, and they're destroyed - never changed
// but theory and practice may not be the same thing...
var f = window.fields[ent[0]];
if (f.options.timestamp >= ent[1]) return; // this data is identical (or order) than that rendered - abort processing
// the data we have is newer - two options
// 1. just update the data, assume the field render appearance is unmodified
// 2. delete the entity, then re-create with the new data
this.deleteFieldEntity(ent[0]); // option 2, for now
}
var team = window.teamStringToId(ent[2][1]);
var latlngs = [
L.latLng(data.points[0].latE6 / 1e6, data.points[0].lngE6 / 1e6),
L.latLng(data.points[1].latE6 / 1e6, data.points[1].lngE6 / 1e6),
L.latLng(data.points[2].latE6 / 1e6, data.points[2].lngE6 / 1e6),
];
var poly = L.geodesicPolygon(latlngs, {
fillColor: window.COLORS[team],
fillOpacity: 0.25,
stroke: false,
interactive: false,
team: team,
ent: ent, // LEGACY - TO BE REMOVED AT SOME POINT! use .guid, .timestamp and .data instead
guid: ent[0],
timestamp: data.timestamp,
data: data,
});
window.runHooks('fieldAdded', { field: poly });
window.fields[ent[0]] = poly;
// TODO? postpone adding to the layer??
if (!IITC.filters.filterField(poly)) poly.addTo(window.map);
};
/**
* Creates a link entity from the provided game entity data.
*
* @function
* @memberof Render
* @param {Array} ent - An array representing the game entity.
*/
window.Render.prototype.createLinkEntity = function (ent) {
// Niantic have been faking link entities, based on data from fields
// these faked links are sent along with the real portal links, causing duplicates
// the faked ones all have longer GUIDs, based on the field GUID (with _ab, _ac, _bc appended)
var fakedLink = new RegExp('^[0-9a-f]{32}.b_[ab][bc]$'); // field GUIDs always end with ".b" - faked links append the edge identifier
if (fakedLink.test(ent[0])) return;
this.seenLinksGuid[ent[0]] = true; // flag we've seen it
var data = {
// TODO add other properties and check correction direction
// type: ent[2][0],
timestamp: ent[1],
team: ent[2][1],
oGuid: ent[2][2],
oLatE6: ent[2][3],
oLngE6: ent[2][4],
dGuid: ent[2][5],
dLatE6: ent[2][6],
dLngE6: ent[2][7],
};
// create placeholder entities for link start and end points (before checking if the link itself already exists
this.createPlaceholderPortalEntity(data.oGuid, data.oLatE6, data.oLngE6, data.team, data.timestamp);
this.createPlaceholderPortalEntity(data.dGuid, data.dLatE6, data.dLngE6, data.team, data.timestamp);
// check if entity already exists
if (ent[0] in window.links) {
var l = window.links[ent[0]];
if (l.options.timestamp >= ent[1]) return; // this data is older or identical to the rendered data - abort processing
// the data is newer/better - two options
// 1. just update the data. assume the link render appearance is unmodified
// 2. delete the entity, then re-create it with the new data
this.deleteLinkEntity(ent[0]); // option 2 - for now
}
var team = window.teamStringToId(ent[2][1]);
var latlngs = [L.latLng(data.oLatE6 / 1e6, data.oLngE6 / 1e6), L.latLng(data.dLatE6 / 1e6, data.dLngE6 / 1e6)];
var poly = L.geodesicPolyline(latlngs, {
color: window.COLORS[team],
opacity: 1,
weight: 2,
interactive: false,
team: team,
ent: ent, // LEGACY - TO BE REMOVED AT SOME POINT! use .guid, .timestamp and .data instead
guid: ent[0],
timestamp: ent[1],
data: data,
});
window.runHooks('linkAdded', { link: poly });
window.links[ent[0]] = poly;
if (!IITC.filters.filterLink(poly)) poly.addTo(window.map);
};
/**
* Rescales portal markers based on the current map zoom level.
*
* @function
* @memberof Render
*/
window.Render.prototype.rescalePortalMarkers = function () {
if (this.portalMarkerScale === undefined || this.portalMarkerScale !== window.portalMarkerScale()) {
this.portalMarkerScale = window.portalMarkerScale();
log.log('Render: map zoom ' + window.map.getZoom() + ' changes portal scale to ' + window.portalMarkerScale() + ' - redrawing all portals');
// NOTE: we're not calling this because it resets highlights - we're calling it as it
// resets the style (inc size) of all portal markers, applying the new scale
window.resetHighlightedPortals();
}
};
/**
* Adds a portal to the visible map layer.
*
* @function
* @memberof Render
* @param {Object} portal - The portal object to add to the map layer.
*/
window.Render.prototype.addPortalToMapLayer = function (portal) {
if (!IITC.filters.filterPortal(portal)) portal.addTo(window.map);
};
/**
* Removes a portal from the visible map layer.
*
* @function
* @memberof Render
* @param {Object} portal - The portal object to remove from the map layer.
*/
window.Render.prototype.removePortalFromMapLayer = function (portal) {
// remove it from the portalsLevels layer
portal.remove();
};