Widget:Map floors/script
From Guild Wars 2 Wiki
Jump to navigationJump to search
/* eslint-disable no-useless-escape */
/* <nowiki> */
/**
* widget-map-floors
* https://github.com/GW2Wiki/widget-map-floors
*
* Created by Smiley on 11.06.2016.
* https://github.com/codemasher
* https://wiki.gw2.org.cn/wiki/User:Smiley-1
*
* scripts & libraries used:
*
* https://leafletjs.com/
* http://vanilla-js.com/
*/
'use strict';
const GW2MapOptions = {
// errorTile : 'https://wiki.gw2.org.cn/images/a/af/Widget_Map_floors_blank_tile.png',
initLayers : [
'region_label','map_label','task_icon','heropoint_icon','waypoint_icon','landmark_icon','vista_icon',
'unlock_icon','masterypoint_icon','adventure_icon','jumpingpuzzle_icon', 'sector_label',
],
};
/**
* Class GW2Map
*/
class GW2Map{
errorTile = 'data:image/png;base64,'
+'iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAADHUlEQVR4nO3UMQEAIAzAsIF/zyBjRxMF'
+'vXpm5g2QdLcDgD0GAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEG'
+'AGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEG'
+'AGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEG'
+'AGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEG'
+'AGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEG'
+'AGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEG'
+'AGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEG'
+'AGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEG'
+'AGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEG'
+'AGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEG'
+'AGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEG'
+'AGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEG'
+'AGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEfdCIC/5Nk'
+'Vo8AAAAASUVORK5CYII=';
// common default settings for all maps
options = {
containerClassName: 'gw2map',
linkboxClassName : 'gw2map-linkbox', // additional to containerClassName
navClassName : 'gw2map-nav',
lang : 'en',
initLayers : null,
mapAttribution : true,
attributionText : ' © <a href="http://www.arena.net/" target="_blank">ArenaNet</a>',
errorTile : this.errorTile,
padding : 0.5,
defaultZoom : 4,
minZoom : 0,
maxZoom : 7,
fullscreenControl : true,
coordView : true,
apiBase : 'https://api.guildwars2.com',
tileBase : 'https://tiles.guildwars2.com/',
tileExt : '.jpg',
colors : {
map_poly : 'rgba(255, 255, 255, 0.5)',
region_poly: 'rgba(255, 155, 255, 0.5)',
sector_poly: 'rgba(40, 140, 25, 0.5)',
task_poly : 'rgba(250, 250, 30, 0.5)',
event_poly : 'rgba(210, 125, 40, 0.5)',
},
};
// @todo: un-hardcode
iconZoomLayers = [
'waypoint_icon',
'landmark_icon',
'vista_icon',
'heropoint_icon',
'task_icon',
'unlock_icon',
'masterypoint_icon',
'adventure_icon',
'jumpingpuzzle_icon',
'region_label',
'map_label',
'sector_label',
'event_icon',
'lavatubes',
'guild_bounty',
];
linkboxExclude = [
'region_label',
'region_poly',
'map_poly',
'sector_poly',
'task_poly',
'event_poly',
];
// per-map options parsed from the container's dataset
dataset = {};
layers = {};
/**
* GW2Map constructor.
*
* @param {HTMLElement} container
* @param {string} id
* @param {Object} options
* @returns {GW2Map}
*/
constructor(container, id, options){
this.container = container;
this.id = id;
this.options = GW2MapUtil.extend(this.options, options);
this.dataset = new GW2MapDataset(this.container.dataset, this.options).getData();
}
/**
* initializes the map
*
* @returns {GW2Map}
* @public
*/
init(){
// limit maxZoom for continent 2 (PvP)
if(this.dataset.continentId === 2){
this.options.maxZoom = 6;
}
// adjust maxZoom if a dataset value is given
if(this.dataset.maxZoom){
this.options.maxZoom = this.dataset.maxZoom;
}
// create an optional linkbox container and add it besides the map
if(this.dataset.linkbox){
this.linkbox = document.createElement('div');
this.linkbox.className = this.options.navClassName;
this.linkbox.style = 'max-height:'+this.container.clientHeight+'px;';
this.container.className += ' '+this.options.linkboxClassName;
this.container.parentNode.insertBefore(this.linkbox, this.container.nextSibling);
}
this._setBaseMap();
// build the request path
let url = this.options.apiBase + '/v2'
+ '/continents/' + this.dataset.continentId
+ '/floors/' + this.dataset.floorId
+ (this.dataset.regionId ? '/regions/' + this.dataset.regionId : '')
+ (this.dataset.regionId && this.dataset.mapId ? '/maps/' + this.dataset.mapId : '')
+ '?wiki=1&lang=' + this.dataset.language;
this._request(url, '_renderFloor');
return this;
}
/**
* fetches json data from $url and calls $callback with the response data as argument
*
* @param {string} url
* @param {string} callback
* @protected
*/
_request(url, callback){
// xhr > fetch. DON'T @ ME
let request = new XMLHttpRequest();
request.open('GET', url, true);
request.addEventListener('load', ev => {
if(request.readyState === 4 && request.status === 200){
let json = JSON.parse(request.responseText);
// callback is a class method
if(typeof callback === 'string'){
return this[callback](json);
}
// lambda callback
return callback(json);
}
console.log('(╯°□°)╯彡┻━┻ ', request.status);
});
request.send();
}
/**
* sets the base tiles and adds an optional copyright info
*
* @returns {GW2Map}
* @protected
*/
_setBaseMap(){
// the map object
this.map = L.map(this.container, {
crs : L.CRS.Simple,
minZoom : this.options.minZoom,
maxZoom : this.options.maxZoom,
attributionControl: this.options.mapAttribution,
zoomControl : this.dataset.mapControls,
fullscreenControl : this.options.fullscreenControl,
coordView : this.options.coordView,
});
// the main tile layer
L.tileLayer(null, {
// use the custom tile getter
tileGetter : (coords, zoom) => this._tileGetter(coords, zoom),
continuousWorld : true,
minZoom : this.options.minZoom,
maxZoom : this.options.maxZoom,
attribution : this.options.mapAttribution === true
? GW2MAP_I18N.attribution + this.options.attributionText
: false,
}).addTo(this.map);
// add the layer controls
if(this.dataset.mapControls){
this.controls = L.control.layers().addTo(this.map);
}
return this;
}
/**
* parses the floor data and rencers it on the map
*
* @param {*} json
* @protected
*/
_renderFloor(json){
// transform the response to GeoJSON - polyfill for https://github.com/arenanet/api-cdi/pull/62
this.floorGeoJSON = new GW2FloorGeoJSON(
json,
this.dataset.customRect,
this.dataset.extraLayers,
this.dataset.includeMaps
);
let geojson = this.floorGeoJSON.getData();
this.viewRect = geojson.viewRect; // set viewRect for the tile getter
// set map bounds and center
let rect = new GW2ContinentRect(this.viewRect).getBounds();
let bounds = new L.LatLngBounds(this._p2ll(rect[0]), this._p2ll(rect[1])).pad(this.options.padding);
let center = bounds.getCenter();
let coords = this.dataset.centerCoords || [];
if(coords.length === 2){
if(coords[0] > 0 && coords[0] <= 49152 && coords[1] > 0 && coords[1] <= 49152){
center = this._p2ll(coords);
}
}
this.map.setMaxBounds(bounds).setView(center, this.dataset.zoom);
// create layer panes
let panes = Object.keys(geojson.featureCollections);
let initLayers = this.dataset.initLayers || this.options.initLayers || panes;
panes.forEach(pane => this._createPane(geojson.featureCollections[pane].getJSON(), pane, initLayers));
this.map.on('zoomend', ev => this._zoomEndEvent());
this._zoomEndEvent(); // invoke once to set the icon zoom on the newly created map
// render optional event data
if(this.dataset.events){
this._renderEvents();
}
}
/**
* handles leaflet's zoomEnd event, adjusts icon sizes and label positions
*
* @protected
*/
_zoomEndEvent(){
let zoom = this.map.getZoom();
this.iconZoomLayers.forEach(layer => {
if(!this.layers[layer]){
return;
}
let element = this.layers[layer].options.pane;
if(zoom >= 5){
PrototypeElement.removeClassName(element, 'half');
}
else if(zoom < 5 && zoom >= 3){
PrototypeElement.removeClassName(element, 'quarter');
PrototypeElement.addClassName(element, 'half');
}
else if(zoom < 3 && zoom >= 1){
PrototypeElement.removeClassName(element, 'half');
PrototypeElement.removeClassName(element, 'invis');
PrototypeElement.addClassName(element, 'quarter');
}
else if(zoom < 1){
PrototypeElement.removeClassName(element, 'quarter');
PrototypeElement.addClassName(element, 'invis');
}
// i hate this.
if(GW2MapUtil.in_array(layer, ['region_label','map_label','sector_label'])){
Object.keys(element.children).forEach(c => {
let e = element.children[c];
let origin = window.getComputedStyle(e).perspectiveOrigin.split(' ');
e.style.left = '-'+origin[0];
e.style.top = '-'+origin[1];
});
}
});
}
/**
* parse and render additional GW2 event data
*
* @link https://github.com/arenanet/api-cdi/pull/61
*
* @protected
*/
_renderEvents(){
this._request(this.options.apiBase + '/v1/event_details.json?lang=' + this.dataset.language, event_details => {
this._request(this.options.apiBase + '/v1/maps.json?lang=' + this.dataset.language, maps => {
let eventGeoJSON = new GW2EventGeoJSON(event_details.events, maps.maps, this.floorGeoJSON.maps).getData();
let panes = Object.keys(eventGeoJSON.featureCollections);
let initLayers = this.dataset.initLayers || this.options.initLayers || panes;
panes.forEach(pane => {this._createPane(eventGeoJSON.featureCollections[pane].getJSON(), pane, initLayers)});
});
});
}
/**
* creates a layer pane and adds data to it
*
* @param {GW2FloorGeoJSON[]} geojson
* @param {string} pane
* @param {string[]}initLayers
* @protected
*/
_createPane(geojson, pane, initLayers){
let name = '<span class="gw2map-layer-control '+pane+'"> </span> ' + GW2MAP_I18N.layers[pane];
// create the pane if it doesn't exist
if(!this.layers[pane]){
this.layers[pane] = L.geoJson(geojson, {
pane : this.map.createPane(pane),
coordsToLatLng: coords => this._p2ll(coords),
pointToLayer : (feature, coords) => this._pointToLayer(feature, coords, pane),
onEachFeature : (feature, layer) => this._onEachFeature(feature, layer, pane),
style : (feature) => this._layerStyle(feature, pane),
});
this.controls.addOverlay(this.layers[pane], name)
}
// otherwise just add the data
else{
this.layers[pane].addData(geojson);
}
// optionally show that layer on the map
if(GW2MapUtil.in_array(pane, initLayers)){
this.layers[pane].addTo(this.map);
}
}
/**
* prepares the infobox/popup content
*
* @link http://leafletjs.com/reference-1.5.0.html#geojson-oneachfeature
* @param {*} feature
* @param {L.Layer} layer
* @param {string} pane
* @protected
*/
_onEachFeature(feature, layer, pane){
let p = feature.properties;
let content = '';
// no popup for event circles
// if(p.layertype === 'poly' && p.type === 'event'){
// return;
// }
if(p.layertype === 'icon'){
content += p.icon
? '<img class="gw2map-popup-icon gw2map-layer-control" src="'+ p.icon +'" alt="'+ p.name +'"/>'
: '<span class="gw2map-layer-control '+pane+'" ></span>';
}
if(p.name){
if(!GW2MapUtil.in_array(p.type, ['vista'])){
//noinspection RegExpRedundantEscape
let wikiname = p.name.toString()
.replace(/\.$/, '')
.replace(/\s/g, '_')
.replace(/\<br\>/g, '_')
.replace(/(Mount\:_|Raid—)/, ''); // @todo: i18n
// remove trailing bracket suffixes
let displayname = p.name.toString()
.replace(/ \((.*?)\)/,'');
content += '<a class="gw2map-wikilink" href="'
+ GW2MAP_I18N.wiki+encodeURIComponent(wikiname)
+ '" target="_blank">' + displayname + '</a>';
}
else{
content += p.name;
}
}
if(p.level){
content += ' (' + p.level + ')';
}
else if(p.min_level && p.max_level){
content += ' (' + (p.min_level === p.max_level ? p.max_level : p.min_level + '-' + p.max_level) + ')';
}
if(p.chat_link){
if(content){
content += '<br>';
}
content += '<input class="gw2map-chatlink" type="text" value="' + p.chat_link
+ '" readonly="readonly" onclick="this.select();return false;" />';
}
if(p.description){
if(content){
content += '<br>';
}
content += '<div class="gw2map-description">' + this._parseWikilinks(p.description) + '</div>';
}
if(content){
layer.bindPopup(content);
}
if(this.dataset.linkbox){
this._linkboxItem(feature, layer, pane)
}
}
/**
* a simple parser that allows creating links in popup texts using wikicode: [[article]] and [[article|name]]
*
* @param {string} str
* @returns {string}
* @protected
*/
_parseWikilinks(str){
// noinspection RegExpRedundantEscape
return str
.replace(/\[\[([^\]\|]+)\]\]/gi, '<a href="'+GW2MAP_I18N.wiki+'$1" target="_blank">$1</a>')
.replace(/\[\[([^\|]+)(\|)([^\]]+)\]\]/gi, '<a href="'+GW2MAP_I18N.wiki+'$1" target="_blank">$3</a>');
}
/**
* creates a clickable navigation item for the optional linkbox
*
* @param {*} feature
* @param {L.Layer} layer
* @param {string} pane
* @protected
*/
_linkboxItem(feature, layer, pane){
let p = feature.properties;
if(GW2MapUtil.in_array(pane, this.linkboxExclude) || p.mapID === -1){
return;
}
let navid = 'gw2map-navbox-map-'+p.mapID;
let nav = document.getElementById(navid);
if(!nav){
nav = document.createElement('div');
nav.id = navid;
nav.className = 'gw2map-navbox';
this.linkbox.appendChild(nav);
}
let paneContentID = 'gw2map-navbox-'+p.mapID+'-'+pane;
let paneContent = document.getElementById(paneContentID);
if(!paneContent && pane !== 'map_label'){
paneContent = document.createElement('div');
paneContent.id = paneContentID;
nav.appendChild(paneContent);
}
let item = document.createElement('span');
if(pane !== 'map_label'){
item.innerHTML = '<span class="gw2map-layer-control '+ pane +'"></span>';
}
item.innerHTML += (p.name || p.id || '-');
if(typeof layer.getLatLng === 'function'){
item.addEventListener('click', ev => {
let latlng = layer.getLatLng();
this.map
.panTo(latlng)
.openPopup(layer.getPopup(), latlng);
});
// insert the map label as first item
pane === 'map_label'
? nav.insertBefore(item, nav.firstChild)
: paneContent.appendChild(item);
}
}
/**
* handle layer icons/markers
*
* @link http://leafletjs.com/reference-1.5.0.html#geojson-pointtolayer
* @param {*} feature
* @param {LatLng} coords
* @param {string} pane
* @protected
*/
_pointToLayer(feature, coords, pane){
let icon;
let p = feature.properties;
if(p.layertype === 'poly' && p.type === 'event'){
return new L.Circle(coords, feature.properties.radius);
}
let iconParams = {
pane: pane,
iconSize : null,
popupAnchor: 'auto',
// temporarily adding the "completed" classname
// https://discordapp.com/channels/384735285197537290/384735523521953792/623750587921465364
className: 'gw2map-' + p.layertype + ' gw2map-' + p.type + '-' + p.layertype + ' completed'
};
if(p.icon){
iconParams.iconUrl = p.icon;
if(p.className){
iconParams.className += ' '+p.className;
}
icon = L.icon(iconParams);
}
else if(p.layertype === 'label'){
iconParams.html = p.name;
iconParams.iconAnchor = 'auto';
icon = new L.LabelIcon(iconParams);
return new L.LabelMarker(coords, {
pane: pane,
title: p.name,
icon: icon
});
}
else{
if(p.type === 'masterypoint'){
iconParams.className += ' ' + p.region.toLowerCase()
}
else if(p.type === 'heropoint'){
iconParams.className += p.id.split('-')[0] === '0' ? ' core' : ' expac';
}
icon = L.divIcon(iconParams);
}
return L.marker(coords, {
pane: pane,
title: p.layertype === 'icon' ? p.name : null,
icon: icon
});
}
/**
* @link http://leafletjs.com/reference-1.5.0.html#geojson-style
* @param {*} feature
* @param {string} pane
* @protected
*/
_layerStyle(feature, pane){
let p = feature.properties;
if(GW2MapUtil.in_array(pane, ['region_poly', 'map_poly', 'sector_poly', 'task_poly', 'event_poly'])){
return {
pane: pane,
stroke: true,
opacity: 0.6,
color: this.options.colors[pane] || 'rgb(255, 255, 255)',
weight: 2,
interactive: false,
}
}
return {
pane: pane,
stroke: true,
opacity: 0.6,
color: p.color || 'rgb(255, 255, 255)',
weight: 3,
interactive: true,
}
}
/**
* @param {[*,*]} coords
* @returns {LatLng}
* @protected
*/
_p2ll(coords){
return this.map.unproject(coords, this.options.maxZoom);
}
/**
* @param {[*,*]} coords
* @param {number} zoom
* @returns {[*,*]}
* @protected
*/
_project(coords, zoom){
return coords.map(c => Math.floor((c / (1 << (this.options.maxZoom - zoom))) / 256));
}
/**
* @param {[*,*]} coords
* @param {number} zoom
* @returns {string}
* @protected
*/
_tileGetter(coords, zoom){
let clamp = this.viewRect.map(c => this._project(c, zoom));
let ta = this.dataset.tileAdjust;
if(
coords.x < clamp[0][0] - ta
|| coords.x > clamp[1][0] + ta
|| coords.y < clamp[0][1] - ta
|| coords.y > clamp[1][1] + ta
){
return this.options.errorTile;
}
return this.options.tileBase
+ this.dataset.continentId + '/'
+ (this.dataset.customFloor || this.dataset.floorId) + '/'
+ zoom + '/' + coords.x + '/' + coords.y + this.options.tileExt;
}
}
class GW2MapLocal extends GW2Map{
localTileZoomedRects = {};
constructor(container, id, options){
super(container, id, options);
// pre-calculate zoomed/projected rects for local tiles
for(let z = this.options.minZoom; z <= this.options.maxZoom; z++){
this.localTileZoomedRects[z] = this.options.localTileRects.map(r => r.map(c => this._project(c, z)));
}
}
_tileGetter(coords, zoom){
let clamp = this.viewRect.map(c => this._project(c, zoom));
let ta = this.dataset.tileAdjust;
if(
coords.x < clamp[0][0] - ta
|| coords.x > clamp[1][0] + ta
|| coords.y < clamp[0][1] - ta
|| coords.y > clamp[1][1] + ta
){
return this.options.errorTile;
}
let floor = (this.dataset.customFloor || this.dataset.floorId);
// allow custom local tiles to be used direct from the wiki
for(let i = 0; i < this.localTileZoomedRects[zoom].length; i++){
clamp = this.localTileZoomedRects[zoom][i];
let file = 'World_map_tile_C' + this.dataset.continentId;
if(GW2MapUtil.in_array(floor, [1,2,3,4]) && !(
coords.x < clamp[0][0]
|| coords.x > clamp[1][0]
|| coords.y < clamp[0][1]
|| coords.y > clamp[1][1]
)){
file += ('_Z' + zoom + '_X' + coords.x + '_Y' + coords.y + '.jpg');
let md5file = this.md5(file);
return 'https://wiki.gw2.org.cn/images/' + md5file.slice(0,1) + '/'+ md5file.slice(0,2) + '/' + file;
}
}
return this.options.tileBase + this.dataset.continentId
+ '/' + floor + '/' + zoom + '/' + coords.x + '/' + coords.y + this.options.tileExt;
}
/**
* @link https://locutus.io/php/md5/
*
* @param str
* @returns {string}
*/
md5(str){
let hash, xl;
let _rotateLeft = function(lValue, iShiftBits){
return (lValue << iShiftBits)|(lValue >>> (32 - iShiftBits));
};
let _addUnsigned = function(lX, lY){
let lX4, lY4, lX8, lY8, lResult;
lX8 = (lX&0x80000000);
lY8 = (lY&0x80000000);
lX4 = (lX&0x40000000);
lY4 = (lY&0x40000000);
lResult = (lX&0x3FFFFFFF) + (lY&0x3FFFFFFF);
if(lX4&lY4){
return (lResult^0x80000000^lX8^lY8);
}
if(lX4|lY4){
if(lResult&0x40000000){
return (lResult^0xC0000000^lX8^lY8);
}
else{
return (lResult^0x40000000^lX8^lY8);
}
}
else{
return (lResult^lX8^lY8);
}
};
let _F = function(x, y, z){
return (x&y)|((~x)&z);
};
let _G = function(x, y, z){
return (x&z)|(y&(~z));
};
let _H = function(x, y, z){
return (x^y^z);
};
let _I = function(x, y, z){
return (y^(x|(~z)));
};
let _FF = function(a, b, c, d, x, s, ac){
a = _addUnsigned(a, _addUnsigned(_addUnsigned(_F(b, c, d), x), ac));
return _addUnsigned(_rotateLeft(a, s), b);
};
let _GG = function(a, b, c, d, x, s, ac){
a = _addUnsigned(a, _addUnsigned(_addUnsigned(_G(b, c, d), x), ac));
return _addUnsigned(_rotateLeft(a, s), b);
};
let _HH = function(a, b, c, d, x, s, ac){
a = _addUnsigned(a, _addUnsigned(_addUnsigned(_H(b, c, d), x), ac));
return _addUnsigned(_rotateLeft(a, s), b);
};
let _II = function(a, b, c, d, x, s, ac){
a = _addUnsigned(a, _addUnsigned(_addUnsigned(_I(b, c, d), x), ac));
return _addUnsigned(_rotateLeft(a, s), b);
};
let _convertToWordArray = function(str){
let lWordCount;
let lMessageLength = str.length;
let lNumberOfWordsTemp1 = lMessageLength + 8;
let lNumberOfWordsTemp2 = (lNumberOfWordsTemp1 - (lNumberOfWordsTemp1 % 64)) / 64;
let lNumberOfWords = (lNumberOfWordsTemp2 + 1) * 16;
let lWordArray = new Array(lNumberOfWords - 1);
let lBytePosition = 0;
let lByteCount = 0;
while(lByteCount < lMessageLength){
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = (lWordArray[lWordCount]|(str.charCodeAt(lByteCount) << lBytePosition));
lByteCount++;
}
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = lWordArray[lWordCount]|(0x80 << lBytePosition);
lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
return lWordArray;
};
let _wordToHex = function(lValue){
let wordToHexValue = '';
let wordToHexValueTemp = '';
let lByte;
let lCount;
for(lCount = 0; lCount <= 3; lCount++){
lByte = (lValue >>> (lCount * 8))&255;
wordToHexValueTemp = '0' + lByte.toString(16);
wordToHexValue = wordToHexValue + wordToHexValueTemp.substr(wordToHexValueTemp.length - 2, 2);
}
return wordToHexValue;
};
let k, AA, BB, CC, DD, a, b, c, d;
let S11 = 7;
let S12 = 12;
let S13 = 17;
let S14 = 22;
let S21 = 5;
let S22 = 9;
let S23 = 14;
let S24 = 20;
let S31 = 4;
let S32 = 11;
let S33 = 16;
let S34 = 23;
let S41 = 6;
let S42 = 10;
let S43 = 15;
let S44 = 21;
let x = _convertToWordArray(this.utf8_encode(str));
a = 0x67452301;
b = 0xEFCDAB89;
c = 0x98BADCFE;
d = 0x10325476;
xl = x.length;
for(k = 0; k < xl; k += 16){
AA = a;
BB = b;
CC = c;
DD = d;
a = _FF(a, b, c, d, x[k], S11, 0xD76AA478);
d = _FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
c = _FF(c, d, a, b, x[k + 2], S13, 0x242070DB);
b = _FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
a = _FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
d = _FF(d, a, b, c, x[k + 5], S12, 0x4787C62A);
c = _FF(c, d, a, b, x[k + 6], S13, 0xA8304613);
b = _FF(b, c, d, a, x[k + 7], S14, 0xFD469501);
a = _FF(a, b, c, d, x[k + 8], S11, 0x698098D8);
d = _FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
c = _FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
b = _FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
a = _FF(a, b, c, d, x[k + 12], S11, 0x6B901122);
d = _FF(d, a, b, c, x[k + 13], S12, 0xFD987193);
c = _FF(c, d, a, b, x[k + 14], S13, 0xA679438E);
b = _FF(b, c, d, a, x[k + 15], S14, 0x49B40821);
a = _GG(a, b, c, d, x[k + 1], S21, 0xF61E2562);
d = _GG(d, a, b, c, x[k + 6], S22, 0xC040B340);
c = _GG(c, d, a, b, x[k + 11], S23, 0x265E5A51);
b = _GG(b, c, d, a, x[k], S24, 0xE9B6C7AA);
a = _GG(a, b, c, d, x[k + 5], S21, 0xD62F105D);
d = _GG(d, a, b, c, x[k + 10], S22, 0x2441453);
c = _GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
b = _GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
a = _GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
d = _GG(d, a, b, c, x[k + 14], S22, 0xC33707D6);
c = _GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
b = _GG(b, c, d, a, x[k + 8], S24, 0x455A14ED);
a = _GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
d = _GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
c = _GG(c, d, a, b, x[k + 7], S23, 0x676F02D9);
b = _GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
a = _HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
d = _HH(d, a, b, c, x[k + 8], S32, 0x8771F681);
c = _HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
b = _HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
a = _HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
d = _HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
c = _HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
b = _HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
a = _HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
d = _HH(d, a, b, c, x[k], S32, 0xEAA127FA);
c = _HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
b = _HH(b, c, d, a, x[k + 6], S34, 0x4881D05);
a = _HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
d = _HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
c = _HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
b = _HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
a = _II(a, b, c, d, x[k], S41, 0xF4292244);
d = _II(d, a, b, c, x[k + 7], S42, 0x432AFF97);
c = _II(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
b = _II(b, c, d, a, x[k + 5], S44, 0xFC93A039);
a = _II(a, b, c, d, x[k + 12], S41, 0x655B59C3);
d = _II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
c = _II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
b = _II(b, c, d, a, x[k + 1], S44, 0x85845DD1);
a = _II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
d = _II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
c = _II(c, d, a, b, x[k + 6], S43, 0xA3014314);
b = _II(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
a = _II(a, b, c, d, x[k + 4], S41, 0xF7537E82);
d = _II(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
c = _II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
b = _II(b, c, d, a, x[k + 9], S44, 0xEB86D391);
a = _addUnsigned(a, AA);
b = _addUnsigned(b, BB);
c = _addUnsigned(c, CC);
d = _addUnsigned(d, DD);
}
let temp = _wordToHex(a) + _wordToHex(b) + _wordToHex(c) + _wordToHex(d);
return temp.toLowerCase();
}
/**
* @link https://locutus.io/php/utf8_encode/
*
* @param argString
* @returns {string}
*/
utf8_encode(argString){
if(argString === null || typeof argString === 'undefined'){
return '';
}
// .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
let string = (argString + '');
let utftext = '';
let start;
let end;
start = end = 0;
let stringl = string.length;
for(let n = 0; n < stringl; n++){
let c1 = string.charCodeAt(n);
let enc = null;
if(c1 < 128){
end++;
}
else if(c1 > 127 && c1 < 2048){
enc = String.fromCharCode(
(c1 >> 6)|192, (c1&63)|128,
);
}
else if((c1&0xF800) !== 0xD800){
enc = String.fromCharCode(
(c1 >> 12)|224, ((c1 >> 6)&63)|128, (c1&63)|128,
);
}
else{
// surrogate pairs
if((c1&0xFC00) !== 0xD800){
throw new RangeError('Unmatched trail surrogate at ' + n);
}
let c2 = string.charCodeAt(++n);
if((c2&0xFC00) !== 0xDC00){
throw new RangeError('Unmatched lead surrogate at ' + (n - 1));
}
c1 = ((c1&0x3FF) << 10) + (c2&0x3FF) + 0x10000;
enc = String.fromCharCode(
(c1 >> 18)|240, ((c1 >> 12)&63)|128, ((c1 >> 6)&63)|128, (c1&63)|128,
);
}
if(enc !== null){
if(end > start){
utftext += string.slice(start, end);
}
utftext += enc;
start = end = n + 1;
}
}
if(end > start){
utftext += string.slice(start, stringl);
}
return utftext;
}
}
/**
* Class GW2MapDataset
*
* reads the dataset from the container element, validates and stores the values in this.dataset
*
* i hate all of this.
*/
class GW2MapDataset{
//noinspection RegExpRedundantEscape
metadata = {
continentId : {type: 'int', default: 1},
floorId : {type: 'int', default: 1},
regionId : {type: 'int', default: null},
mapId : {type: 'int', default: null},
customFloor : {type: 'int', default: null},
language : {type: 'int', default: null},
zoom : {type: 'int', default: -1},
maxZoom : {type: 'int', default: 7},
tileAdjust : {type: 'int', default: 0},
mapControls : {type: 'bool', default: true},
linkbox : {type: 'bool', default: false},
events : {type: 'bool', default: false},
initLayers : {type: 'array', default: null, regex: /^([a-z_,\s]+)$/i},
extraLayers : {type: 'array', default: [], regex: /^([a-z_,\s]+)$/i},
centerCoords: {type: 'array', default: null, regex: /^([\[\]\s\d\.,]+)$/},
customRect : {type: 'array', default: null, regex: /^([\[\]\s\d\.,]+)$/},
includeMaps : {type: 'array', default: [], regex: /^([\s\d,]+)$/},
};
dataset = {};
/**
* @param {Object} dataset
* @param {Object} options
*/
constructor(dataset, options){
this.options = options;
this._parse(dataset);
}
/**
* @returns {Object}
*/
getData(){
return this.dataset;
}
/**
* @param {Object} dataset
* @private
*/
_parse(dataset){
Object.keys(this.metadata).forEach(k => {
if(typeof dataset[k] === 'undefined' || dataset[k] === ''){
this.dataset[k] = this.metadata[k].default;
}
else{
['int', 'bool', 'array', 'string'].forEach(t => {
if(this.metadata[k].type === t){
this.dataset[k] = this['_parse_'+t](dataset[k], this.metadata[k]);
}
});
}
if(typeof this['_parse_'+k] === 'function'){
this.dataset[k] = this['_parse_'+k](this.dataset[k], this.metadata[k]);
}
});
}
/**
* @param {Object} data
* @returns {number}
* @private
*/
_parse_int(data){
return GW2MapUtil.intval(data);
}
/**
* @param {Object} data
* @returns {boolean}
* @private
*/
_parse_bool(data){
return GW2MapUtil.in_array(data.toLowerCase(), ['1', 'true', 't', 'yes', 'y']);
}
/**
* @param {Object} data
* @param {Object} meta
* @returns {*}
* @private
*/
_parse_array(data, meta){
let match = data.match(meta.regex);
if(match){
return match
}
return meta.default;
}
/**
* @param {Object} data
* @param {Object} meta
* @returns {*}
* @private
*/
_parse_string(data, meta){
return this._parse_array(data, meta);
}
/**
* @param {Object} data
* @param {Object} meta
* @returns {number}
* @private
*/
_parse_continentId(data, meta){
return GW2MapUtil.in_array(data, [1, 2]) ? data : meta.default;
}
/**
* @param {Object} data
* @param {Object} meta
* @returns {number}
* @private
*/
_parse_regionId(data, meta){
return data > 0 ? data : meta.default;
}
/**
* @param {Object} data
* @param {Object} meta
* @returns {number}
* @private
*/
_parse_mapId(data, meta){
return data > 0 ? data : meta.default;
}
/**
* @param {Object} data
* @param {Object} meta
* @returns {string}
* @private
*/
_parse_language(data, meta){
return ['de', 'en', 'es', 'fr', 'zh'][data] || this.options.lang;
}
/**
* @param {Object} data
* @returns {number}
* @private
*/
_parse_zoom(data){
return data < this.options.minZoom || data > this.options.maxZoom ? this.options.defaultZoom : data
}
/**
* @param {Object} data
* @param {Object} meta
* @returns {number|null}
* @private
*/
_parse_maxZoom(data, meta){
return GW2MapUtil.in_array(data, [6, 7]) ? data : meta.default;
}
/**
* @param {Object} data
* @param {Object} meta
* @returns {[]}
* @private
*/
_parse_includeMaps(data, meta){
if(data === meta.default){
return data;
}
let ret = [];
data[0].replace(/[^\d,]/g, '').split(',').forEach(v => {
if(v){
ret.push(GW2MapUtil.intval(v));
}
});
return ret
}
/**
* @param {Object} data
* @param {Object} meta
* @returns {number[][]}
* @private
*/
_parse_customRect(data, meta){
if(data === meta.default){
return data;
}
data = JSON.parse(data[0]);
if(data.length < 2 || data[0].length < 2 || data[1].length < 2){
return meta.default;
}
return data;
}
/**
* @param {Object} data
* @param {Object} meta
* @returns {number[]}
* @private
*/
_parse_centerCoords(data, meta){
if(data === meta.default){
return data;
}
data = JSON.parse(data[0]);
if(data.length < 2 || typeof data[0] !== 'number' || typeof data[1] !== 'number'){
return meta.default;
}
return data;
}
/**
* @param {Object} data
* @param {Object} meta
* @returns {string[]}
* @private
*/
_parse_extraLayers(data, meta){
if(data === meta.default){
return data;
}
let ret = [];
data[0].replace(/\s/g, '').split(',').forEach(v => {
if(v){
ret.push(v.toLowerCase());
}
});
return ret;
}
/**
* @param {Object} data
* @param {Object} meta
* @returns {string[]}
* @private
*/
_parse_initLayers(data, meta){
return this._parse_extraLayers(data, meta);
}
/**
* @param {Object} data
* @returns {number}
* @private
*/
_parse_tileAdjust(data){
return data < 0 ? 0 : data;
}
}
/**
* Class GW2MapUtil
*/
class GW2MapUtil{
/**
* @param {Object} target
* @param {Object} source
* @returns {Object}
*/
static extend(target, source) {
for(let property in source) {
// eslint-disable-next-line no-prototype-builtins
if(source.hasOwnProperty(property)) {
target[property] = source[property];
}
}
return target;
}
/**
* @link http://locutus.io/php/var/intval/
*
* @param {*} mixed_var
* @param {number} base
* @returns {*}
*/
static intval(mixed_var, base){
let tmp;
let type = typeof(mixed_var);
if(type === 'boolean'){
return +mixed_var;
}
else if(type === 'string'){
tmp = parseInt(mixed_var, base || 10);
return (isNaN(tmp) || !isFinite(tmp)) ? 0 : tmp;
}
else if(type === 'number' && isFinite(mixed_var)){
return mixed_var|0;
}
else{
return 0;
}
}
/**
* @param {*} needle
* @param {*} haystack
* @returns {boolean}
*/
static in_array(needle, haystack){
for(let key in haystack){
// eslint-disable-next-line no-prototype-builtins
if(haystack.hasOwnProperty(key)){
if(haystack[key] === needle){
return true;
}
}
}
return false;
}
}
/**
* Class GW2GeoJSONAbstract
*/
class GW2GeoJSONAbstract{
featureCollections = {};
includeMaps = [];
constructor(includeMaps){
this.includeMaps = includeMaps;
}
/**
* @param {string} layer
* @param {string|number} id
* @param {number} mapID
* @param {string} name
* @param {*} properties
* @param {*} geometry
* @param {string} [geometryType]
* @returns {GW2FloorGeoJSON}
* @protected
*/
_addFeature(layer, id, mapID, name, properties, geometry, geometryType){
if(!this.featureCollections[layer]){
this.featureCollections[layer] = new GeoJSONFeatureCollection();
}
this.featureCollections[layer]
.addFeature(GW2MapUtil.extend({
name : (name || ''),
mapID : mapID,
layertype: 'icon',
}, properties))
.setID(id)
.setGeometry(geometry, geometryType)
;
return this;
}
}
/**
* Class GW2FloorGeoJSON
*
* polyfill for https://github.com/arenanet/api-cdi/pull/62
*/
class GW2FloorGeoJSON extends GW2GeoJSONAbstract{
floordata = {};
maps = [];
/**
* GW2FloorGeoJSON constructor
*
* @param {*} floordata
* @param {[[],[]]} customRect
* @param {string[]} extraMarkers
* @param {number[]} includeMaps
*/
constructor(floordata, customRect, extraMarkers, includeMaps){
super(includeMaps);
this.floordata = floordata;
this.extraMarkers = ['adventure_icon', 'jumpingpuzzle_icon', 'polylines'].concat(extraMarkers);
this.setView(customRect);
}
/**
* @returns {GW2FloorGeoJSON}
*/
setView(customRect){
if(customRect){
this.viewRect = customRect; // @todo
}
else if(this.floordata.continent_rect){
this.viewRect = this.floordata.continent_rect;
}
else if(this.floordata.clamped_view){
this.viewRect = this.floordata.clamped_view;
}
else if(this.floordata.texture_dims){
this.viewRect = [[0, 0], this.floordata.texture_dims];
}
else{
this.viewRect = [[0, 0], [49152, 49152]];
}
return this;
}
/**
* @returns {*}
*/
getData(){
// a response to floors
if(this.floordata.regions){
this.continent(this.floordata.regions);
}
// a regions response
else if(this.floordata.maps){
this.region(this.floordata);
}
// an actual map response
else if(this.floordata.points_of_interest){
this.map(this.floordata);
}
return {
viewRect: this.viewRect,
featureCollections: this.featureCollections,
};
}
/**
* @param {*} continent
* @returns {GW2FloorGeoJSON}
*/
continent(continent){
Object.keys(continent).forEach(regionID => this.region(continent[regionID]));
return this;
}
/**
* @param {*} region
* @returns {GW2FloorGeoJSON}
*/
region(region){
this._addFeature('region_label', region.id, -1, region.name, {
type : 'region',
layertype: 'label',
}, region.label_coord);
/*
this._addFeature('region_poly', region.id, -1, region.name, {
type : 'region',
layertype: 'poly',
}, new GW2ContinentRect(region.continent_rect).getPoly(), 'Polygon');
*/
Object.keys(region.maps).forEach(mapID => {
let map = region.maps[mapID];
map.id = GW2MapUtil.intval(mapID);
// console.log('map', map.id, map.name);
// @todo
if(this.includeMaps.length > 0){
if(!GW2MapUtil.in_array(map.id, this.includeMaps)){
return this;
}
}
this.map(map);
});
return this;
}
/**
* @param {*} map
* @returns {GW2FloorGeoJSON}
*/
map(map){
this.maps.push(map.id);
let rect = new GW2ContinentRect(map.continent_rect);
// https://github.com/arenanet/api-cdi/issues/334
this._addFeature('map_label', map.id, map.id, map.name, {
min_level : map.min_level,
max_level : map.max_level,
type : 'map',
layertype : 'label',
}, map.label_coord || rect.getCenter());
/*
this._addFeature('map_poly', map.id, map.id, map.name, {
type : 'map',
layertype: 'poly',
}, rect.getPoly(), 'Polygon');
*/
this
.sectors(map.sectors, map.id)
.poi(map.points_of_interest, map.id)
.task(map.tasks, map.id)
.heropoint(map.skill_challenges, map.id)
.masteryPoint(map.mastery_points, map.id)
.adventure(map.adventures || [], map.id)
;
if(this.extraMarkers.length){
this.extraMarkers.forEach(layer => {
if(!GW2W_EXTRA_DATA[layer] || !GW2W_EXTRA_DATA[layer].data[map.id]){
return;
}
this.extra(GW2W_EXTRA_DATA[layer], layer, map.id);
});
}
return this;
}
/**
* @param {*} extra
* @param {string} layer
* @param {number} mapID
* @returns {GW2FloorGeoJSON}
*/
extra(extra, layer, mapID){
extra.data[mapID].forEach(e => {
let extraOptions = {
icon : e.icon || extra.icon || null,
className : extra.className,
type : extra.type,
color : e.color || extra.color,
layertype : e.layertype || extra.layertype || 'icon',
description: e.description || extra.description || null
};
if(e.antPath || extra.antPath){
extraOptions.antPath = e.antPath || extra.antPath;
extraOptions.antColor = e.antColor || extra.antColor;
extraOptions.antOpacity = e.antOpacity || extra.antOpacity;
extraOptions.antDashArray = e.antDashArray || extra.antDashArray;
}
this._addFeature(
layer,
e.id,
mapID,
(e.name || extra.name),
extraOptions,
e.coord,
(e.featureType ||extra.featureType || 'Point')
);
});
}
/**
* @param {*} sectors
* @param {number} mapID
* @returns {GW2FloorGeoJSON}
*/
sectors(sectors, mapID){
Object.keys(sectors).forEach(sectorId =>{
let sector = sectors[sectorId];
if(GW2W_SECTOR_NAMES[sectorId]){
sector.name = GW2W_SECTOR_NAMES[sectorId];
}
this._addFeature('sector_label', sector.id, mapID, sector.name, {
chat_link: sector.chat_link,
level : sector.level,
type : 'sector',
layertype: 'label',
}, sector.coord);
this._addFeature('sector_poly', sector.id, mapID, sector.name, {
type : 'sector',
layertype: 'poly',
}, [sector.bounds], 'Polygon');
});
return this;
}
/**
* @param {*} pois
* @param {number} mapID
* @returns {GW2FloorGeoJSON}
*/
poi(pois, mapID){
Object.keys(pois).forEach(poiID =>{
let poi = pois[poiID];
if(GW2W_POIDATA[poi.type] && GW2W_POIDATA[poi.type][poiID]){
poi = GW2MapUtil.extend(poi, GW2W_POIDATA[poi.type][poiID]);
}
this._addFeature(poi.type + '_icon', poi.id || null, mapID, null, {
name : poi.name || poi.id || '',
type : poi.type,
chat_link: poi.chat_link || false,
// floor : poi.floor, // ???
icon : poi.icon
}, poi.coord);
});
return this;
}
/**
* @param {*} tasks
* @param {number} mapID
* @returns {GW2FloorGeoJSON}
*/
task(tasks, mapID){
Object.keys(tasks).forEach(taskID =>{
let task = tasks[taskID];
this._addFeature('task_icon', task.id, mapID, task.objective, {
level : task.level,
type : 'task',
}, task.coord);
this._addFeature('task_poly', task.id, mapID, task.objective, {
type : 'task',
layertype: 'poly',
}, [task.bounds], 'Polygon');
});
return this;
}
/**
* @param {*} heropoints
* @param {number} mapID
* @returns {GW2FloorGeoJSON}
*/
heropoint(heropoints, mapID){
if(!heropoints.length){
return this;
}
heropoints.forEach(heropoint =>{
// https://github.com/arenanet/api-cdi/issues/329
this._addFeature('heropoint_icon', heropoint.id, mapID, null, {
name : GW2W_HEROPOINT_NAMES[heropoint.id] || '',
type : 'heropoint',
}, heropoint.coord)
});
return this;
}
/**
* @param {*} masterypoints
* @param {number} mapID
* @returns {GW2FloorGeoJSON}
*/
masteryPoint(masterypoints, mapID){
if(!masterypoints.length){
return this;
}
masterypoints.forEach(masterypoint =>{
this._addFeature('masterypoint_icon', masterypoint.id, mapID, null, {
name : GW2W_MASTERYPOINT_NAMES[masterypoint.id] || '',
region : masterypoint.region,
type : 'masterypoint',
}, masterypoint.coord)
});
return this;
}
/**
* @param {*} adventures
* @param {number} mapID
* @returns {GW2FloorGeoJSON}
*/
adventure(adventures, mapID){
if(!adventures.length){
return this;
}
adventures.forEach(adventure =>{
this._addFeature('adventure_icon', null, mapID, adventure.name, {
description: adventure.description || '',
type : 'adventure',
}, adventure.coord);
});
return this;
}
}
/**
* Class GW2EventGeoJSON
*/
class GW2EventGeoJSON extends GW2GeoJSONAbstract{
event_details = {};
map_details = {};
map = {};
constructor(event_details, map_details, includeMaps){
super(includeMaps);
this.event_details = event_details;
this.map_details = map_details;
}
getData(){
Object.keys(this.event_details).forEach(id => {
let event = this.event_details[id];
if(!GW2MapUtil.in_array(event.map_id, this.includeMaps)){
delete this.event_details[id];
delete this.map_details[event.map_id];
return;
}
let map = this.map_details[event.map_id];
if(!this.map[event.map_id]){
this.map[event.map_id] = map;
this.map[event.map_id].rect = new GW2ContinentRect(map.continent_rect, map.map_rect);
}
map = this.map[event.map_id];
this._addFeature('event_icon', id, event.map_id, event.name, {
icon : event.icon ? 'https://render.guildwars2.com/file/'+event.icon.signature+'/'+event.icon.file_id+'.png' : null,
flags : event.flags,
type : 'event',
layertype: 'icon',
}, map.rect.scaleCoords(event.location.center));
if(event.location.type === 'poly'){
this._addFeature('event_poly', id, event.map_id, event.name, {
type : 'event',
layertype: 'poly',
}, [event.location.points.map(point => map.rect.scaleCoords(point))], 'Polygon');
}
else{
this._addFeature('event_poly', id, event.map_id, event.name, {
type : 'event',
layertype: 'poly',
radius : map.rect.scaleLength(event.location.radius),
}, map.rect.scaleCoords(event.location.center), 'Point');
}
});
return {
featureCollections: this.featureCollections,
};
}
}
/**
* Class GW2ContinentRect
*/
class GW2ContinentRect{
/**
* GW2ContinentRect constructor
*
* @param continent_rect
* @param map_rect
*/
constructor(continent_rect, map_rect){
this.rect = continent_rect;
this.map_rect = map_rect;
}
/**
* returns bounds for L.LatLngBounds()
*
* @returns {*[]}
*/
getBounds(){
return [
[this.rect[0][0], this.rect[1][1]],
[this.rect[1][0], this.rect[0][1]]
]
}
/**
* returns the center of the rectangle
*
* @returns {*[]}
*/
getCenter(){
return [
(this.rect[0][0] + this.rect[1][0]) / 2,
(this.rect[0][1] + this.rect[1][1]) / 2
]
}
/**
* returns a polygon made of the rectangles corners
*
* @returns {*[]}
*/
getPoly(){
return [[
[this.rect[0][0], this.rect[0][1]],
[this.rect[1][0], this.rect[0][1]],
[this.rect[1][0], this.rect[1][1]],
[this.rect[0][0], this.rect[1][1]]
]]
}
/**
* @param {[]} coords from event_details.json or Mumble Link data.
* @param {[]} [mr] map_rect taken from maps.json or map_floor.json
* @returns {*[]}
*/
scaleCoords(coords, mr){
mr = this.map_rect || mr;
return [
Math.round(this.rect[0][0]+(this.rect[1][0]-this.rect[0][0])*(coords[0]-mr[0][0])/(mr[1][0]-mr[0][0])),
Math.round(this.rect[0][1]+(this.rect[1][1]-this.rect[0][1])*(1-(coords[1]-mr[0][1])/(mr[1][1]-mr[0][1])))
]
}
/**
* @param {number} length from event_details.json or Mumble Link data
* @param {[]} [map_rect] taken from maps.json or map_floor.json
* @returns {number}
*/
scaleLength(length, map_rect){
// still unsure about the correct values here
length = length / (1/24);
map_rect = this.map_rect || map_rect;
let scalex = (length - map_rect[0][0]) / (map_rect[1][0] - map_rect[0][0]);
let scaley = (length - map_rect[0][1]) / (map_rect[1][1] - map_rect[0][1]);
return Math.sqrt((scalex * scalex) + (scaley * scaley));
}
}
/**
* Class GeoJSONFeatureCollection
*/
class GeoJSONFeatureCollection{
/**
* GeoJSONFeatureCollection constructor
*/
constructor(){
this.json = {
type: 'FeatureCollection',
features: [],
};
}
/**
* @returns {{type: string, features: Array}|*}
*/
getJSON(){
this.json.features.forEach((feature, i) => this.json.features[i] = feature.getJSON());
return this.json;
}
/**
* @param type
* @param properties
* @returns {GeoJSONFeatureCollection}
*/
setCRS(type, properties){
this.json.crs = {
type: type,
properties: properties,
};
return this;
}
/**
* @param properties
* @returns {GeoJSONFeature}
*/
addFeature(properties){
let feature = new GeoJSONFeature(properties);
this.json.features.push(feature);
return feature;
}
}
/**
* Class GeoJSONFeature
*/
class GeoJSONFeature{
/**
* GeoJSONFeature constructor
*
* @param properties
*/
constructor(properties){
this.json = {
type: 'Feature',
geometry: {
type : '',
coordinates: [],
},
properties: properties || {},
};
}
/**
* @returns {{type: string, geometry: {type: string, coordinates: Array}, properties: (*|{})}|*}
*/
getJSON(){
return this.json;
}
/**
* @param id
* @returns {GeoJSONFeature}
*/
setID(id){
if(id){
this.json.id = id; // gmaps
this.json.properties.id = id; // leaflet
}
return this;
}
/**
* @param coords
* @param type
* @returns {GeoJSONFeature}
*/
setGeometry(coords, type){
this.json.geometry.coordinates = coords;
this.json.geometry.type = GW2MapUtil.in_array(type, [
'Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon', 'GeometryCollection'
]) ? type : 'Point';
return this;
}
}
/**
* prototype DOM rewrite inc
* @link https://github.com/prototypejs/prototype/blob/master/src/prototype/dom/dom.js
*/
class PrototypeElement{
static addClassName(element, className){
if(!this.hasClassName(element, className)){
element.className += (element.className ? ' ' : '') + className;
}
return element;
}
static removeClassName(element, className){
element.className = element.className
.replace(this.getRegExpForClassName(className), ' ')
.replace(/^\s+/, '')
.replace(/\s+$/, '');
return element;
}
static toggleClassName(element, className, bool) {
if(typeof bool === 'undefined'){
bool = !this.hasClassName(element, className);
}
return this[bool ? 'addClassName' : 'removeClassName'](element, className);
}
static hasClassName(element, className){
let elementClassName = element.className;
if(elementClassName.length === 0){
return false;
}
if(elementClassName === className){
return true;
}
return this.getRegExpForClassName(className).test(elementClassName);
}
static getRegExpForClassName(className){
return new RegExp('(^|\\s+)' + className + '(\\s+|$)');
}
}
// invoke the maps
(($options, $containers) => {
$containers = $containers || document.getElementsByClassName($options.containerClassName);
// no map, no scripts.
if(!$containers.length){
return;
}
$options = GW2MapUtil.extend({
containerClassName: 'gw2map',
linkboxClassName : 'gw2map-linkbox',
navClassName : 'gw2map-nav',
scriptContainerId : 'gw2map-script',
localTiles : false,
localTileRects : [],
scripts:[
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.6.0/leaflet-src.js',
'https://wiki.gw2.org.cn/index.php?title=Widget:Map_floors/data&action=raw&ctype=text/javascript',
],
stylesheets: [
'https://wiki.gw2.org.cn/index.php?title=Widget:Map_floors/style&action=raw&ctype=text/css',
],
}, $options);
// scripts to <body>
$options.scripts.forEach(script => {
let s = document.getElementById($options.scriptContainerId);
let node = document.createElement('script');
node.src = script;
s.parentNode.insertBefore(node, s);
});
// stylesheets to the <head>
$options.stylesheets.forEach(stylesheet => {
let node = document.createElement('link');
node.rel = 'stylesheet';
node.href = stylesheet;
document.getElementsByTagName('head')[0].appendChild(node);
});
// ogogog
window.addEventListener('load', () => {
// check if leaflet is loaded (paranoid)
if(typeof L === 'undefined' || !L.version){
console.log('GW2Map error: leaflet not loaded!');
return;
}
// https://github.com/Leaflet/Leaflet.fullscreen
L.Control.Fullscreen = L.Control.extend({
options: {
position: 'topleft',
title : {
'false': 'View Fullscreen',
'true' : 'Exit Fullscreen',
},
},
onAdd: function(map){
let container = L.DomUtil.create('div', 'leaflet-control-fullscreen leaflet-bar leaflet-control');
this.link = L.DomUtil.create('a', 'leaflet-control-fullscreen-button leaflet-bar-part', container);
this.link.href = '#';
this._map = map;
this._map.on('fullscreenchange', this._toggleTitle, this);
this._toggleTitle();
L.DomEvent.on(this.link, 'click', this._click, this);
return container;
},
_click: function(e){
L.DomEvent.stopPropagation(e);
L.DomEvent.preventDefault(e);
this._map.toggleFullscreen(this.options);
},
_toggleTitle: function(){
this.link.title = this.options.title[this._map.isFullscreen()];
},
});
L.Map.include({
isFullscreen: function(){
return this._isFullscreen || false;
},
toggleFullscreen: function(options){
let container = this.getContainer();
if(this.isFullscreen()){
if(options && options.pseudoFullscreen){
this._disablePseudoFullscreen(container);
}
else if(document.exitFullscreen){
document.exitFullscreen();
}
else if(document.mozCancelFullScreen){
document.mozCancelFullScreen();
}
else if(document.webkitCancelFullScreen){
document.webkitCancelFullScreen();
}
else if(document.msExitFullscreen){
document.msExitFullscreen();
}
else{
this._disablePseudoFullscreen(container);
}
}
else{
if(options && options.pseudoFullscreen){
this._enablePseudoFullscreen(container);
}
else if(container.requestFullscreen){
container.requestFullscreen();
}
else if(container.mozRequestFullScreen){
container.mozRequestFullScreen();
}
else if(container.webkitRequestFullscreen){
container.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
}
else if(container.msRequestFullscreen){
container.msRequestFullscreen();
}
else{
this._enablePseudoFullscreen(container);
}
}
},
_enablePseudoFullscreen: function(container){
L.DomUtil.addClass(container, 'leaflet-pseudo-fullscreen');
this._setFullscreen(true);
this.fire('fullscreenchange');
},
_disablePseudoFullscreen: function(container){
L.DomUtil.removeClass(container, 'leaflet-pseudo-fullscreen');
this._setFullscreen(false);
this.fire('fullscreenchange');
},
_setFullscreen: function(fullscreen){
this._isFullscreen = fullscreen;
let container = this.getContainer();
if(fullscreen){
L.DomUtil.addClass(container, 'leaflet-fullscreen-on');
}
else{
L.DomUtil.removeClass(container, 'leaflet-fullscreen-on');
}
this.invalidateSize();
},
_onFullscreenChange: function(e){
let fullscreenElement =
document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement ||
document.msFullscreenElement;
if(fullscreenElement === this.getContainer() && !this._isFullscreen){
this._setFullscreen(true);
this.fire('fullscreenchange');
}
else if(fullscreenElement !== this.getContainer() && this._isFullscreen){
this._setFullscreen(false);
this.fire('fullscreenchange');
}
},
});
L.Map.mergeOptions({fullscreenControl: false});
L.Map.addInitHook(function(){
if(this.options.fullscreenControl){
this.fullscreenControl = new L.Control.Fullscreen(this.options.fullscreenControl);
this.addControl(this.fullscreenControl);
}
let fullscreenchange;
if('onfullscreenchange' in document){
fullscreenchange = 'fullscreenchange';
}
else if('onmozfullscreenchange' in document){
fullscreenchange = 'mozfullscreenchange';
}
else if('onwebkitfullscreenchange' in document){
fullscreenchange = 'webkitfullscreenchange';
}
else if('onmsfullscreenchange' in document){
fullscreenchange = 'MSFullscreenChange';
}
if(fullscreenchange){
let onFullscreenChange = L.bind(this._onFullscreenChange, this);
this.whenReady(function(){
L.DomEvent.on(document, fullscreenchange, onFullscreenChange);
});
this.on('unload', function(){
L.DomEvent.off(document, fullscreenchange, onFullscreenChange);
});
}
});
L.control.fullscreen = function(options){
return new L.Control.Fullscreen(options);
};
// coordinate view with selectable input (eases gw2wiki use)
L.Control.Coordview = L.Control.extend({
options: {
position: 'bottomleft',
},
onAdd: function(map){
let container = L.DomUtil.create('div', 'leaflet-control-coordview leaflet-control');
let input = L.DomUtil.create('input');
input.type = 'text';
input.placeholder = '<coords>';
input.readOnly = true;
container.appendChild(input);
L.DomEvent.disableClickPropagation(container);
L.DomEvent.on(input, 'click', ev => ev.target.select());
map.on('click', ev => {
let point = map.project(ev.latlng, map.options.maxZoom);
input.value = '['+Math.round(point.x)+', '+Math.round(point.y)+']';
// ckeckbox: copy to clipboard
// navigator.clipboard.writeText(input.value);
});
return container;
},
});
L.Map.mergeOptions({coordView: true});
L.Map.addInitHook(function () {
if (this.options.coordView) {
new L.Control.Coordview().addTo(this);
}
});
L.control.coordview = function(options){
return new L.Control.Coordview(options);
};
// override L.TileLayer.getTileUrl() and add a custom tile getter
L.TileLayer.include({
getTileUrl: function(coords){
let tileGetter = this.options.tileGetter;
if(typeof tileGetter === 'function'){
return tileGetter(coords, this._getZoomForUrl());
}
return false;
}
});
// auto center popups and align div/html icons
L.Popup.include({
_getAnchor: function(){
let anchor = this._source && this._source._getPopupAnchor
? this._source._getPopupAnchor()
: [0, 0];
if(typeof anchor === 'string' && anchor.toLowerCase() === 'auto'){
let style = {left: 0, top: 0, width: 0};
// is the layer active?
if(this._source._icon){
style = window.getComputedStyle(this._source._icon);
}
anchor = [
GW2MapUtil.intval(style.left) + Math.round(GW2MapUtil.intval(style.width) / 2),
GW2MapUtil.intval(style.top)
];
}
return L.point(anchor);
}
});
// i hate this so much. all of it. but it's necessary :(
L.LabelMarker = L.Marker.extend({
_initIcon: function(){
let options = this.options;
let classToAdd = 'leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');
let icon = options.icon.createIcon(this._icon);
let addIcon = false;
// if we're not reusing the icon, remove the old one and init new one
if(icon !== this._icon){
if(this._icon){
this._removeIcon();
}
addIcon = true;
if(options.title){
icon.title = options.title;
}
}
L.DomUtil.addClass(icon, classToAdd);
if(options.keyboard){
icon.tabIndex = '0';
}
this._icon = icon;
if(options.riseOnHover){
this.on({
mouseover: this._bringToFront,
mouseout : this._resetZIndex,
});
}
if(options.opacity < 1){
this._updateOpacity();
}
if(addIcon){
this.getPane().appendChild(this._icon);
// set icon styles after the node is appended to properly get the computed dimensions
options.icon._setIconStyles(this._icon, 'icon');
}
this._initInteraction();
},
});
L.LabelIcon = L.DivIcon.extend({
_setIconStyles: function(img, name){
img.className = 'leaflet-marker-icon ' + (this.options.className || '');
let sizeOption = this.options.iconSize;
let anchor = this.options.iconAnchor;
if(typeof sizeOption === 'number'){
sizeOption = [sizeOption, sizeOption];
}
let size = L.point(sizeOption);
if(anchor && anchor.toString().toLowerCase() === 'auto'){
let origin = window.getComputedStyle(img).perspectiveOrigin.split(' ');
img.style.left = '-'+origin[0];
img.style.top = '-'+origin[1];
}
else{
anchor = L.point(anchor || size && size.divideBy(2, true));
if(anchor){
img.style.marginLeft = (-anchor.x) + 'px';
img.style.marginTop = (-anchor.y) + 'px';
}
}
if(size){
img.style.width = size.x + 'px';
img.style.height = size.y + 'px';
}
},
});
// leaflet-ant-path, but different
// https://github.com/rubenspgcavalcante/leaflet-ant-path
L.AntPath = L.FeatureGroup.extend({
_antOptions: {
interactive: false,
className: 'leaflet-ant-path',
color: 'rgb(255, 255, 255)',
opacity: 0.7,
dashArray: [10 ,20],
},
_optionsMap:{
antColor: 'color',
antOpacity: 'opacity',
antDashArray: 'dashArray',
},
_latLng: null,
_antLayers: {main: null, ants: null},
initialize: function (latLng, options, type){
L.FeatureGroup.prototype.initialize.call(this);
this._latLng = latLng;
this._parseOptions(options);
this._add(type);
},
_parseOptions(options){
this.options = L.Util.extend(this.options, options || {});
Object.keys(this._optionsMap).forEach(k => {
if(this.options[k]){
this._antOptions[this._optionsMap[k]] = this.options[k];
delete this.options[k];
}
});
delete this.options.antPath;
this._antOptions.pane = this.options.pane;
},
_add: function(type){
this._antLayers.ants = new L[type](this._latLng, this._antOptions);
this._antLayers.main = new L[type](this._latLng, this.options);
this.addLayer(this._antLayers.ants);
this.addLayer(this._antLayers.main);
},
// @todo: extend the L.Layer/L.FeatureGroup interface if you need it...
});
L.GeoJSON.include({
_pathTypes: ['Circle', 'CircleMarker', 'Polygon', 'Polyline'],
addLayer: function(layer){
if(layer instanceof L.Path){
let type = this._guessPathType(layer);
let o = layer.options;
let p = layer.feature.properties;
if((o.antPath || p.antPath) && type){
let ll = type.match(/Circle/) ? layer.getLatLng() : layer.getLatLngs();
let popup = layer.getPopup();
// allow setting antPath options from the feature's properties
if(p.antPath){
['antColor', 'antOpacity', 'antDashArray', ]
.forEach(e => o[e] = p[e] || o[e] || null);
}
layer = new L.AntPath(ll, o, type);
if(popup){
layer.bindPopup(popup);
}
}
}
this._layers[this.getLayerId(layer)] = layer;
if(this._map){
this._map.addLayer(layer);
}
return this;
},
_guessPathType: function(layer){
for(let i = 0; i < this._pathTypes.length; i++){
if(layer instanceof L[this._pathTypes[i]]){
return this._pathTypes[i];
}
}
return false;
}
});
// save the GW2Map objects for later usage
// noinspection JSMismatchedCollectionQueryUpdate
let maps = [];
let mapOptions = GW2MapUtil.extend(GW2MapOptions, $options);
Object.keys($containers).forEach(id => {
let gw2map = $options.localTiles
? new GW2MapLocal($containers[id], id, mapOptions)
: new GW2Map($containers[id], id, mapOptions);
maps[id] = gw2map.init();
});
// console.log(maps);
});
})(GW2MapInvokerOptions, GW2MapContainers);
/* </nowiki> */