/* global IITC, L, log -- eslint */
/**
* @file This file contains the code related to creating and updating portal markers on the map.
* @module portal_marker
*/
// portal hooks
function handler_portal_click(e) {
window.selectPortal(e.target.options.guid, e.type);
window.renderPortalDetails(e.target.options.guid);
}
function handler_portal_dblclick(e) {
window.selectPortal(e.target.options.guid, e.type);
window.renderPortalDetails(e.target.options.guid);
window.map.setView(e.target.getLatLng(), window.DEFAULT_ZOOM);
}
function handler_portal_contextmenu(e) {
window.selectPortal(e.target.options.guid, e.type);
window.renderPortalDetails(e.target.options.guid);
if (window.isSmartphone()) {
window.show('info');
} else if (!$('#scrollwrapper').is(':visible')) {
$('#sidebartoggle').click();
}
}
L.PortalMarker = L.CircleMarker.extend({
options: {},
statics: {
// base style
portalBaseStyle: {
stroke: true,
opacity: 1,
fill: true,
fillOpacity: 0.5,
interactive: true,
},
// placeholder style
placeholderStyle: {
dashArray: '1,2',
weight: 1,
},
// portal level 0 1 2 3 4 5 6 7 8
LEVEL_TO_WEIGHT: [2, 2, 2, 2, 2, 3, 3, 4, 4],
LEVEL_TO_RADIUS: [7, 7, 7, 7, 8, 8, 9, 10, 11],
},
initialize: function (latlng, data) {
L.CircleMarker.prototype.initialize.call(this, latlng);
this._selected = data.guid === window.selectedPortal;
this.updateDetails(data);
this.on('click', handler_portal_click);
this.on('dblclick', handler_portal_dblclick);
this.on('contextmenu', handler_portal_contextmenu);
},
willUpdate: function (details) {
// details are from a placeholder
if (details.level === undefined) {
// if team differs and corresponding link is more recent (ignore field)
return this._details.timestamp < details.timestamp && this._details.team !== details.team;
}
// more recent timestamp, this occurs when the data has changed because of:
// - resonator deploy/upgrade
// - mod deploy
// - recharge/damage/decay
// - portal edit (title, location, portal main picture)
if (this._details.timestamp < details.timestamp) {
return true;
}
// current marker is a placeholder, and details is real data
if (this.isPlaceholder() && this._details.team === details.team) {
return true;
}
// even if we get history that was missing ? is it even possible ?
if (this._details.timestamp > details.timestamp) {
return false;
}
// this._details.timestamp === details.timestamp
// get new history
if (details.history) {
if (!this._details.history) {
return true;
}
if (this._details.history._raw !== details.history._raw) {
return true;
}
}
// get details portal data
if (!this._details.mods && details.mods) {
return true;
}
return false;
},
updateDetails: function (details) {
if (this._details) {
// portal has been moved
if (this._details.latE6 !== details.latE6 || this._details.lngE6 !== details.lngE6) {
this.setLatLng(L.latLng(details.latE6 / 1e6, details.lngE6 / 1e6));
}
// core data from a placeholder
if (details.level === undefined) {
// if team has changed
if (this._details.timestamp < details.timestamp && this._details.team !== details.team) {
// keep history, title, image
details.title = this._details.title;
details.image = this._details.image;
details.history = this._details.history;
this._details = details;
}
} else if (this._details.timestamp === details.timestamp) {
// we got more details (core/summary -> summary/detailed/extended)
var localThis = this;
[
'level',
'health',
'resCount',
'image',
'title',
'ornaments',
'mission',
'mission50plus',
'artifactBrief',
'mods',
'resonators',
'owner',
'artifactDetail',
].forEach(function (prop) {
if (details[prop]) localThis._details[prop] = details[prop];
});
// smarter update for history (cause it's missing sometimes)
if (details.history) {
if (!this._details.history) {
this._details.history = details.history;
} else {
if (this._details.history._raw && details.history._raw !== this._details.history._raw) {
log.warn('new portal data has lost some history');
}
this._details.history._raw |= details.history._raw;
['visited', 'captured', 'scoutControlled'].forEach(function (prop) {
localThis._details.history[prop] ||= details.history[prop];
});
}
}
// LEGACY - TO BE REMOVED AT SOME POINT! use .guid, .timestamp and .data instead
this._details.ent = details.ent;
} else {
// permanent data (history only)
if (!details.history) {
details.history = this._details.history;
}
this._details = details;
}
} else {
this._details = details;
}
this._level = parseInt(this._details.level) || 0;
this._team = IITC.utils.getTeamId(this._details.team);
// the data returns unclaimed portals as level 1 - but IITC wants them treated as level 0
if (this._team === window.TEAM_NONE) {
this._level = 0;
}
// compatibility
var dataOptions = {
guid: this._details.guid,
level: this._level,
team: this._team,
ent: this._details.ent, // LEGACY - TO BE REMOVED AT SOME POINT! use .guid, .timestamp and .data instead
timestamp: this._details.timestamp,
data: this._details,
};
L.setOptions(this, dataOptions);
if (this._selected) {
this.renderDetails();
}
this.setSelected();
},
renderDetails() {
if (!this._rendering) {
this._rendering = true;
window.renderPortalDetails(this._details.guid);
this._rendering = false;
}
},
getDetails: function () {
return this._details;
},
isPlaceholder: function () {
return this._details.level === undefined;
},
hasFullDetails: function () {
return !!this._details.mods;
},
setStyle: function (style) {
// stub for highlighters
L.Util.setOptions(this, style);
return this;
},
setMarkerStyle: function (style) {
var styleOptions = L.Util.extend(this._style(), style);
L.Util.setOptions(this, styleOptions);
L.Util.setOptions(this, window.highlightPortal(this));
var selected = L.extend({ radius: this.options.radius }, this._selected && { color: window.COLOR_SELECTED_PORTAL });
return L.CircleMarker.prototype.setStyle.call(this, selected);
},
setSelected: function (selected) {
if (selected === false) {
this._selected = false;
} else {
this._selected = this._selected || selected;
}
this.setMarkerStyle();
if (this._selected && window.map.hasLayer(this)) {
this.bringToFront();
}
},
_style: function () {
var dashArray = null;
// dashed outline for placeholder portals
if (this.isPlaceholder()) {
dashArray = L.PortalMarker.placeholderStyle.dashArray;
}
return L.extend(this._scale(), L.PortalMarker.portalBaseStyle, {
color: window.COLORS[this._team],
fillColor: window.COLORS[this._team],
dashArray: dashArray,
});
},
_scale: function () {
var scale = window.portalMarkerScale();
var level = Math.floor(this._level || 0);
var lvlWeight = L.PortalMarker.LEVEL_TO_WEIGHT[level] * Math.sqrt(scale);
var lvlRadius = L.PortalMarker.LEVEL_TO_RADIUS[level] * scale;
// thinner outline for placeholder portals
if (this.isPlaceholder()) {
lvlWeight = L.PortalMarker.placeholderStyle.weight;
}
return {
radius: lvlRadius,
weight: lvlWeight,
};
},
});
/**
* Calculates the scale of portal markers based on the current zoom level of the map.
*
* @function portalMarkerScale
* @returns {number} The scale factor for portal markers.
*/
window.portalMarkerScale = function () {
var zoom = window.map.getZoom();
if (L.Browser.mobile) return zoom >= 16 ? 1.5 : zoom >= 14 ? 1.2 : zoom >= 11 ? 1.0 : zoom >= 8 ? 0.65 : 0.5;
else return zoom >= 14 ? 1 : zoom >= 11 ? 0.8 : zoom >= 8 ? 0.65 : 0.5;
};
/**
* Creates a new portal marker on the map.
*
* @function createMarker
* @param {L.LatLng} latlng - The latitude and longitude where the marker will be placed.
* @param {Object} data - The IITC-specific entity data to be stored in the marker options.
* @returns {L.PortalMarker} A Leaflet circle marker representing the portal.
*/
window.createMarker = function (latlng, data) {
return new L.PortalMarker(latlng, data);
};
/**
* Sets the style of a portal marker, including options for when the portal is selected.
*
* @function setMarkerStyle
* @param {L.PortalMarker} marker - The portal marker whose style will be set.
* @param {boolean} selected - Indicates if the portal is selected.
*/
window.setMarkerStyle = function (marker, selected) {
marker.setSelected(selected);
};
/**
* Determines the style options for a portal marker based on its details.
*
* @function getMarkerStyleOptions
* @param {Object} details - Details of the portal, including team and level.
* @returns {Object} Style options for the portal marker.
*/
window.getMarkerStyleOptions = function (details) {
var scale = window.portalMarkerScale();
var level = Math.floor(details.level || 0);
var lvlWeight = L.PortalMarker.LEVEL_TO_WEIGHT[level] * Math.sqrt(scale);
var lvlRadius = L.PortalMarker.LEVEL_TO_RADIUS[level] * scale;
var dashArray = null;
// thinner and dashed outline for placeholder portals
if (details.team !== window.TEAM_NONE && level === 0) {
lvlWeight = L.PortalMarker.placeholderStyle.weight;
dashArray = L.PortalMarker.placeholderStyle.dashArray;
}
var options = L.extend(
{
radius: lvlRadius,
weight: lvlWeight,
},
L.PortalMarker.portalBaseStyle,
{
color: window.COLORS[details.team],
fillColor: window.COLORS[details.team],
dashArray: dashArray,
}
);
return options;
};