// ==UserScript== // @name PoGO Tools, PoGOHWH Edition // @namespace https://github.com/PoGOHWH/iitc-ce-pogo-s2 // @id s2check@pogohwh // @category Layer // @updateURL https://raw.githubusercontent.com/PoGOHWH/iitc-ce-pogo-s2/pogohwh/s2check.user.js // @downloadURL https://raw.githubusercontent.com/PoGOHWH/iitc-ce-pogo-s2/pogohwh/s2check.user.js // @homepageURL https://github.com/PoGOHWH/iitc-ce-pogo-s2 // @supportURL https://twitter.com/PogoCells // @version 0.99.2 // @description Pokemon Go tools over IITC. News on https://twitter.com/PogoCells // @author Alfonso M. // @match https://intel.ingress.com/* // @grant none // ==/UserScript== /* eslint-env es6 */ /* eslint no-var: "error" */ /* globals L, map */ /* globals GM_info, $, dialog */ /* globals renderPortalDetails, findPortalGuidByPositionE6, chat */ ;(function () { // eslint-disable-line no-extra-semi /** S2 Geometry functions S2 extracted from Regions Plugin https:static.iitc.me/build/release/plugins/regions.user.js the regional scoreboard is based on a level 6 S2 Cell - https:docs.google.com/presentation/d/1Hl4KapfAENAOf4gv-pSngKwvS_jwNVHRPZTTDzXXn6Q/view?pli=1#slide=id.i22 at the time of writing there's no actual API for the intel map to retrieve scoreboard data, but it's still useful to plot the score cells on the intel map the S2 geometry is based on projecting the earth sphere onto a cube, with some scaling of face coordinates to keep things close to approximate equal area for adjacent cells to convert a lat,lng into a cell id: - convert lat,lng to x,y,z - convert x,y,z into face,u,v - u,v scaled to s,t with quadratic formula - s,t converted to integer i,j offsets - i,j converted to a position along a Hubbert space-filling curve - combine face,position to get the cell id NOTE: compared to the google S2 geometry library, we vary from their code in the following ways - cell IDs: they combine face and the hilbert curve position into a single 64 bit number. this gives efficient space and speed. javascript doesn't have appropriate data types, and speed is not cricical, so we use as [face,[bitpair,bitpair,...]] instead - i,j: they always use 30 bits, adjusting as needed. we use 0 to (1< temp[1]) { if (temp[0] > temp[2]) { return 0; } return 2; } if (temp[1] > temp[2]) { return 1; } return 2; } function faceXYZToUV(face,xyz) { let u, v; switch (face) { case 0: u = xyz[1] / xyz[0]; v = xyz[2] / xyz[0]; break; case 1: u = -xyz[0] / xyz[1]; v = xyz[2] / xyz[1]; break; case 2: u = -xyz[0] / xyz[2]; v = -xyz[1] / xyz[2]; break; case 3: u = xyz[2] / xyz[0]; v = xyz[1] / xyz[0]; break; case 4: u = xyz[2] / xyz[1]; v = -xyz[0] / xyz[1]; break; case 5: u = -xyz[1] / xyz[2]; v = -xyz[0] / xyz[2]; break; default: throw {error: 'Invalid face'}; } return [u,v]; } function XYZToFaceUV(xyz) { let face = largestAbsComponent(xyz); if (xyz[face] < 0) { face += 3; } const uv = faceXYZToUV(face, xyz); return [face, uv]; } function FaceUVToXYZ(face, uv) { const u = uv[0]; const v = uv[1]; switch (face) { case 0: return [1, u, v]; case 1: return [-u, 1, v]; case 2: return [-u,-v, 1]; case 3: return [-1,-v,-u]; case 4: return [v,-1,-u]; case 5: return [v, u,-1]; default: throw {error: 'Invalid face'}; } } function STToUV(st) { const singleSTtoUV = function (st) { if (st >= 0.5) { return (1 / 3.0) * (4 * st * st - 1); } return (1 / 3.0) * (1 - (4 * (1 - st) * (1 - st))); }; return [singleSTtoUV(st[0]), singleSTtoUV(st[1])]; } function UVToST(uv) { const singleUVtoST = function (uv) { if (uv >= 0) { return 0.5 * Math.sqrt (1 + 3 * uv); } return 1 - 0.5 * Math.sqrt (1 - 3 * uv); }; return [singleUVtoST(uv[0]), singleUVtoST(uv[1])]; } function STToIJ(st,order) { const maxSize = 1 << order; const singleSTtoIJ = function (st) { const ij = Math.floor(st * maxSize); return Math.max(0, Math.min(maxSize - 1, ij)); }; return [singleSTtoIJ(st[0]), singleSTtoIJ(st[1])]; } function IJToST(ij,order,offsets) { const maxSize = 1 << order; return [ (ij[0] + offsets[0]) / maxSize, (ij[1] + offsets[1]) / maxSize ]; } // S2Cell class S2.S2Cell = function () {}; //static method to construct S2.S2Cell.FromLatLng = function (latLng, level) { const xyz = LatLngToXYZ(latLng); const faceuv = XYZToFaceUV(xyz); const st = UVToST(faceuv[1]); const ij = STToIJ(st,level); return S2.S2Cell.FromFaceIJ(faceuv[0], ij, level); }; S2.S2Cell.FromFaceIJ = function (face, ij, level) { const cell = new S2.S2Cell(); cell.face = face; cell.ij = ij; cell.level = level; return cell; }; S2.S2Cell.prototype.toString = function () { return 'F' + this.face + 'ij[' + this.ij[0] + ',' + this.ij[1] + ']@' + this.level; }; S2.S2Cell.prototype.getLatLng = function () { const st = IJToST(this.ij, this.level, [0.5, 0.5]); const uv = STToUV(st); const xyz = FaceUVToXYZ(this.face, uv); return XYZToLatLng(xyz); }; S2.S2Cell.prototype.getCornerLatLngs = function () { const offsets = [ [0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0] ]; return offsets.map(offset => { const st = IJToST(this.ij, this.level, offset); const uv = STToUV(st); const xyz = FaceUVToXYZ(this.face, uv); return XYZToLatLng(xyz); }); }; S2.S2Cell.prototype.getNeighbors = function (deltas) { const fromFaceIJWrap = function (face,ij,level) { const maxSize = 1 << level; if (ij[0] >= 0 && ij[1] >= 0 && ij[0] < maxSize && ij[1] < maxSize) { // no wrapping out of bounds return S2.S2Cell.FromFaceIJ(face,ij,level); } // the new i,j are out of range. // with the assumption that they're only a little past the borders we can just take the points as // just beyond the cube face, project to XYZ, then re-create FaceUV from the XYZ vector let st = IJToST(ij,level,[0.5, 0.5]); let uv = STToUV(st); let xyz = FaceUVToXYZ(face, uv); const faceuv = XYZToFaceUV(xyz); face = faceuv[0]; uv = faceuv[1]; st = UVToST(uv); ij = STToIJ(st,level); return S2.S2Cell.FromFaceIJ(face, ij, level); }; const face = this.face; const i = this.ij[0]; const j = this.ij[1]; const level = this.level; if (!deltas) { deltas = [ {a: -1, b: 0}, {a: 0, b: -1}, {a: 1, b: 0}, {a: 0, b: 1} ]; } return deltas.map(function (values) { return fromFaceIJWrap(face, [i + values.a, j + values.b], level); }); }; /** Our code * For safety, S2 must be initialized before our code */ // based on https://github.com/iatkin/leaflet-svgicon function initSvgIcon() { L.DivIcon.SVGIcon = L.DivIcon.extend({ options: { 'className': 'svg-icon', 'iconAnchor': null, //defaults to [iconSize.x/2, iconSize.y] (point tip) 'iconSize': L.point(48, 48) }, initialize: function (options) { options = L.Util.setOptions(this, options); //iconSize needs to be converted to a Point object if it is not passed as one options.iconSize = L.point(options.iconSize); if (!options.iconAnchor) { options.iconAnchor = L.point(Number(options.iconSize.x) / 2, Number(options.iconSize.y)); } else { options.iconAnchor = L.point(options.iconAnchor); } }, // https://github.com/tonekk/Leaflet-Extended-Div-Icon/blob/master/extended.divicon.js#L13 createIcon: function (oldIcon) { let div = L.DivIcon.prototype.createIcon.call(this, oldIcon); if (this.options.id) { div.id = this.options.id; } if (this.options.style) { for (let key in this.options.style) { div.style[key] = this.options.style[key]; } } return div; } }); L.divIcon.svgIcon = function (options) { return new L.DivIcon.SVGIcon(options); }; L.Marker.SVGMarker = L.Marker.extend({ options: { 'iconFactory': L.divIcon.svgIcon, 'iconOptions': {} }, initialize: function (latlng, options) { options = L.Util.setOptions(this, options); options.icon = options.iconFactory(options.iconOptions); this._latlng = latlng; }, onAdd: function (map) { L.Marker.prototype.onAdd.call(this, map); } }); L.marker.svgMarker = function (latlng, options) { return new L.Marker.SVGMarker(latlng, options); }; } /** * Saves a file to disk with the provided text * @param {string} text - The text to save * @param {string} filename - Proposed filename */ function saveToFile(text, filename) { if (typeof text != 'string') { text = JSON.stringify(text); } if (typeof window.saveFile != 'undefined') { window.saveFile(text, filename, 'application/json'); return; } alert('You are using an old version of IITC.\r\nIn the future this plugin will no longer be compatible with it.\r\nPlease, upgrade ASAP to IITC-CE https://iitc.modos189.ru/'); if (typeof window.android !== 'undefined' && window.android.saveFile) { window.android.saveFile(filename, 'application/json', text); return; } if (isIITCm()) { promptForCopy(text); return; } const element = document.createElement('a'); // http://stackoverflow.com/questions/13405129/javascript-create-and-save-file const file = new Blob([text], {type: 'text/plain'}); const objectURL = URL.createObjectURL(file); element.setAttribute('href', objectURL); element.setAttribute('download', filename); element.style.display = 'none'; document.body.appendChild(element); element.click(); setTimeout(function () { document.body.removeChild(element); URL.revokeObjectURL(objectURL); }, 0); } /** * Prompts the user to select a file and then reads its contents and calls the callback function with those contents * @param {Function} callback - Function that will be called when the file is read. * Callback signature: function( {string} contents ) {} */ function readFromFile(callback) { if (typeof L.FileListLoader != 'undefined') { L.FileListLoader.loadFiles({accept: 'application/json'}) .on('load',function (e) { callback(e.reader.result); }); return; } alert('You are using an old version of IITC.\r\nIn the future this plugin will no longer be compatible with it.\r\nPlease, upgrade ASAP to IITC-CE https://iitc.modos189.ru/'); // special hook from iitcm if (typeof window.requestFile != 'undefined') { window.requestFile(function (filename, content) { callback(content); }); return; } if (isIITCm()) { promptForPaste(callback); return; } const input = document.createElement('input'); input.type = 'file'; document.body.appendChild(input); input.addEventListener('change', function () { const reader = new FileReader(); reader.onload = function () { callback(reader.result); }; reader.readAsText(input.files[0]); document.body.removeChild(input); }, false); input.click(); } function promptForPaste(callback) { const div = document.createElement('div'); const textarea = document.createElement('textarea'); textarea.style.width = '100%'; textarea.style.minHeight = '8em'; div.appendChild(textarea); const width = Math.min(screen.availWidth, 360); const container = dialog({ id: 'promptForPaste', html: div, width: width + 'px', title: 'Paste here the data', buttons: { OK: function () { container.dialog('close'); callback(textarea.value); } } }); } function promptForCopy(text) { const div = document.createElement('div'); const textarea = document.createElement('textarea'); textarea.style.width = '100%'; textarea.style.minHeight = '8em'; textarea.value = text; div.appendChild(textarea); const width = Math.min(screen.availWidth, 360); const container = dialog({ id: 'promptForCopy', html: div, width: width + 'px', title: 'Copy this data', buttons: { OK: function () { container.dialog('close'); } } }); } const TIMERS = {}; function createThrottledTimer(name, callback, ms) { if (TIMERS[name]) clearTimeout(TIMERS[name]); // throttle if there are several calls to the functions TIMERS[name] = setTimeout(function () { delete TIMERS[name]; if (typeof window.requestIdleCallback == 'undefined') callback(); else // and even now, wait for iddle requestIdleCallback(function () { callback(); }, {timeout: 2000}); }, ms || 100); } /** * Try to identify if the browser is IITCm due to special bugs like file picker not working */ function isIITCm() { const ua = navigator.userAgent; if (!ua.match(/Android.*Mobile/)) return false; if (ua.match(/; wb\)/)) return true; return ua.match(/ Version\//); } function is_iOS() { const ua = navigator.userAgent; return ua.includes('iPhone') || ua.includes('iPad'); } let pokestops = {}; let gyms = {}; // Portals that are marked as no PoGo items let notpogo = {}; let allPortals = {}; let newPortals = {}; let checkNewPortalsTimer; let relayoutTimer; // timer for relayout when portal is added // Portals that the user hasn't classified as Pokestops (2 or more in the same Lvl17 cell) let skippedPortals = {}; let newPokestops = {}; let notClassifiedPokestops = []; // Portals that we know, but that have been moved from our stored location. let movedPortals = []; // Pogo items that are no longer available. let missingPortals = {}; // Cells currently detected with extra gyms let cellsExtraGyms = {}; // Cells that the user has marked to ignore extra gyms let ignoredCellsExtraGyms = {}; // Cells with missing Gyms let ignoredCellsMissingGyms = {}; // Leaflet layers let regionLayer; // parent layer let stopLayerGroup; // pokestops let gymLayerGroup; // gyms let notpogoLayerGroup; // not in PoGO let nearbyLayerGroup; // circles to mark the too near limit let gridLayerGroup; // s2 grid let cellLayerGroup; // cell shading and borders let gymCenterLayerGroup; // gym centers // Group of items added to the layer let stopLayers = {}; let gymLayers = {}; let notpogoLayers = {}; let nearbyCircles = {}; const highlighterTitle = 'PoGo Tools'; const gymCellLevel = 14; // the cell level which is considered when counting POIs to determine # of gyms const poiCellLevel = 17; // the cell level where there can only be 1 POI translated to pogo const defaultSettings = { highlightGymCandidateCells: true, highlightGymCenter: false, thisIsPogo: false, analyzeForMissingData: true, centerMapOnClick: true, grids: [ { level: gymCellLevel, width: 5, color: '#004D40', opacity: 0.5 }, { level: poiCellLevel, width: 2, color: '#388E3C', opacity: 0.5 } ], colors: { cellsExtraGyms: { color: '#ff0000', opacity: 0.5 }, cellsMissingGyms: { color: '#ffa500', opacity: 0.5 }, cell17Filled: { color: '#000000', opacity: 0.6 }, cell14Filled: { color: '#000000', opacity: 0.5 }, nearbyCircleBorder: { color: '#000000', opacity: 0.6 }, nearbyCircleFill: { color: '#000000', opacity: 0.4 }, missingStops1: { color: '#BF360C', opacity: 1 }, missingStops2: { color: '#E64A19', opacity: 1 }, missingStops3: { color: '#FF5722', opacity: 1 }, stopInner: { color: '#666666', opacity: 1 }, stopOuter: { color: '#0000cd', // mediumblue opacity: 1 }, photoStopInner: { color: '#00bfff', // deepskyblue opacity: 1 }, gymInner: { color: '#3cb371', // mediumseagreen opacity: 1 }, gymOuter: { color: '#f8f8ff', // ghostwhite opacity: 1 }, exGymInner: { color: '#ff1493', // deeppink opacity: 1 }, notpogoInner: { color: '#ff0000', // red opacity: 1 }, notpogoOuter: { color: '#8b0000', // darkred opacity: 1 } }, saveDataType: 'Gyms', saveDataFormat: 'CSV' }; let settings = defaultSettings; function saveSettings() { createThrottledTimer('saveSettings', function () { localStorage[KEY_SETTINGS] = JSON.stringify(settings); }); } function loadSettings() { const tmp = localStorage[KEY_SETTINGS]; if (!tmp) { loadOldSettings(); return; } try { settings = JSON.parse(tmp); if (!settings.colors.notpogoOuter) { // Migrate to new color settings settings.colors.notpogoOuter = defaultSettings.colors.notpogoOuter; settings.colors.notpogoInner = defaultSettings.colors.notpogoInner; settings.colors.stopOuter = defaultSettings.colors.stopOuter; settings.colors.photoStopInner = defaultSettings.colors.photoStopInner; settings.colors.stopInner = defaultSettings.colors.stopInner; settings.colors.gymInner = defaultSettings.colors.gymInner; settings.colors.gymOuter = defaultSettings.colors.gymOuter; settings.colors.exGymInner = defaultSettings.colors.exGymInner; } } catch (e) { // eslint-disable-line no-empty } setThisIsPogo(); } /** * Migrate from old key to new one in order to avoid conflict with other plugin that reused this code. */ function loadOldSettings() { const tmp = localStorage['s2check_settings']; if (!tmp) return; try { settings = JSON.parse(tmp); } catch (e) { // eslint-disable-line no-empty } if (typeof settings.analyzeForMissingData == 'undefined') { settings.analyzeForMissingData = true; } if (typeof settings.promptForMissingData != 'undefined') { delete settings.promptForMissingData; } if (!settings.colors.notpogoOuter) { settings.colors.notpogoOuter = defaultSettings.colors.notpogoOuter; settings.colors.notpogoInner = defaultSettings.colors.notpogoInner; settings.colors.stopOuter = defaultSettings.colors.stopOuter; settings.colors.photoStopInner = defaultSettings.colors.photoStopInner; settings.colors.stopInner = defaultSettings.colors.stopInner; settings.colors.gymInner = defaultSettings.colors.gymInner; settings.colors.gymOuter = defaultSettings.colors.gymOuter; settings.colors.exGymInner = defaultSettings.colors.exGymInner; } if (!settings.colors) { resetColors(); } if (typeof settings.saveDataType == 'undefined') { settings.saveDataType = 'Gyms'; } if (typeof settings.saveDataFormat == 'undefined') { settings.saveDataFormat = 'CSV'; } if (typeof settings.centerMapOnClick == 'undefined') { settings.centerMapOnClick = true; } setThisIsPogo(); // migrate key localStorage.removeItem('s2check_settings'); saveStorage(); } function resetColors() { settings.grids[0].color = defaultSettings.grids[0].color; settings.grids[0].opacity = defaultSettings.grids[0].opacity; settings.grids[1].color = defaultSettings.grids[1].color; settings.grids[1].opacity = defaultSettings.grids[1].opacity; settings.colors = defaultSettings.colors; } let originalHighlightPortal; let originalChatRequestPublic; let originalChatRequestFaction; let originalChatRequestAlerts; let originalRANGE_INDICATOR_COLOR; let originalHACK_RANGE; function markPortalsAsNeutral(data) { const hidePortalOwnershipStyles = window.getMarkerStyleOptions({team: window.TEAM_NONE, level: 0}); data.portal.setStyle(hidePortalOwnershipStyles); } function changeLocationCircle() { if (window.plugin.userLocation && window.plugin.userLocation.circle) { var newRadius = settings.thisIsPogo ? 80 : 40; window.plugin.userLocation.circle.setRadius(newRadius); window.plugin.userLocation.onZoomEnd(); } } function setThisIsPogo() { document.body.classList[settings.thisIsPogo ? 'add' : 'remove']('thisIsPogo'); // It seems that iOS has some bug in the following code, but I can't debug it. if (is_iOS()) return; try { if (settings.thisIsPogo) { removeIngressLayers(); changeLocationCircle(); if (chat && chat.requestPublic) { originalChatRequestPublic = chat && chat.requestPublic; chat.requestPublic = function () {}; // no requests for chat } if (chat && chat.requestFaction) { originalChatRequestFaction = chat && chat.requestFaction; chat.requestFaction = function () {}; // no requests for chat } if (chat && chat.requestAlerts) { originalChatRequestAlerts = chat && chat.requestAlerts; chat.requestAlerts = function () {}; // no requests for chat } // Hide the link range indicator around the selected portal originalRANGE_INDICATOR_COLOR = window.RANGE_INDICATOR_COLOR; window.RANGE_INDICATOR_COLOR = 'transparent'; // Use 80 m. interaction radius originalHACK_RANGE = window.HACK_RANGE; window.HACK_RANGE = 80; if (window._current_highlighter == window._no_highlighter) { window.changePortalHighlights(highlighterTitle); } } else { restoreIngressLayers(); changeLocationCircle(); if (originalChatRequestPublic) { chat.requestPublic = originalChatRequestPublic; originalChatRequestPublic = null; } if (originalChatRequestFaction) { chat.requestFaction = originalChatRequestFaction; originalChatRequestFaction = null; } if (originalChatRequestAlerts) { chat.requestAlerts = originalChatRequestAlerts; originalChatRequestAlerts = null; } if (originalRANGE_INDICATOR_COLOR != null) window.RANGE_INDICATOR_COLOR = originalRANGE_INDICATOR_COLOR; if (originalHACK_RANGE != null) window.HACK_RANGE = originalHACK_RANGE; if (window._current_highlighter == highlighterTitle) { window.changePortalHighlights(window._no_highlighter); } if (originalHighlightPortal != null) { window.highlightPortal = originalHighlightPortal; originalHighlightPortal = null; window.resetHighlightedPortals(); } } } catch (e) { alert('Error initializing ThisIsPogo: ' + e); console.log(e); // eslint-disable-line no-console } } function sortByName(a, b) { if (!a.name) return -1; return a.name.localeCompare(b.name); } function isCellOnScreen(mapBounds, cell) { const corners = cell.getCornerLatLngs(); const cellBounds = L.latLngBounds([corners[0],corners[1]]).extend(corners[2]).extend(corners[3]); return cellBounds.intersects(mapBounds); } // return only the cells that are visible by the map bounds to ignore far away data that might not be complete function filterWithinScreen(cells) { const bounds = map.getBounds(); const filtered = {}; Object.keys(cells).forEach(cellId => { const cellData = cells[cellId]; const cell = cellData.cell; if (isCellInsideScreen(bounds, cell)) { filtered[cellId] = cellData; } }); return filtered; } function isCellInsideScreen(mapBounds, cell) { const corners = cell.getCornerLatLngs(); const cellBounds = L.latLngBounds([corners[0],corners[1]]).extend(corners[2]).extend(corners[3]); return mapBounds.contains(cellBounds); } /** * Filter a group of items (gyms/stops) excluding those out of the screen */ function filterItemsByMapBounds(items) { const bounds = map.getBounds(); const filtered = {}; Object.keys(items).forEach(id => { const item = items[id]; if (isPointOnScreen(bounds, item)) { filtered[id] = item; } }); return filtered; } function isPointOnScreen(mapBounds, point) { if (point._latlng) return mapBounds.contains(point._latlng); return mapBounds.contains(L.latLng(point)); } function groupByCell(level) { const cells = {}; classifyGroup(cells, gyms, level, (cell, item) => cell.gyms.push(item)); classifyGroup(cells, pokestops, level, (cell, item) => cell.stops.push(item)); classifyGroup(cells, newPortals, level, (cell, item) => cell.notClassified.push(item)); classifyGroup(cells, notpogo, level, (cell, item) => cell.notpogo.push(item)); return cells; } function classifyGroup(cells, items, level, callback) { Object.keys(items).forEach(id => { const item = items[id]; if (!item.cells) { item.cells = {}; } let cell; // Compute the cell only once for each level if (!item.cells[level]) { cell = S2.S2Cell.FromLatLng(item, level); item.cells[level] = cell.toString(); } const cellId = item.cells[level]; // Add it to the array of gyms of that cell if (!cells[cellId]) { if (!cell) { cell = S2.S2Cell.FromLatLng(item, level); } cells[cellId] = { cell: cell, gyms: [], stops: [], notClassified: [], notpogo: [] }; } callback(cells[cellId], item); }); } /** * Returns the items that belong to the specified cell */ function findCellItems(cellId, level, items) { return Object.values(items).filter(item => item.cells[level] == cellId); } /** Tries to add the portal photo when exporting from Ingress.com/intel */ function findPhotos(items) { if (!window.portals) { return items; } Object.keys(items).forEach(id => { const item = items[id]; if (item.image) return; const portal = window.portals[id]; if (portal && portal.options && portal.options.data) { item.image = portal.options.data.image; } }); return items; } function configureGridLevelSelect(select, i) { select.value = settings.grids[i].level; select.addEventListener('change', e => { settings.grids[i].level = parseInt(select.value, 10); saveSettings(); updateMapGrid(); }); } function showS2Dialog() { const selectRow = `

{{level}} level of grid to display:

`; const html = selectRow.replace('{{level}}', '1st') + selectRow.replace('{{level}}', '2nd') + `

Colors

`; const container = dialog({ id: 's2Settings', width: 'auto', html: html, title: 'S2 & Pokemon Settings' }); const div = container[0]; const selects = div.querySelectorAll('select'); for (let i = 0; i < 2; i++) { configureGridLevelSelect(selects[i], i); } const chkHighlight = div.querySelector('#chkHighlightCandidates'); chkHighlight.checked = settings.highlightGymCandidateCells; chkHighlight.addEventListener('change', e => { settings.highlightGymCandidateCells = chkHighlight.checked; saveSettings(); updateMapGrid(); }); const chkHighlightCenters = div.querySelector('#chkHighlightCenters'); chkHighlightCenters.checked = settings.highlightGymCenter; chkHighlightCenters.addEventListener('change', e => { settings.highlightGymCenter = chkHighlightCenters.checked; saveSettings(); updateMapGrid(); }); const chkThisIsPogo = div.querySelector('#chkThisIsPogo'); chkThisIsPogo.checked = !!settings.thisIsPogo; chkThisIsPogo.addEventListener('change', e => { settings.thisIsPogo = chkThisIsPogo.checked; saveSettings(); setThisIsPogo(); }); const chkanalyzeForMissingData = div.querySelector('#chkanalyzeForMissingData'); chkanalyzeForMissingData.checked = !!settings.analyzeForMissingData; chkanalyzeForMissingData.addEventListener('change', e => { settings.analyzeForMissingData = chkanalyzeForMissingData.checked; saveSettings(); if (newPortals.length > 0) { checkNewPortals(); } }); const chkcenterMapOnClick = div.querySelector('#chkcenterMapOnClick'); chkcenterMapOnClick.checked = settings.centerMapOnClick; chkcenterMapOnClick.addEventListener('change', e => { settings.centerMapOnClick = chkcenterMapOnClick.checked; saveSettings(); }); const PogoEditColors = div.querySelector('#PogoEditColors'); PogoEditColors.addEventListener('click', function (e) { editColors(); e.preventDefault(); return false; }); } function editColors() { const selectRow = `

{{title}}
Color: Opacity: {{width}}

`; const html = selectRow.replace('{{title}}', '1st Grid').replace('{{width}}', ` Width: `).replace(/{{id}}/g, 'grid0') + selectRow.replace('{{title}}', '2nd Grid').replace('{{width}}', ` Width: `).replace(/{{id}}/g, 'grid1') + selectRow.replace('{{title}}', 'Cells with extra gyms').replace(/{{id}}/g, 'cellsExtraGyms').replace('{{width}}', '') + selectRow.replace('{{title}}', 'Cells with missing gyms').replace(/{{id}}/g, 'cellsMissingGyms').replace('{{width}}', '') + selectRow.replace('{{title}}', `Cell ${poiCellLevel} with a gym or stop`).replace(/{{id}}/g, 'cell17Filled').replace('{{width}}', '') + selectRow.replace('{{title}}', `Cell ${gymCellLevel} with 3 gyms`).replace(/{{id}}/g, 'cell14Filled').replace('{{width}}', '') + selectRow.replace('{{title}}', '20m submit radius border').replace(/{{id}}/g, 'nearbyCircleBorder').replace('{{width}}', '') + selectRow.replace('{{title}}', '20m submit radius fill').replace(/{{id}}/g, 'nearbyCircleFill').replace('{{width}}', '') + selectRow.replace('{{title}}', '1 more stop to get a gym').replace(/{{id}}/g, 'missingStops1').replace('{{width}}', '') + selectRow.replace('{{title}}', '2 more stops to get a gym').replace(/{{id}}/g, 'missingStops2').replace('{{width}}', '') + selectRow.replace('{{title}}', '3 more stops to get a gym').replace(/{{id}}/g, 'missingStops3').replace('{{width}}', '') + selectRow.replace('{{title}}', 'PokeStop inside').replace(/{{id}}/g, 'photoStopInner').replace('{{width}}', '') + selectRow.replace('{{title}}', 'PokeStop inside (no photo)').replace(/{{id}}/g, 'stopInner').replace('{{width}}', '') + selectRow.replace('{{title}}', 'PokeStop outside').replace(/{{id}}/g, 'stopOuter').replace('{{width}}', '') + selectRow.replace('{{title}}', 'Gym inside').replace(/{{id}}/g, 'gymInner').replace('{{width}}', '') + selectRow.replace('{{title}}', 'EX-Gym inside').replace(/{{id}}/g, 'exGymInner').replace('{{width}}', '') + selectRow.replace('{{title}}', 'Gym outside').replace(/{{id}}/g, 'gymOuter').replace('{{width}}', '') + selectRow.replace('{{title}}', 'Not in PoGO inside').replace(/{{id}}/g, 'notpogoInner').replace('{{width}}', '') + selectRow.replace('{{title}}', 'Not in PoGO outside').replace(/{{id}}/g, 'notpogoOuter').replace('{{width}}', '') + 'Reset all colors' ; const container = dialog({ id: 's2Colors', width: 'auto', html: html, title: 'PoGo Grid Colors' }); const div = container[0]; const updatedSetting = function (id) { saveSettings(); if (id == 'nearbyCircleBorder' || id == 'nearbyCircleFill') { redrawNearbyCircles(); } else { updateMapGrid(); thisPlugin.addAllMarkers(); } }; const configureItems = function (key, item, id) { if (!id) id = item; const entry = settings[key][item]; const select = div.querySelector('#' + id + 'Opacity'); select.value = entry.opacity; select.addEventListener('change', function (event) { settings[key][item].opacity = select.value; updatedSetting(id); }); const input = div.querySelector('#' + id + 'Color'); input.value = entry.color; input.addEventListener('change', function (event) { settings[key][item].color = input.value; updatedSetting(id); }); if (entry.width != null) { const widthInput = div.querySelector('#' + id + 'Width'); widthInput.value = entry.width; widthInput.addEventListener('change', function (event) { settings[key][item].width = widthInput.value; updatedSetting(id); }); } }; configureItems('grids', 0, 'grid0'); configureItems('grids', 1, 'grid1'); configureItems('colors', 'cellsExtraGyms'); configureItems('colors', 'cellsMissingGyms'); configureItems('colors', 'cell17Filled'); configureItems('colors', 'cell14Filled'); configureItems('colors', 'nearbyCircleBorder'); configureItems('colors', 'nearbyCircleFill'); configureItems('colors', 'missingStops1'); configureItems('colors', 'missingStops2'); configureItems('colors', 'missingStops3'); configureItems('colors', 'photoStopInner'); configureItems('colors', 'stopInner'); configureItems('colors', 'stopOuter'); configureItems('colors', 'gymInner'); configureItems('colors', 'exGymInner'); configureItems('colors', 'gymOuter'); configureItems('colors', 'notpogoInner'); configureItems('colors', 'notpogoOuter'); const resetColorsLink = div.querySelector('#resetColorsLink'); resetColorsLink.addEventListener('click', function () { container.dialog('close'); resetColors(); updatedSetting('nearbyCircleBorder'); updatedSetting(); editColors(); }); } /** * Refresh the S2 grid over the map */ function updateMapGrid() { // preconditions if (!map.hasLayer(regionLayer)) { return; } const zoom = map.getZoom(); // first draw nearby circles at the bottom if (zoom > 16) { if (!regionLayer.hasLayer(nearbyLayerGroup)) { regionLayer.addLayer(nearbyLayerGroup); } nearbyLayerGroup.bringToBack(); } else if (regionLayer.hasLayer(nearbyLayerGroup)) { regionLayer.removeLayer(nearbyLayerGroup); } // shade level 14 and level 17 cells let cellsCloseToThreshold; if (settings.highlightGymCandidateCells && zoom > 14) { cellsCloseToThreshold = updateCandidateCells(zoom); if (!regionLayer.hasLayer(cellLayerGroup)) { regionLayer.addLayer(cellLayerGroup); } cellLayerGroup.bringToBack(); } else if (regionLayer.hasLayer(cellLayerGroup)) { regionLayer.removeLayer(cellLayerGroup); } // then draw the cell grid if (zoom > 4) { drawCellGrid(zoom); // update cell grid with cells close to a threshold for a gym if (cellsCloseToThreshold) { // draw missing cells in reverse order for (let missingStops = 3; missingStops >= 1; missingStops--) { const color = settings.colors['missingStops' + missingStops].color; const opacity = settings.colors['missingStops' + missingStops].opacity; cellsCloseToThreshold[missingStops].forEach(cell => gridLayerGroup.addLayer(drawCell(cell, color, 3, opacity))); } } if (!regionLayer.hasLayer(gridLayerGroup)) { regionLayer.addLayer(gridLayerGroup); } } else if (regionLayer.hasLayer(gridLayerGroup)) { regionLayer.removeLayer(gridLayerGroup); } // update gym centers if (settings.highlightGymCenter && zoom > 16) { updateGymCenters(); if (!regionLayer.hasLayer(gymCenterLayerGroup)) { regionLayer.addLayer(gymCenterLayerGroup); } } else if (regionLayer.hasLayer(gymCenterLayerGroup)) { regionLayer.removeLayer(gymCenterLayerGroup); } } function getLatLngPoint(data) { const result = { lat: typeof data.lat == 'function' ? data.lat() : data.lat, lng: typeof data.lng == 'function' ? data.lng() : data.lng }; return result; } /** * Highlight cells that are missing a few stops to get another gym. Also fills level 17 cells with a stop/gym. * based on data from https://www.reddit.com/r/TheSilphRoad/comments/7ppb3z/gyms_pok%C3%A9stops_and_s2_cells_followup_research/ * Cut offs: 2, 6, 20 */ function updateCandidateCells(zoom) { cellLayerGroup.clearLayers(); // All cells with items const allCells = groupByCell(gymCellLevel); const bounds = map.getBounds(); const seenCells = {}; const cellsCloseToThreshold = { 1: [], 2: [], 3: [] }; const drawCellAndNeighbors = function (cell) { const cellStr = cell.toString(); if (!seenCells[cellStr]) { // cell not visited - flag it as visited now seenCells[cellStr] = true; if (isCellOnScreen(bounds, cell)) { // on screen - draw it const cellData = allCells[cellStr]; if (cellData) { // check for errors const missingGyms = computeMissingGyms(cellData); if (missingGyms > 0 && !ignoredCellsMissingGyms[cellStr]) { cellLayerGroup.addLayer(fillCell(cell, settings.colors.cellsMissingGyms.color, settings.colors.cellsMissingGyms.opacity)); } else if (missingGyms < 0 && !ignoredCellsExtraGyms[cellStr]) { cellLayerGroup.addLayer(fillCell(cell, settings.colors.cellsExtraGyms.color, settings.colors.cellsExtraGyms.opacity)); if (!cellsExtraGyms[cellStr]) { cellsExtraGyms[cellStr] = true; updateCounter('extraGyms', Object.keys(cellsExtraGyms)); } } // shade filled level 17 cells if (zoom > 15) { const subCells = {}; const coverLevel17Cell = function (point) { const cell = S2.S2Cell.FromLatLng(point, poiCellLevel); const cellId = cell.toString(); if (subCells[cellId]) return; subCells[cellId] = true; cellLayerGroup.addLayer(fillCell(cell, settings.colors.cell17Filled.color, settings.colors.cell17Filled.opacity)); }; cellData.gyms.forEach(coverLevel17Cell); cellData.stops.forEach(coverLevel17Cell); } // number of stops to next gym const missingStops = computeMissingStops(cellData); switch (missingStops) { case 0: if (missingGyms <= 0) { cellLayerGroup.addLayer(fillCell(cell, settings.colors.cell14Filled.color, settings.colors.cell14Filled.opacity)); } break; case 1: case 2: case 3: cellsCloseToThreshold[missingStops].push(cell); break; default: break; } cellLayerGroup.addLayer(writeInCell(cell, missingStops)); } // and recurse to our neighbors const neighbors = cell.getNeighbors(); for (let i = 0; i < neighbors.length; i++) { drawCellAndNeighbors(neighbors[i]); } } } }; const cell = S2.S2Cell.FromLatLng(getLatLngPoint(map.getCenter()), gymCellLevel); drawCellAndNeighbors(cell); return cellsCloseToThreshold; } function drawCellGrid(zoom) { // clear, to redraw gridLayerGroup.clearLayers(); const bounds = map.getBounds(); const seenCells = {}; const drawCellAndNeighbors = function (cell, color, width, opacity) { const cellStr = cell.toString(); if (!seenCells[cellStr]) { // cell not visited - flag it as visited now seenCells[cellStr] = true; if (isCellOnScreen(bounds, cell)) { // on screen - draw it gridLayerGroup.addLayer(drawCell(cell, color, width, opacity)); // and recurse to our neighbors const neighbors = cell.getNeighbors(); for (let i = 0; i < neighbors.length; i++) { drawCellAndNeighbors(neighbors[i], color, width, opacity); } } } }; for (let i = settings.grids.length - 1; i >= 0; --i) { const grid = settings.grids[i]; const gridLevel = grid.level; if (gridLevel >= 6 && gridLevel < (zoom + 2)) { const cell = S2.S2Cell.FromLatLng(getLatLngPoint(map.getCenter()), gridLevel); drawCellAndNeighbors(cell, grid.color, grid.width, grid.opacity); } } return gridLayerGroup; } /** * Draw a cross to the center of level 20 cells that have a Gym to check better EX locations */ function updateGymCenters() { // clear gymCenterLayerGroup.clearLayers(); const visibleGyms = filterItemsByMapBounds(gyms); const level = 20; Object.keys(visibleGyms).forEach(id => { const gym = gyms[id]; const cell = S2.S2Cell.FromLatLng(gym, level); const corners = cell.getCornerLatLngs(); // center point const center = cell.getLatLng(); const style = {fill: false, color: 'red', opacity: 0.8, weight: 1, clickable: false, interactive: false}; const line1 = L.polyline([corners[0], corners[2]], style); gymCenterLayerGroup.addLayer(line1); const line2 = L.polyline([corners[1], corners[3]], style); gymCenterLayerGroup.addLayer(line2); const circle = L.circle(center, 1, style); gymCenterLayerGroup.addLayer(circle); }); } // Computes how many new stops must be added to the L14 Cell to get a new Gym function computeMissingStops(cellData) { const gyms = cellData.gyms.length; // exclude from the count those pokestops that have been marked as missing photos const validStops = cellData.stops.filter(p => typeof p.photos == 'undefined' || p.photos > 0); const sum = gyms + validStops.length; if (sum < 2 && gyms == 0) return 2 - sum; if (sum < 6 && gyms < 2) return 6 - sum; if (sum < 20 && gyms < 3) return 20 - sum; // No options to more gyms ATM. return 0; } // Checks if the L14 cell has enough Gyms and Stops and one of the stops should be marked as a Gym // If the result is negative then it has extra gyms function computeMissingGyms(cellData) { const totalGyms = cellData.gyms.length; // exclude from the count those pokestops that have been marked as missing photos const validStops = cellData.stops.filter(p => typeof p.photos == 'undefined' || p.photos > 0); const sum = totalGyms + validStops.length; if (sum < 2) return 0 - totalGyms; if (sum < 6) return 1 - totalGyms; if (sum < 20) return 2 - totalGyms; return 3 - totalGyms; } function drawCell(cell, color, weight, opacity) { // corner points const corners = cell.getCornerLatLngs(); // the level 6 cells have noticible errors with non-geodesic lines - and the larger level 4 cells are worse // NOTE: we only draw two of the edges. as we draw all cells on screen, the other two edges will either be drawn // from the other cell, or be off screen so we don't care const region = L.polyline([corners[0], corners[1], corners[2], corners[3], corners[0]], {fill: false, color: color, opacity: opacity, weight: weight, clickable: false, interactive: false}); return region; } function fillCell(cell, color, opacity) { // corner points const corners = cell.getCornerLatLngs(); const region = L.polygon(corners, {color: color, fillOpacity: opacity, weight: 0, clickable: false, interactive: false}); return region; } /** * Writes a text in the center of a cell */ function writeInCell(cell, text) { // center point let center = cell.getLatLng(); let marker = L.marker(center, { icon: L.divIcon({ className: 'pogo-text', iconAnchor: [25, 5], iconSize: [50, 10], html: text }) }); // fixme, maybe add some click handler marker.on('click', function () { displayCellSummary(cell); }); return marker; } /** Show a summary with the pokestops and gyms of a L14 Cell */ function displayCellSummary(cell) { const cellStr = cell.toString(); const allCells = groupByCell(gymCellLevel); const cellData = allCells[cellStr]; if (!cellData) return; function updateScore(portal, wrapper) { const photos = typeof portal.photos == 'undefined' ? 1 : portal.photos; const votes = photos == 0 || typeof portal.votes == 'undefined' ? 0 : portal.votes; const score = photos + votes; wrapper.querySelector('.Pogo_Score').textContent = score; } function dumpGroup(data, title, useHeader) { const div = document.createElement('div'); const header = document.createElement('h3'); if (useHeader) { header.className = 'header'; header.innerHTML = '' + title + ' (' + data.length + ')PhotosVotesScore'; } else { header.textContent = title + ' (' + data.length + ')'; } div.appendChild(header); data.sort(sortByName).forEach(portal => { const wrapper = document.createElement('div'); wrapper.setAttribute('data-guid', portal.guid); wrapper.className = 'PortalSummary'; const img = getPortalImage(portal); let scoreData = ''; if (title == 'Pokestops' || title == 'Portals') { const photos = typeof portal.photos == 'undefined' ? 1 : portal.photos; const votes = typeof portal.votes == 'undefined' ? 0 : portal.votes; scoreData = '' + '' + '' + (photos + votes) + ''; } wrapper.innerHTML = '' + img + '' + '' + getPortalName(portal) + '' + scoreData ; if (scoreData != '') { wrapper.querySelector('.Pogo_Photos').addEventListener('input', function () { const update = portal.photos !== this.valueAsNumber && (portal.photos === 0 || this.valueAsNumber === 0); portal.photos = this.valueAsNumber; updateScore(portal, wrapper); saveStorage(); if (update) { refreshPokestopMissingPhotoStatus(portal); updateMapGrid(); } }); wrapper.querySelector('.Pogo_Votes').addEventListener('input', function () { portal.votes = this.valueAsNumber; updateScore(portal, wrapper); saveStorage(); }); } div.appendChild(wrapper); }); return div; } const div = document.createElement('div'); div.appendChild(dumpGroup(cellData.gyms, 'Gyms')); div.appendChild(dumpGroup(cellData.stops, 'Pokestops', true)); //div.appendChild(dumpGroup(cellData.notClassified, 'Other portals')); They don't matter, they have been removed from Pokemon //div.appendChild(dumpGroup(cellData.portals, 'Portals', true)); FIXME: portals from Ingress that are hidden in Pokemon div.className = 'PogoListing'; const width = Math.min(screen.availWidth, 420); const container = dialog({ id: 'PokemonList', html: div, width: width + 'px', title: 'List of Pokestops and Gyms' }); configureHoverMarker(container); } // *************************** // IITC code // *************************** // ensure plugin framework is there, even if iitc is not yet loaded if (typeof window.plugin !== 'function') { window.plugin = function () {}; } // PLUGIN START // use own namespace for plugin window.plugin.pogo = function () {}; const thisPlugin = window.plugin.pogo; const KEY_STORAGE = 'plugin-pogo'; const KEY_SETTINGS = 'plugin-pogo-settings'; // Update the localStorage function saveStorage() { createThrottledTimer('saveStorage', function () { localStorage[KEY_STORAGE] = JSON.stringify({ gyms: cleanUpExtraData(gyms), pokestops: cleanUpExtraData(pokestops), notpogo: cleanUpExtraData(notpogo), ignoredCellsExtraGyms: ignoredCellsExtraGyms, ignoredCellsMissingGyms: ignoredCellsMissingGyms }); }); } /** * Create a new object where the extra properties of each pokestop/gym have been removed. Store only the minimum. */ function cleanUpExtraData(group) { let newGroup = {}; Object.keys(group).forEach(id => { const data = group[id]; const newData = { guid: data.guid, lat: data.lat, lng: data.lng, name: data.name }; if (data.isEx) newData.isEx = data.isEx; if (data.medal) newData.medal = data.medal; if (typeof data.photos != 'undefined') newData.photos = data.photos; if (data.votes) newData.votes = data.votes; newGroup[id] = newData; }); return newGroup; } // Load the localStorage thisPlugin.loadStorage = function () { const tmp = JSON.parse(localStorage[KEY_STORAGE] || '{}'); gyms = tmp.gyms || {}; pokestops = tmp.pokestops || {}; notpogo = tmp.notpogo || {}; ignoredCellsExtraGyms = tmp.ignoredCellsExtraGyms || {}; ignoredCellsMissingGyms = tmp.ignoredCellsMissingGyms || {}; }; thisPlugin.createEmptyStorage = function () { gyms = {}; pokestops = {}; notpogo = {}; ignoredCellsExtraGyms = {}; ignoredCellsMissingGyms = {}; saveStorage(); allPortals = {}; newPortals = {}; movedPortals = []; missingPortals = {}; }; /*************************************************************************/ thisPlugin.findByGuid = function (guid) { if (gyms[guid]) { return {'type': 'gyms', 'store': gyms}; } if (pokestops[guid]) { return {'type': 'pokestops', 'store': pokestops}; } if (notpogo[guid]) { return {'type': 'notpogo', 'store': notpogo}; } return null; }; // Append a 'star' flag in sidebar. thisPlugin.onPortalSelectedPending = false; thisPlugin.onPortalSelected = function () { $('.pogoStop').remove(); $('.pogoGym').remove(); $('.notPogo').remove(); const portalDetails = document.getElementById('portaldetails'); portalDetails.classList.remove('isGym'); if (window.selectedPortal == null) { return; } if (!thisPlugin.onPortalSelectedPending) { thisPlugin.onPortalSelectedPending = true; setTimeout(function () { // the sidebar is constructed after firing the hook thisPlugin.onPortalSelectedPending = false; $('.pogoStop').remove(); $('.pogoGym').remove(); $('.notPogo').remove(); // Show PoGo icons in the mobile status-bar if (thisPlugin.isSmart) { document.querySelector('.PogoStatus').innerHTML = thisPlugin.htmlStar; $('.PogoStatus > a').attr('title', ''); } $(portalDetails).append('
Pokemon Go: ' + thisPlugin.htmlStar + '
' + `


`); document.getElementById('PogoGymMedal').addEventListener('change', ev => { const guid = window.selectedPortal; const icon = document.getElementById('gym' + guid.replace('.', '')); // remove styling of gym marker if (icon) { icon.classList.remove(gyms[guid].medal + 'Medal'); } gyms[guid].medal = ev.target.value; saveStorage(); // update gym marker if (icon) { icon.classList.add(gyms[guid].medal + 'Medal'); } }); document.getElementById('PogoGymEx').addEventListener('change', ev => { const guid = window.selectedPortal; const icon = document.getElementById('gym' + guid.replace('.', '')); gyms[guid].isEx = ev.target.checked; saveStorage(); // update gym marker if (icon) { icon.classList[gyms[guid].isEx ? 'add' : 'remove']('exGym'); } }); thisPlugin.updateStarPortal(); }, 0); } }; // Update the status of the star (when a portal is selected from the map/pogo-list) thisPlugin.updateStarPortal = function () { $('.pogoStop').removeClass('favorite'); $('.pogoGym').removeClass('favorite'); $('.notPogo').removeClass('favorite'); document.getElementById('portaldetails').classList.remove('isGym'); const guid = window.selectedPortal; // If current portal is into pogo: select pogo portal from portals list and select the star const pogoData = thisPlugin.findByGuid(guid); if (pogoData) { if (pogoData.type === 'pokestops') { $('.pogoStop').addClass('favorite'); } if (pogoData.type === 'gyms') { $('.pogoGym').addClass('favorite'); document.getElementById('portaldetails').classList.add('isGym'); const gym = gyms[guid]; if (gym.medal) { document.getElementById('PogoGymMedal').value = gym.medal; } document.getElementById('PogoGymEx').checked = gym.isEx; } if (pogoData.type === 'notpogo') { $('.notPogo').addClass('favorite'); } } }; function removePogoObject(type, guid) { if (type === 'pokestops') { delete pokestops[guid]; const starInLayer = stopLayers[guid]; stopLayerGroup.removeLayer(starInLayer); delete stopLayers[guid]; } if (type === 'gyms') { delete gyms[guid]; const gymInLayer = gymLayers[guid]; gymLayerGroup.removeLayer(gymInLayer); delete gymLayers[guid]; } if (type === 'notpogo') { delete notpogo[guid]; const notpogoInLayer = notpogoLayers[guid]; notpogoLayerGroup.removeLayer(notpogoInLayer); delete notpogoLayers[guid]; } } // Switch the status of the star thisPlugin.switchStarPortal = function (type) { const guid = window.selectedPortal; // It has been manually classified, remove from the detection if (newPortals[guid]) delete newPortals[guid]; // If portal is saved in pogo: Remove this pogo const pogoData = thisPlugin.findByGuid(guid); if (pogoData) { const existingType = pogoData.type; removePogoObject(existingType, guid); saveStorage(); thisPlugin.updateStarPortal(); // Get portal name and coordinates const p = window.portals[guid]; const ll = p.getLatLng(); if (existingType !== type) { thisPlugin.addPortalpogo(guid, ll.lat, ll.lng, p.options.data.title, type); } // we've changed one item from pogo, if the cell was marked as ignored, reset it. if ((type == 'gyms' || existingType == 'gyms') && updateExtraGymsCells(ll.lat, ll.lng)) saveStorage(); } else { // If portal isn't saved in pogo: Add this pogo // Get portal name and coordinates const portal = window.portals[guid]; const latlng = portal.getLatLng(); thisPlugin.addPortalpogo(guid, latlng.lat, latlng.lng, portal.options.data.title, type); } if (settings.highlightGymCandidateCells) { updateMapGrid(); } }; // Add portal thisPlugin.addPortalpogo = function (guid, lat, lng, name, type) { // Add pogo in the localStorage const obj = {'guid': guid, 'lat': lat, 'lng': lng, 'name': name}; // prevent that it would trigger the missing portal detection if it's in our data if (window.portals[guid]) { obj.exists = true; } if (type == 'gyms') { updateExtraGymsCells(lat, lng); gyms[guid] = obj; } if (type == 'pokestops') { pokestops[guid] = obj; } if (type == 'notpogo') { notpogo[guid] = obj; } saveStorage(); thisPlugin.updateStarPortal(); thisPlugin.addStar(guid, lat, lng, name, type); }; /** * An item has been changed in a cell, check if the cell should no longer be ignored */ function updateExtraGymsCells(lat, lng) { if (Object.keys(ignoredCellsExtraGyms).length == 0 && Object.keys(ignoredCellsMissingGyms).length == 0) return false; const cell = S2.S2Cell.FromLatLng(new L.LatLng(lat, lng), gymCellLevel); const cellId = cell.toString(); if (ignoredCellsExtraGyms[cellId]) { delete ignoredCellsExtraGyms[cellId]; return true; } if (ignoredCellsMissingGyms[cellId]) { delete ignoredCellsMissingGyms[cellId]; return true; } return false; } /* OPTIONS */ // Manual import, export and reset data thisPlugin.pogoActionsDialog = function () { const content = `
Save... Reset PoGo portals Import Gyms & Pokestops Export Gyms & Pokestops
`; const container = dialog({ html: content, title: 'S2 & Pokemon Actions' }); const div = container[0]; div.querySelector('#save-dialog').addEventListener('click', e => saveDialog()); }; function saveDialog() { const content = `

Select the data to save from the info on screen

Which data?
Format
`; const container = dialog({ html: content, title: 'Save visible data', buttons: { 'Save': function () { const SaveDataType = document.querySelector('input[name="PogoSaveDataType"]:checked').value; const SaveDataFormat = document.querySelector('input[name="PogoSaveDataFormat"]:checked').value; settings.saveDataType = SaveDataType; settings.saveDataFormat = SaveDataFormat; saveSettings(); container.dialog('close'); let filename = (SaveDataType == 'Gyms' ? 'gyms_' : 'gyms+stops_') + new Date().toISOString().substr(0, 19).replace(/[\D]/g, '_'); if (SaveDataFormat == 'CSV') { filename += '.csv'; const allData = SaveDataType == 'Gyms' ? gyms : Object.assign({}, gyms, pokestops); const data = filterItemsByMapBounds(allData); const keys = Object.keys(data); const contents = keys.map(id => { const gym = data[id]; return (gym.name ? gym.name.replace(/,/g, ' ') + ',' : '') + gym.lat + ',' + gym.lng; }); saveToFile(contents.join('\n'), filename); } else { filename += '.json'; const data = { gyms: findPhotos(cleanUpExtraData(filterItemsByMapBounds(gyms))) }; if (SaveDataType != 'Gyms') data.pokestops = findPhotos(cleanUpExtraData(filterItemsByMapBounds(pokestops))); saveToFile(JSON.stringify(data), filename); } } } }); // Remove ok button const outer = container.parent(); outer.find('.ui-dialog-buttonset button:first').remove(); const div = container[0]; div.querySelector('#PogoSaveDataType' + settings.saveDataType).checked = true; div.querySelector('#PogoSaveDataFormat' + settings.saveDataFormat).checked = true; } thisPlugin.optAlert = function (message) { $('.ui-dialog .ui-dialog-buttonset').prepend('

' + message + '

'); $('.pogo-alert').delay(2500).fadeOut(); }; thisPlugin.optExport = function () { saveToFile(localStorage[KEY_STORAGE], 'IITC-pogo.json'); }; thisPlugin.optImport = function () { readFromFile(function (content) { try { const list = JSON.parse(content); // try to parse JSON first let importExStatus = true; let importGymMedal = true; Object.keys(list).forEach(type => { for (let idpogo in list[type]) { const item = list[type][idpogo]; const lat = item.lat; const lng = item.lng; const name = item.name; let guid = item.guid; if (!guid) { guid = findPortalGuidByPositionE6(lat * 1E6, lng * 1E6); if (!guid) { console.log('portal guid not found', name, lat, lng); // eslint-disable-line no-console guid = idpogo; } } if (typeof lat !== 'undefined' && typeof lng !== 'undefined' && name && !thisPlugin.findByGuid(guid)) { thisPlugin.addPortalpogo(guid, lat, lng, name, type); if (type == 'gyms') { if (importExStatus && item.isEx) { gyms[guid].isEx = true; } // don't overwrite existing medals if (importGymMedal && !gyms[guid].medal) { gyms[guid].medal = item.medal; } } } } }); thisPlugin.updateStarPortal(); thisPlugin.resetAllMarkers(); thisPlugin.optAlert('Successful.'); } catch (e) { console.warn('pogo: failed to import data: ' + e); // eslint-disable-line no-console thisPlugin.optAlert('Import failed'); } }); }; thisPlugin.optReset = function () { if (confirm('All pogo will be deleted. Are you sure?', '')) { delete localStorage[KEY_STORAGE]; thisPlugin.createEmptyStorage(); thisPlugin.updateStarPortal(); thisPlugin.resetAllMarkers(); if (settings.highlightGymCandidateCells) { updateMapGrid(); } thisPlugin.optAlert('Successful.'); } }; /* POKEMON GO PORTALS LAYER */ thisPlugin.addAllMarkers = function () { function iterateStore(store, type) { for (let idpogo in store) { const item = store[idpogo]; const lat = item.lat; const lng = item.lng; const guid = item.guid; const name = item.name; if (guid != null) thisPlugin.addStar(guid, lat, lng, name, type); } } iterateStore(notpogo, 'notpogo'); iterateStore(gyms, 'gyms'); iterateStore(pokestops, 'pokestops'); }; thisPlugin.resetAllMarkers = function () { for (let guid in stopLayers) { const starInLayer = stopLayers[guid]; stopLayerGroup.removeLayer(starInLayer); delete stopLayers[guid]; } for (let gymGuid in gymLayers) { const gymInLayer = gymLayers[gymGuid]; gymLayerGroup.removeLayer(gymInLayer); delete gymLayers[gymGuid]; } for (let notpogoGuid in notpogoLayers) { const notpogoInLayer = notpogoLayers[notpogoGuid]; notpogoLayerGroup.removeLayer(notpogoInLayer); delete notpogoLayers[notpogoGuid]; } thisPlugin.addAllMarkers(); }; /** * Update the disk color and title if the portal has no photo or switches to have at least one */ function refreshPokestopMissingPhotoStatus(portal) { const hasPhoto = typeof portal.photos == 'undefined' || portal.photos > 0; const guid = portal.guid; const icon = document.getElementById('pokestop' + guid.replace('.', '')); if (icon) { icon.classList.toggle('missingPhoto', !hasPhoto); icon.title = portal.name + (!hasPhoto ? '\r\n
Missing Photo, add one to make it count for Gym creation.' : ''); } } thisPlugin.addStar = function (guid, lat, lng, name, type) { let star; // Note: PoGOHWH Edition: Pokéstop and Gym markers just use CircleMarkers const m = navigator.userAgent.match(/Android.*Mobile/) ? 1.5 : 1.0; // Note: the isIITCm() implementation here does not work on IITC-CE-m at least if (type === 'pokestops') { const pokestop = pokestops[guid]; const hasPhoto = typeof pokestop.photos == 'undefined' || pokestop.photos > 0; star = new L.circleMarker([lat, lng], { title: name, radius: 7*m, weight: 5*m, color: settings.colors.stopOuter.color, opacity: settings.colors.stopOuter.opacity, fillColor: hasPhoto ? settings.colors.photoStopInner.color : settings.colors.stopInner.color, fillOpacity: hasPhoto ? settings.colors.photoStopInner.opacity : settings.colors.stopInner.opacity, pane: 'pogoPaneStops' }); } if (type === 'gyms') { const gym = gyms[guid]; star = new L.circleMarker([lat, lng], { title: name, radius: 8*m, weight: 6*m, color: settings.colors.gymOuter.color, opacity: settings.colors.gymOuter.opacity, fillColor: gym.isEx ? settings.colors.exGymInner.color : settings.colors.gymInner.color, fillOpacity: gym.isEx ? settings.colors.exGymInner.opacity : settings.colors.gymInner.opacity, pane: 'pogoPaneGyms' }); } if (type === 'notpogo') { star = new L.circleMarker([lat, lng], { title: name, radius: 6*m, weight: 4*m, color: settings.colors.notpogoOuter.color, opacity: settings.colors.notpogoOuter.opacity, fillColor: settings.colors.notpogoInner.color, fillOpacity: settings.colors.notpogoInner.opacity, pane: 'pogoPaneNotinpogo' }); } if (!star) return; window.registerMarkerForOMS(star); star.on('spiderfiedclick', function () { // don't try to render fake portals if (guid.indexOf('.') > -1) { renderPortalDetails(guid); } }); if (type === 'pokestops') { stopLayers[guid] = star; star.addTo(stopLayerGroup); } if (type === 'gyms') { gymLayers[guid] = star; star.addTo(gymLayerGroup); } if (type === 'notpogo') { notpogoLayers[guid] = star; star.addTo(notpogoLayerGroup); } }; thisPlugin.setupCSS = function () { $('