/* global IITC, L, log -- eslint */
/**
* @namespace window.ornaments
* @description Manages the overlay of additional images (ornaments) on portals, such as beacons, frackers,
* and anomaly markers.
*
* Added as part of the Ingress #Helios in 2014, ornaments are additional image overlays for portals.
* currently there are 6 known types of ornaments: `ap$x$suffix`
* - `cluster portals` (without suffix)
* - `volatile portals` (_v)
* - `meeting points` (_start)
* - `finish points` (_end)
*
* Beacons and Frackers were introduced at the launch of the Ingress ingame store on November 1st, 2015
* - `Beacons` (pe$TAG - $NAME) ie: `peNIA - NIANTIC`
* - `Frackers` ('peFRACK')
* (there are 7 different colors for each of them)
*
* Ornament IDs are dynamic. NIANTIC might change them at any time without prior notice.
* New ornamnent IDs found on the map will be recorded and saved to knownOrnaments from
* which the Ornaments dialog will be filled with checked checkboxes.
* To exclude a set of ornaments, even if they have not yet shown up on the map, the user
* can add an entry to excludedOrnaments, which will compared (startsWith) to all known and
* future IDs. example: "ap" to exclude all Ornaments for anomalies (ap1, ap2, ap2_v)
*/
window.ornaments = {
/**
* Default size for ornament.
* @constant
* @type {number}
*/
OVERLAY_SIZE: 60,
/**
* Default opacity for ornament.
* @constant
* @type {number}
*/
OVERLAY_OPACITY: 0.6,
/**
* Object holding optional definitions for ornaments and beacons.
* The icon object holds optional definitions for the ornaments an beacons. The object shall
* be filled from a plugin
* ```
* 'ornamentID' : {
* name: 'meaningful name', // shows up in dialog
* layer: 'name for the Layer', // shows up in layerchooser, optional, if not set
* // ornament will be in "Ornaments"
* url: 'url', // from which the image will be taken, optional,
* // 84x84px is default, if not set, stock images will be
* // used
* offset: [dx,dy], // optional, shift the ornament vertically or horizontally by
* // dx (vertical)and dy )horizontal.
* // [0, 0.5] to place right above the portal.
* // default is [0, 0] to center
* opacity: 0..1 // optional, default is 0.6
* }
* ```
*
* @property {object} icon - The icon object for ornaments and beacons.
*/
icon: {},
/**
* List of ornaments to be excluded.
* @property {string[]} excludedOrnaments - Patterns to be excluded from display.
*/
excludedOrnaments: [],
/**
* List of known ornaments.
* @property {object} knownOrnaments - Object tracking known ornaments.
*/
knownOrnaments: {},
/**
* Sets up the ornament layer and necessary event handlers.
*
* @function
* @memberof window.ornaments
*/
setup: function () {
this._portals = {};
this.layerGroup = L.layerGroup;
if (window.map.options.preferCanvas && L.Browser.canvas && !window.DISABLE_CANVASICONLAYER) {
this.layerGroup = L.canvasIconLayer;
L.CanvasIconLayer.mergeOptions({ padding: L.Canvas.prototype.options.padding });
}
this.load();
this.layers = {};
this.layers['Ornaments'] = window.ornaments.layerGroup();
this.layers['Excluded ornaments'] = window.ornaments.layerGroup(); // to keep excluded ornaments in an own layer
window.layerChooser.addOverlay(this.layers['Ornaments'], 'Ornaments');
window.layerChooser.addOverlay(this.layers['Excluded ornaments'], 'Excluded ornaments', { default: false });
IITC.toolbox.addButton({
id: 'ornaments-toolbox-link',
label: 'Ornaments Opt',
title: 'Edit ornament exclusions',
accesskey: 'o',
action: window.ornaments.ornamentsOpt,
});
},
/**
* Creates a new layer for a given ornament ID.
*
* @function
* @memberof window.ornaments
* @param {string} layerID - The ID for the new layer.
*/
createLayer: function (layerID) {
window.ornaments.layers[layerID] = window.ornaments.layerGroup();
window.layerChooser.addOverlay(window.ornaments.layers[layerID], layerID);
},
/**
* Adds ornament overlays to the specified portal.
*
* @function
* @memberof window.ornaments
* @param {object} portal - The portal to which ornaments are added.
*/
addPortal: function (portal) {
this.removePortal(portal);
var ornaments = portal.options.data.ornaments;
if (ornaments && ornaments.length) {
this._portals[portal.options.guid] = ornaments.map(function (ornament) {
var layer = this.layers['Ornaments'];
var opacity = this.OVERLAY_OPACITY;
var size = this.OVERLAY_SIZE * window.portalMarkerScale();
var anchor = [size / 2, size / 2];
var iconUrl = '//commondatastorage.googleapis.com/ingress.com/img/map_icons/marker_images/' + ornament + '.png';
if (!this.knownOrnaments[ornament]) {
this.knownOrnaments[ornament] = false;
}
if (ornament in this.icon) {
if (this.icon[ornament].layer) {
if (this.layers[this.icon[ornament].layer] === undefined) {
log.log('Add missing layer: ', this.icon[ornament].layer);
window.ornaments.createLayer(window.ornaments.icon[ornament].layer);
}
layer = this.layers[window.ornaments.icon[ornament].layer];
}
if (window.ornaments.icon[ornament].url) {
iconUrl = window.ornaments.icon[ornament].url;
if (this.icon[ornament].offset) {
var offset = this.icon[ornament].offset;
anchor = [size * offset[0] + anchor[0], size * offset[1] + anchor[1]];
}
if (this.icon[ornament].opacity) {
opacity = this.icon[ornament].opacity;
}
}
}
var exclude = false;
if (this.excludedOrnaments && !(this.excludedOrnaments.length === 1 && this.excludedOrnaments[0] === '')) {
exclude = this.excludedOrnaments.some(function (pattern) {
return ornament.startsWith(pattern);
});
}
exclude = exclude || this.knownOrnaments[ornament];
if (exclude) {
layer = this.layers['Excluded ornaments'];
}
return L.marker(portal.getLatLng(), {
icon: L.icon({
iconUrl: iconUrl,
iconSize: [size, size],
iconAnchor: anchor, // https://github.com/IITC-CE/Leaflet.Canvas-Markers/issues/4
className: 'no-pointer-events',
}),
interactive: false,
keyboard: false,
opacity: opacity,
layer: layer,
}).addTo(layer);
}, this);
}
},
/**
* Removes ornament overlays from the specified portal.
*
* @function
* @memberof window.ornaments
* @param {object} portal - The portal from which ornaments are removed.
*/
removePortal: function (portal) {
var guid = portal.options.guid;
if (this._portals[guid]) {
this._portals[guid].forEach(function (marker) {
marker.options.layer.removeLayer(marker);
});
delete this._portals[guid];
}
},
/**
* Initializes known ornaments.
*
* @function
* @memberof window.ornaments
*/
initOrnaments: function () {
this.knownOrnaments = {};
this.save();
},
/**
* Loads ornament data from localStorage.
*
* @function
* @memberof window.ornaments
*/
load: function () {
var dataStr;
try {
dataStr = localStorage.getItem('excludedOrnaments');
if (!dataStr) {
return;
}
this.excludedOrnaments = JSON.parse(dataStr);
} catch (e) {
log.warn('ornaments: failed to load excludedOrnaments from localStorage: ' + e);
}
try {
dataStr = localStorage.getItem('knownOrnaments');
if (!dataStr) {
this.initOrnaments();
return;
}
this.knownOrnaments = JSON.parse(dataStr);
} catch (e) {
log.warn('ornaments: failed to load data from localStorage: ' + e);
}
},
/**
* Saves the current ornament configuration to localStorage.
*
* @function
* @memberof window.ornaments
*/
save: function () {
localStorage['excludedOrnaments'] = JSON.stringify(this.excludedOrnaments);
localStorage['knownOrnaments'] = JSON.stringify(this.knownOrnaments);
},
/**
* Reloads all ornaments on the map.
* @function
* @memberof window.ornaments
*/
reload: function () {
// reload: addPortal also calls removePortal
for (var guid in window.ornaments._portals) {
window.ornaments.addPortal(window.portals[guid]);
}
},
/**
* Processes input data for managing ornaments.
*
* @function
* @memberof window.ornaments
*/
processInput: function () {
window.ornaments.excludedOrnaments = $('#ornaments_E')
.val()
.split(/[\s,]+/);
window.ornaments.excludedOrnaments = window.ornaments.excludedOrnaments.filter(function (ornamentCode) {
return ornamentCode !== '';
});
// process the input from the checkboxes
for (var ornamentCode in window.ornaments.knownOrnaments) {
var input = $('#chk_orn_' + ornamentCode);
window.ornaments.knownOrnaments[ornamentCode] = input.is(':checked');
}
},
/**
* Generates a list of ornaments for display in the options dialog.
*
* @function
* @memberof window.ornaments
* @returns {string} HTML string representing the list of ornaments.
*/
ornamentsList: function () {
var text = '';
var sortedIDs = Object.keys(window.ornaments.knownOrnaments).sort();
sortedIDs.forEach(function (ornamentCode) {
var hidden = window.ornaments.excludedOrnaments.some(function (code) {
return ornamentCode.startsWith(code);
});
var name = window.ornaments.icon[ornamentCode] ? window.ornaments.icon[ornamentCode].name + ' (' + ornamentCode + ')' : ornamentCode;
var checked = window.ornaments.knownOrnaments[ornamentCode] || hidden ? 'checked ' : '';
text += '<label><input id="chk_orn_' + ornamentCode + '" type="checkbox" ' + checked;
text += ' onchange="window.ornaments.processInput();window.ornaments.save();window.ornaments.reload()"';
text += hidden ? 'disabled' : '';
text += '>' + name + '</label><br>';
});
return text;
},
/**
* Replaces the content of the ornaments list in the dialog.
*
* @function
* @memberof window.ornaments
*/
replaceOL: function () {
document.getElementById('ornamentsList').innerHTML = window.ornaments.ornamentsList();
},
/**
* Handles changes in ornament options and updates the map accordingly.
*
* @function
* @memberof window.ornaments
*/
onChangeHandler: function () {
window.ornaments.processInput();
window.ornaments.replaceOL();
window.ornaments.save();
window.ornaments.reload();
},
/**
* Opens the dialog for ornament options, allowing users to manage ornament visibility.
*
* @function
* @memberof window.ornaments
*/
ornamentsOpt: function () {
var excludedIDs = window.ornaments.excludedOrnaments.join(',');
var html =
'<div class="ornamentsOpts">' +
'Hide Ornaments from IITC that start with:<br>' +
`<input type="text" value="${excludedIDs}" id="ornaments_E"` +
' onchange="window.ornaments.onChangeHandler()" /><br>' +
'(separator: space or comma allowed)<hr>' +
'<b>known Ornaments, check to hide:</b><br>' +
`<div id="ornamentsList"> ${window.ornaments.ornamentsList()}</div>` +
'</div>';
window.dialog({
html: html,
id: 'ornamentsOpt',
title: 'Ornament excludes',
buttons: {
RESET: function () {
window.ornaments.initOrnaments();
window.ornaments.reload();
$(this).dialog('close');
},
OK: function () {
// process the input from the input
window.ornaments.processInput();
window.ornaments.save();
window.ornaments.reload();
$(this).dialog('close');
},
},
});
},
};