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.guildwars2.com/wiki/User:Smiley-1 * * scripts & libraries used: * * https://leafletjs.com/ * http://vanilla-js.com/ */ 'use strict'; const GW2MapOptions = { // errorTile : 'https://wiki.guildwars2.com/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.guildwars2.com/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.guildwars2.com/index.php?title=Widget:Map_floors/data&action=raw&ctype=text/javascript', ], stylesheets: [ 'https://wiki.guildwars2.com/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> */