Widget:Karte v2/script

Aus Guild Wars 2 Wiki
Wechseln zu: Navigation, Suche

/* */ /** * 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 { // common settings for all maps options = { containerClassName: 'gw2map', linkboxClassName : 'gw2map-linkbox', // additional to containerClassName navClassName : 'gw2map-nav', lang : 'en', initLayers : null, mapAttribution : true, errorTile : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAADHUlEQVR4nO3UMQEAIAzAsIF/zyBjRxMFvXpm5g2QdLcDgD0GAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEGAGEfdCIC/5NkVo8AAAAASUVORK5CYII=', 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)', }, }; 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', ]; 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(); } /** * @returns {GW2Map} * @public */ init(){ 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 @todo let url = this.options.apiBase + '/v2/continents/' + this.dataset.continentId + '/floors/' + this.dataset.floorId; url += this.dataset.regionId ? '/regions/' + this.dataset.regionId : ''; url += this.dataset.regionId && this.dataset.mapId ? '/maps/' + this.dataset.mapId : ''; url += '?wiki=1&lang=' + this.dataset.language; this._request(url, '_renderFloor'); return this; } /** * @param {string} url * @param {string} callback * @private */ _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); if(typeof callback === 'string'){ return this[callback](json); } return callback(json); } console.log('(╯°□°)╯彡┻━┻ ', request.status); }); request.send(); } /** * sets the base tiles and adds an optional copyright info * * @returns {GW2Map} * @private */ _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 + ' © <a href="http://www.arena.net/" target="_blank">ArenaNet</a>' : false, }).addTo(this.map); // add the layer controls if(this.dataset.mapControls){ this.controls = L.control.layers().addTo(this.map); } return this; } /** * @todo https://github.com/arenanet/api-cdi/pull/61 * @todo https://github.com/arenanet/api-cdi/pull/62 * @todo https://github.com/arenanet/api-cdi/issues/308 * * @param {*} json * @private */ _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 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); let panes = Object.keys(geojson.featureCollections); panes.forEach(pane => this._createPane(geojson.featureCollections[pane].getJSON(), pane, (this.dataset.initLayers || this.options.initLayers || panes))); this.map.on('zoomend', ev => this._zoomEndEvent()); this._zoomEndEvent(); // invoke once to set the icon zoom on the newly created map if(this.dataset.events){ this._renderEvents(); } } /** * @private */ _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'); } }); } /** * @private */ _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); panes.forEach(pane => {this._createPane(eventGeoJSON.featureCollections[pane].getJSON(), pane, (this.dataset.initLayers || this.options.initLayers || panes))}); }); }); } /** * @param {GW2FloorGeoJSON[]} geojson * @param {string} pane * @param {string[]}initLayers * @private */ _createPane(geojson, pane, initLayers){ let name = '<span class="gw2map-layer-control '+pane+'"> </span> ' + GW2MAP_I18N.layers[pane]; 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) } else{ this.layers[pane].addData(geojson); } if(GW2MapUtil.in_array(pane, initLayers)){ this.layers[pane].addTo(this.map); } } /** * @link http://leafletjs.com/reference-1.5.0.html#geojson-oneachfeature * @param {*} feature * @param {L.Layer} layer * @param {string} pane * @private */ _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(/(Mount\:_|Raid—)/, ''); content += '<a class="gw2map-wikilink" href="' + GW2MAP_I18N.wiki+encodeURIComponent(wikiname) + '" target="_blank">' + p.name + '</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) } } /** * * @param {string} str * @returns {string} * @private */ _parseWikilinks(str){ 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>'); } /** * @param {*} feature * @param {L.Layer} layer * @param {string} pane * @private */ _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); } } /** * @link http://leafletjs.com/reference-1.5.0.html#geojson-pointtolayer * @param {*} feature * @param {LatLng} coords * @param {string} pane * @private */ _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'; } 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 * @private */ _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.8, color: this.options.colors[pane] || 'rgba(255, 255, 255, 0.3)', weight: 2, interactive: false, } } return { pane: pane, stroke: true, opacity: 0.8, color: p.color || 'rgba(255, 255, 255, 0.3)', weight: 3, interactive: true, } } /** * @param {[*,*]} coords * @returns {LatLng} * @private */ _p2ll(coords){ return this.map.unproject(coords, this.options.maxZoom); } /** * @param {[*,*]} coords * @param {number} zoom * @returns {[*,*]} * @private */ _project(coords, zoom){ return coords.map(c => Math.floor((c / (1 << (this.options.maxZoom - zoom))) / 256)); } /** * @param {[*,*]} coords * @param {number} zoom * @returns {string} * @private */ _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 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}, 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 {[]} * @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); } } /** * Class GW2MapUtil */ class GW2MapUtil{ /** * @param {Object} target * @param {Object} source * @returns {Object} */ static extend(target, source) { for(let property in source) { 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){ 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 => { this._addFeature(layer, e.id, mapID, (e.name || extra.name), { icon : e.icon || extra.icon || null, className : extra.className, type : extra.type, color : e.color || extra.color, layertype : extra.layertype || 'icon', description: e.description || extra.description || null }, 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 = GW2MapUtil.extend(sector, 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, { chat_link: task.chat_link, 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 {[]} [map_rect] taken from maps.json or map_floor.json * @returns {*[]} */ scaleCoords(coords, map_rect){ map_rect = this.map_rect || map_rect; return [ Math.round(this.rect[0][0]+(this.rect[1][0]-this.rect[0][0])*(coords[0]-map_rect[0][0])/(map_rect[1][0]-map_rect[0][0])), Math.round(this.rect[0][1]+(this.rect[1][1]-this.rect[0][1])*(1-(coords[1]-map_rect[0][1])/(map_rect[1][1]-map_rect[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', scripts:[ 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.5.1/leaflet-src.js', // 'https://unpkg.com/leaflet-ant-path@1.3.0/dist/leaflet-ant-path.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); } }); L.Marker.include({ _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; } if(icon.tagName === 'IMG'){ icon.alt = options.alt || ''; } } 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', addIcon); } this._initInteraction(); } }); L.Icon.include({ _setIconStyles:function(img, name, addIcon){ if(addIcon !== true){ return; } 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'; } } }); // save the GW2Map objects for later usage // noinspection JSMismatchedCollectionQueryUpdate let maps = []; let mapOptions = GW2MapUtil.extend(GW2MapOptions, $options); Object.keys($containers).forEach(id => { maps[id] = new GW2Map($containers[id], id, mapOptions).init(); }); // console.log(maps); }); })(GW2MapInvokerOptions, GW2MapContainers); /* */