Skip to content

Instantly share code, notes, and snippets.

@5310
Last active March 16, 2024 17:31
Show Gist options
  • Save 5310/c216c40cf6d34834c9cfaad0d820ae4d to your computer and use it in GitHub Desktop.
Save 5310/c216c40cf6d34834c9cfaad0d820ae4d to your computer and use it in GitHub Desktop.
Faster PoGO Tools for IITC-CE #userscript
// ==UserScript==
// @id s2check@pogohwh
// @name Pogo Tools, PoGOHWH
// @category Layer
// @namespace https://gitlab.com/AlfonsoML/pogo-s2/
// @downloadURL https://gitlab.com/AlfonsoML/pogo-s2/raw/master/s2check.user.js
// @homepageURL https://gitlab.com/AlfonsoML/pogo-s2/
// @supportURL https://twitter.com/PogoCells
// @version 0.93
// @description Pokemon Go tools over IITC. News on https://twitter.com/PogoCells. This is a form made to run MUCH faster, kept at: https://gist.github.com/5310/c216c40cf6d34834c9cfaad0d820ae4d
// @author Alfonso M.
// @match https://www.ingress.com/intel*
// @match https://ingress.com/intel*
// @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 */
;(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<<level)-1 instead
(so GetSizeIJ for a cell is always 1)
*/
function wrapperPlugin(plugin_info) {
'use strict';
const d2r = Math.PI / 180.0;
const r2d = 180.0 / Math.PI;
const S2 = {};
function LatLngToXYZ(latLng) {
const phi = latLng.lat * d2r;
const theta = latLng.lng * d2r;
const cosphi = Math.cos(phi);
return [Math.cos(theta) * cosphi, Math.sin(theta) * cosphi, Math.sin(phi)];
}
function XYZToLatLng(xyz) {
const lat = Math.atan2(xyz[2], Math.sqrt(xyz[0] * xyz[0] + xyz[1] * xyz[1]));
const lng = Math.atan2(xyz[1], xyz[0]);
return {lat: lat * r2d, lng: lng * r2d};
}
function largestAbsComponent(xyz) {
const temp = [Math.abs(xyz[0]), Math.abs(xyz[1]), Math.abs(xyz[2])];
if (temp[0] > 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.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) {
// 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 container = dialog({
id: 'promptForPaste',
html: div,
width: '360px',
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 container = dialog({
id: 'promptForCopy',
html: div,
width: '360px',
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\//);
}
let pokestops = {};
let gyms = {};
// Portals that aren't marked as 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 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 nearbyCircles = {};
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: false,
highlightGymCenter: false,
thisIsPogo: false,
analyzeForMissingData: true,
grids: [
{
level: gymCellLevel,
width: 5,
color: '#004D40',
opacity: 0.5
},
{
level: 0,
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
}
},
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);
} 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) {
resetColors();
}
if (typeof settings.saveDataType == 'undefined') {
settings.saveDataType = 'Gyms';
}
if (typeof settings.saveDataFormat == 'undefined') {
settings.saveDataFormat = 'CSV';
}
setThisIsPogo();
// migrate key
localStorage.removeItem('s2check_settings');
thisPlugin.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;
function setThisIsPogo() {
document.body.classList[settings.thisIsPogo ? 'add' : 'remove']('thisIsPogo');
try
{
if (settings.thisIsPogo) {
removeIngressLayers();
if (window._current_highlighter == window._no_highlighter) {
// extracted from IITC plugin: Hide portal ownership
originalHighlightPortal = window.highlightPortal;
window.highlightPortal = function(portal) {
window.portalMarkerScale();
const hidePortalOwnershipStyles = window.getMarkerStyleOptions({team: window.TEAM_NONE, level: 0});
portal.setStyle(hidePortalOwnershipStyles);
};
window.resetHighlightedPortals();
}
} else {
restoreIngressLayers();
if (originalHighlightPortal != null) {
window.highlightPortal = originalHighlightPortal;
originalHighlightPortal = null;
window.resetHighlightedPortals();
}
}
}
catch (e)
{
alert('Error initializing ThisIsPogo');
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) => {/* */});
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: []
};
}
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 = `
<p>{{level}} level of grid to display: <select>
<option value=0>None</option>
<option value=6>6</option>
<option value=7>7</option>
<option value=8>8</option>
<option value=9>9</option>
<option value=10>10</option>
<option value=11>11</option>
<option value=12>12</option>
<option value=13>13</option>
<option value=14>14</option>
<option value=15>15</option>
<option value=16>16</option>
<option value=17>17</option>
<option value=18>18</option>
<option value=19>19</option>
<option value=20>20</option>
</select></p>`;
const html =
selectRow.replace('{{level}}', '1st') +
selectRow.replace('{{level}}', '2nd') +
`<p><input type="checkbox" id="chkHighlightCandidates" /><label for="chkHighlightCandidates">Highlight Cells that might get a Gym</label></p>
<p><input type="checkbox" id="chkHighlightCenters" /><label for="chkHighlightCenters">Put an X in the center of Cells with a Gym<br />(for determining EX-eligibility)</label></p>
<p><input type="checkbox" id="chkThisIsPogo" /><label for="chkThisIsPogo" title='Hide Ingress panes, info and whatever that clutters the map and it is useless for Pokemon Go'>This is PoGo!</label></p>
<p><input type="checkbox" id="chkanalyzeForMissingData" /><label for="chkanalyzeForMissingData" title="Analyze the portal data to show the pane that suggests new Pokestops and Gyms">Analyze portal data</label></p>
<p><a id='PogoEditColors'>Colors</a></p>
`;
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 PogoEditColors = div.querySelector('#PogoEditColors');
PogoEditColors.addEventListener('click', function (e) {
editColors();
e.preventDefault();
return false;
});
}
function editColors() {
const selectRow = `<p class='pogo-colors'>{{title}}<br>
Color: <input type='color' id='{{id}}Color'> Opacity: <select id='{{id}}Opacity'>
<option value=0>0</option>
<option value=0.1>0.1</option>
<option value=0.2>0.2</option>
<option value=0.3>0.3</option>
<option value=0.4>0.4</option>
<option value=0.5>0.5</option>
<option value=0.6>0.6</option>
<option value=0.7>0.7</option>
<option value=0.8>0.8</option>
<option value=0.9>0.9</option>
<option value=1>1</option>
</select>{{width}}</p>`;
const html =
selectRow.replace('{{title}}', '1st Grid').replace(`{{width}}`, ` Width: <input type='number' min='1' max='8' id='{{id}}Width' size='2'> `).replace(/{{id}}/g, 'grid0') +
selectRow.replace('{{title}}', '2nd Grid').replace(`{{width}}`, ` Width: <input type='number' min='1' max='8' id='{{id}}Width' size='2'> `).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}}`, '') +
'<a id="resetColorsLink">Reset all colors</a>'
;
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();
}
};
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');
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 coverLevel17Cell = function(point) {
const cell = S2.S2Cell.FromLatLng(point, poiCellLevel);
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);
cellLayerGroup.addLayer(writeInCell(cell, missingStops));
break;
default:
cellLayerGroup.addLayer(writeInCell(cell, missingStops));
break;
}
}
// 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;
const stops = cellData.stops.length;
const sum = gyms + stops;
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;
const sum = totalGyms + cellData.stops.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
}),
interactive: false
});
// fixme, maybe add some click handler
return marker;
}
// ***************************
// 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
thisPlugin.saveStorage = function () {
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;
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 = {};
thisPlugin.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('<div class="PogoButtons">Pokemon Go: ' + thisPlugin.htmlStar + '</div>' +
`<div id="PogoGymInfo">
<label for='PogoGymMedal'>Medal:</label> <select id='PogoGymMedal'>
<option value='None'>None</option>
<option value='Bronze'>Bronze</option>
<option value='Silver'>Silver</option>
<option value='Gold'>Gold</option>
</select><br>
<label>Is this an EX gym? <input type='checkbox' id='PogoGymEx'> Yes</label><br>
</div>`);
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;
thisPlugin.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;
thisPlugin.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];
}
}
// 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);
thisPlugin.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 (updateExtraGymsCells(ll.lat, ll.lng))
thisPlugin.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') {
gyms[guid] = obj;
}
if (type == 'pokestops') {
pokestops[guid] = obj;
}
if (type == 'notpogo') {
notpogo[guid] = obj;
}
updateExtraGymsCells(lat, lng);
thisPlugin.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 = `<div id="pogoSetbox">
<a id="save-dialog" title="Select the data to save from the info on screen">Save...</a>
<a onclick="window.plugin.pogo.optReset();return false;" title="Deletes all Pokemon Go markers">Reset PoGo portals</a>
<a onclick="window.plugin.pogo.optImport();return false;" title="Import a JSON file with all the PoGo data">Import Gyms & Pokestops</a>
<a onclick="window.plugin.pogo.optExport();return false;" title="Exports a JSON file with all the PoGo data">Export Gyms & Pokestops</a>
</div>`;
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 = `<div>
<p>Select the data to save from the info on screen</p>
<fieldset><legend>Which data?</legend>
<input type='radio' name='PogoSaveDataType' value='Gyms' id='PogoSaveDataTypeGyms'><label for='PogoSaveDataTypeGyms'>Gyms</label><br>
<input type='radio' name='PogoSaveDataType' value='PokeStopsGyms' id='PogoSaveDataTypePokeStopsGyms'><label for='PogoSaveDataTypePokeStopsGyms'>Pokestops + Gyms</label>
</fieldset>
<fieldset><legend>Format</legend>
<input type='radio' name='PogoSaveDataFormat' value='CSV' id='PogoSaveDataFormatCSV'><label for='PogoSaveDataFormatCSV'>CSV</label><br>
<input type='radio' name='PogoSaveDataFormat' value='JSON' id='PogoSaveDataFormatJSON'><label for='PogoSaveDataFormatJSON'>JSON</label>
</fieldset>
</div>`;
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('<p class="pogo-alert" style="float:left;margin-top:4px;">' + message + '</p>');
$('.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('<span style="color: #f88">Import failed</span>');
}
});
};
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;
thisPlugin.addStar(guid, lat, lng, name, type);
}
}
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];
}
thisPlugin.addAllMarkers();
};
thisPlugin.addStar = function (guid, lat, lng, name, type) {
let star;
if (type === 'pokestops') {
star = new L.circleMarker([lat, lng], {
title: name,
radius: 6,
weight: 4,
color: '#13c193',
opacity: 1.0,
fillColor: '#13c193',
fillOpacity: 1.0,
pane: 'pogoPane'
});
}
if (type === 'gyms') {
// icon from https://github.com/FortAwesome/Font-Awesome/issues/9685#issuecomment-239238656
const gym = gyms[guid];
const medal = gym.medal || 'None';
const className = medal + 'Medal' + (gym.isEx ? ' exGym' : '');
star = new L.circleMarker([lat, lng], {
title: name,
radius: 8,
weight: 6,
color: 'black',
opacity: 1.0,
fillColor: gym.isEx ? '#ff7bee' : 'black',
fillOpacity: 1.0,
pane: 'pogoPane'
});
}
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);
}
};
thisPlugin.setupCSS = function () {
$('<style>').prop('type', 'text/css').html(`
#sidebar #portaldetails h3.title{
width:auto;
}
.pogoStop span,
.pogoGym span {
display:inline-block;
float:left;
margin:3px 1px 0 4px;
width:16px;
height:15px;
overflow:hidden;
background-repeat:no-repeat;
}
.pogoStop span, .pogoStop.favorite:focus span,
.pogoGym span, .pogoGym.favorite:focus span {
background-position:left top;
}
.pogoStop:focus span, .pogoStop.favorite span,
.pogoGym:focus span, .pogoGym.favorite span {
background-position:right top;
}
/**********************************************
DIALOG BOX
**********************************************/
/*---- Options panel -----*/
#pogoSetbox a{
display:block;
color:#ffce00;
border:1px solid #ffce00;
padding:3px 0;
margin:10px auto;
width:80%;
text-align:center;
background:rgba(8,48,78,.9);
}
#pogoSetbox a.disabled,
#pogoSetbox a.disabled:hover{
color:#666;
border-color:#666;
text-decoration:none;
}
#pogoSetbox{
text-align:center;
}
.pogoStop span {
background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAPCAMAAACyXj0lAAACZFBMVEUAAAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAQECAAAAAAAGAQEAAAAPDw8AAAAMAgIAAAALAQEBAQETAwMAAAAGBQUMAgISEhIAAAAWFhYBAQEWAwMAAAACAgIDAwMFBQUGBgYJCQkPDw8REREVGBgWFhYXFxchISEiIiIkICAoKCgpICAtLCwtLi4uBQUuKysuLy8vEBAvMjEyMDAzMzM0NDQ4ODg5OTk6Ojo+Pj5AQUFBS0tCSEhDQ0NISEhJSUlMTExSUlJUVFRWVlZXV1dYCwtZCwtaWlpcXFxeXl5gYGBhBgZiYmJjY2NlDAxmDAxnZ2doaGhra2tsbGxtbW1wcHBwfHtxcXFycnJ0dHR1dXV2dnZ4CQl5eXl9fX2CgoKEhISFhYWGhoaIiIiIiomJh4qKioqLi4uMjIyNjY2PiZCQkJCUlJSXBASaERGanJycBAScnJytFRWuDg6urq6wFBS2wcG3t7e4FRW5t7q6Cwu6urq7Dg6+vr7CwsLDwMTEDg7FxcXHxsfIyMjJFxfKDw/MDg7MzMzPz8/P0NDQ0NDRDw/RFxfS09XX19faGBja2trbExPc3NzlGhrl5eXo6Ojs7u7u7u7vGxvwGhrw8PDyGhry8vLz8/P0Ghr3Gxv39/f4+Pj8/Pz8/v79/f3+////HBz/HR3/Hh7///9j6e8DAAAAPnRSTlMAAAIKDBIWGBshJTI0O0tQY2VocnN1fImVnZ6lqKmrrLCxs7u8vb3G0tbW1tra39/i4uXl7Ozv7+/v8fH6+jTKPt8AAAGeSURBVHgBYwACZiFlAxMdWT4Qm5ERImBoqgsUgAAeDfe8hsbaZEd5VpACkED6rK27Nk4IAAoAAbdZVldXd3dXV5OXOgtIAbfFlFMnT5w4eXJ3IVCAgVkzGywNJJo9JIAKmLWnnwJJA9XszZBgYBD0AEp1F2fWd3W3VtpwMTIKZgDlT8yZtPnUiYPrbLkYVEuBuj3t7OxyurpbPEUYGdWWnTp5MjeuwnfqqRMHCkQYjIoqK9Psqu2jHapqyiKlGRmN5y1f3h+7vn1G8Iq1i+qkGczsgMDewS7JDgSUGBnN/fyD3Np67BaG+IUGeisx6M0/fbrELjXK0e7QsfkukoyM+jtOn17ts2R2d8zR4zsmSjIoRJ8+fdoVqLn59LYFdgKMjApzgQKTw+KjN50+vDNPgIHf7jQQLO0EEqvyzdgYGfkTQAJ7tgCJfSst2RiYVJxPQ8E0O2FgODCp9MEEticKA0OSQ9NhP5jbYCcFDmoOrY4jYIENSVLguGCXs3NKKY2wsxIDRxZIILx38ZqZ5dZAAQjgFVdUlhHlhMQmmgAAN4GpuWb98MUAAAAASUVORK5CYII=);
}
.pogoGym span {
background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAPCAMAAACyXj0lAAAC7lBMVEUAAAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEAAAAAAAAAAAAAAAAAAAABAQEAAAABAQEBAQEAAAAAAAAAAAAAAAAAAAADAwMAAAAAAAABAQIAAAAAAAAAAAAAAAAAAAACAgIAAAAAAAABAAAAAAAAAAAAAAAAAAACAgIAAAAHBwcAAAACAgIAAAAbBgYBAQEBAQEZBgcAAAAAAAAAAAABAQEXFxcCAgICAgIHBAUBAQEGBgdyFRcRERFsFRYCAgIDAwMFBQUODg4EBAQFBQUREREFBQUGBgYTExMRCQoEBAQGBAVcIiYaGhoaGhsFBQUUFBRaJSgGBgYdFBgDAwMEBAQNDQ0ODg4fHyAjIyNYWFheLTEHBgcHBwgJCQkLCwsNDQ0PDw8RERESEhIUFBQVFRYWFhYXFxcYGBgZGRkZGRoaGhocHBwdHR0eHh4eHx8fHx8iIiIlJSUmJiYnJycpKSkqKiotLS0uLi4uLi8wMDAyMjIzMzM0NDQ2NjY4ODg6Ojo7Ozs7Oz09PT4+Pj4/Pz9DKS9DQ0NJSUpLS0xMTE1NTU1PT09QUFBRUVFSUlNXV1dZWVlbW1tcXFxeXl5eXl9jY2NkZGRmZmZoaGlsbG1wcHBycnJ1dXV7e3t/f3+AgYGBgYGFhYWIh4mPj4+THyGTk5SVlZWYmJqbm5ygoKCnp6irq6uvr6+wr7KwsLGxsbO1tbW3tri4t7m5ubu9HyDGxcjGxsfJJyjOzs7PHR7QIyTQ0NDR0dHSICHS0tLU1NTY2NjZ2dndIiPd3d3e3t7fIyTi4uLj4+PnICHn5+jq6urs6+zs7Ozu7u7w8PDw8PHx8fHx8fLy8fLy8vLzHR329vb29vf39/j4+Pj5+fn6Hh76Hx/7+/v7+/z8Hx/8/Pz8/P39Hh79/f3///+f+BszAAAAcXRSTlMAAAECAwQFBwoPFhskJSYqKy4yMzU4OTw/Q0hRW1xjZGVmb294e3+Fi4+QkZibnaWmqq+2t7m+x8nKzM3Oz9HR19fd3d/h4eLk5ebm5+rq7O7v8PDy8vP09fX19/f3+Pn5+fr6/Pz8/f3+/v7+/v7+/k5HHiYAAAGUSURBVHgBY2BkFHMMizAVYmRk5NLSVAJSUg5uwYHOlmIMjFzq+soMbHrZ3WsWNyfJ8Gh7pOTxMjJKW6fd/v79S6IFn4FXciUvg3HNoqXNk5Y3ZcXXLSrVBRooW3Dvw/lTr75nZM7Yvd6dgcF37YqGxTOrayZsubkgkpOBkd3v7MddLX2zL7cef3srSoWBIWh1z6yL2zo2XH9wpRLIZeSKu3Bj4uGj03tOv/+60IaBgSG0cWrnypldO5+8nubPDLSBI6GwpGje5KoDn3/uCxAEKvBctH9Oe+/GOy83lykyABUw+aw7sbV/yt4XPx83aTEAgXzxwSeX7t78ca3DDiTPyKBQsePd/YfPP71f5crGAAJGOduP3X3/aHW6AEQBg1ru3DM/fn47kioHFACpMHSy3/PsULc5SB6sQtI2Ov/pm2UeDEAREGLRsPK+uilaAqoApEku/NzJWHGQAASLurd1m4CYcBUuS+abQW0E8xXLQ4RBTLgS1foYfpgCEClSqwFiIYBIqzZEACrMrceKqoBbhxmqAAABho1+nW2udAAAAABJRU5ErkJggg==);
}
.PogoButtons {
color: #fff;
padding: 3px;
}
.PogoButtons span {
float: none;
}
.notPogo span {
color: #FFF;
background: #000;
border-radius: 50%;
font-size: 10px;
letter-spacing: -0.15em;
display: inline-block;
padding: 2px;
opacity: 0.6;
margin: 3px 1px 0 2px;
height: 15px;
width: 16px;
box-sizing: border-box;
line-height: 1;
}
.notPogo span:after {
display: inline-block;
content: "N/A";
position: absolute;
}
.notPogo:focus span, .notPogo.favorite span {
opacity: 1;
}
.pogo-text {
text-align: center;
font-weight: bold;
border: none !important;
background: none !important;
font-size: 130%;
color: #000;
text-shadow: 1px 1px #FFF, 2px 2px 6px #fff, -1px -1px #fff, -2px -2px 6px #fff;
}
#PogoGymInfo {
display: none;
padding: 3px;
}
.isGym #PogoGymInfo {
display: block;
}
.thisIsPogo .layer_off_warning,
.thisIsPogo .mods,
.thisIsPogo #randdetails,
.thisIsPogo #resodetails,
.thisIsPogo #level {
display: none;
}
.thisIsPogo #playerstat,
.thisIsPogo #gamestat,
.thisIsPogo #redeem,
.thisIsPogo #chat,
.thisIsPogo #artifactLink,
.thisIsPogo #scoresLink,
.thisIsPogo #chatinput,
.thisIsPogo #chatcontrols {
display: none;
}
.thisIsPogo #mobileinfo .portallevel,
.thisIsPogo #mobileinfo .resonator {
display: none;
}
.thisIsPogo #sidebar #portaldetails h3.title {
color: #fff;
}
.gym-main-outline {
fill: #FFF;
stroke: #000;
stroke-width: 5;
}
.gym-inner path {
fill: #fff;
stroke: #000;
stroke-width: 2;
}
.GoldMedal .gym-main-outline,
.GoldMedal .ball-outline-center {
fill: #FEED55;
}
.SilverMedal .gym-main-outline,
.SilverMedal .ball-outline-center {
fill: #CEDFE6;
}
.BronzeMedal .gym-main-outline,
.BronzeMedal .ball-outline-center {
fill: #F0B688;
}
.GoldMedal .gym-inner path {
stroke: #EDC13C;
stroke-width: 20;
}
.SilverMedal .gym-inner path {
stroke: #A4C1C7;
stroke-width: 20;
}
.BronzeMedal .gym-inner path {
stroke: #DD9D71;
stroke-width: 10;
}
.gym-inner .ball-outline-top {
fill: #f71208;
}
.exGym {
position: relative;
}
.exGym:after {
content: "EX";
font-weight: bold;
text-shadow: 1px 1px 3px #BED1D5, -1px -1px 3px #BED1D5;
color: #09131D;
font-size: 130%;
position: absolute;
top: 0;
right: 0;
}
.pokestop {
opacity: 0.75;
}
.pokestop path,
.pokestop ellipse {
fill: #2370DA;
}
path.pokestop-circle {
fill: #23FEF8;
stroke-width: 30px;
stroke: #2370DA;
}
.smallpokestops .pokestop {
opacity: 0.85;
}
.smallpokestops path.pokestop-pole,
.smallpokestops ellipse.pokestop-base {
display: none;
}
.smallpokestops .pokestop svg {
transform: translateY(25px) scale(0.8);
}
.PogoClassification div {
display: grid;
grid-template-columns: 200px 60px 60px 60px;
text-align: center;
align-items: center;
height: 140px;
overflow: hidden;
margin-bottom: 10px;
}
.PogoClassification div:nth-child(odd) {
background: rgba(7, 42, 69, 0.9);
}
.PogoClassification img {
max-width: 200px;
max-height: 140px;
display: block;
margin: 0 auto;
}
#dialog-missingPortals .PogoClassification div {
height: 50px;
}
img.photo,
.ingressLocation,
.pogoLocation {
cursor: zoom-in;
}
.PoGo-PortalAnimation {
width: 30px;
height: 30px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 50%;
box-shadow: 0px 0px 4px white;
animation-duration: 1s;
animation-name: shrink;
}
@keyframes shrink {
from {
width: 30px;
height: 30px;
top: 0px;
left: 0px;
}
to {
width: 10px;
height: 10px;
top: 10px;
left: 10px;
}
}
.PoGo-PortalAnimationHover {
background-color: rgb(255, 102, 0, 0.8);
border-radius: 50%;
animation-duration: 1s;
animation-name: shrinkHover;
animation-iteration-count: infinite;
}
@keyframes shrinkHover {
from {
width: 40px;
height: 40px;
top: 0px;
left: 0px;
}
to {
width: 20px;
height: 20px;
top: 10px;
left: 10px;
}
}
#sidebarPogo {
color: #eee;
padding: 2px 5px;
}
#sidebarPogo span {
margin-right: 5px;
}
.refreshingData,
.refreshingPortalCount {
opacity: 0.5;
pointer-events: none;
}
#sidebarPogo.mobile {
width: 100%;
background: rebeccapurple;
display: flex;
}
#sidebarPogo.mobile > div {
margin-right: 1em;
}
.pogo-colors input[type=color] {
border: 0;
padding: 0;
}
.leaflet-pogo-pane {
z-index: 450;
pointer-events: none;
}
`).appendTo('head');
};
// A portal has been received.
function onPortalAdded(data) {
const guid = data.portal.options.guid;
data.portal.on('add', function () {
addNearbyCircle(guid);
window.clearTimeout(relayoutTimer);
relayoutTimer = window.setTimeout(relayerBackgroundGroups, 100);
});
data.portal.on('remove', function () {
removeNearbyCircle(guid);
});
// analyze each portal only once, but sometimes the first time there's no additional data of the portal
if (allPortals[guid] && allPortals[guid].name)
return;
const portal = {
guid: guid,
name: data.portal.options.data.title,
lat: data.portal._latlng.lat,
lng: data.portal._latlng.lng,
image: data.portal.options.data.image,
cells: {}
};
allPortals[guid] = portal;
// If it's already classified in Pokemon, get out
const pogoData = thisPlugin.findByGuid(guid);
if (pogoData) {
const pogoItem = pogoData.store[guid];
if (!pogoItem.exists) {
// Mark that it still exists in Ingress
pogoItem.exists = true;
if (missingPortals[guid]) {
delete missingPortals[guid];
updateMissingPortalsCount();
}
// Check if it has been moved
if (pogoItem.lat != portal.lat || pogoItem.lng != portal.lng) {
movedPortals.push({
pogo: pogoItem,
ingress: portal
});
updateCounter('moved', movedPortals);
}
}
if (!pogoItem.name && portal.name) {
pogoData.store[guid].name = portal.name;
}
return;
}
if (skippedPortals[guid] || newPokestops[guid])
return;
newPortals[guid] = portal;
refreshNewPortalsCounter();
}
/**
* Draw a 20m circle around a portal
*/
function addNearbyCircle(guid) {
const portal = window.portals[guid];
if (!portal)
return;
const circleSettings = {
color: settings.colors.nearbyCircleBorder.color,
opacity: settings.colors.nearbyCircleBorder.opacity,
fillColor: settings.colors.nearbyCircleFill.color,
fillOpacity: settings.colors.nearbyCircleFill.opacity,
weight: 1,
clickable: false,
interactive: false
};
const center = portal._latlng;
const circle = L.circle(center, 20, circleSettings);
nearbyLayerGroup.addLayer(circle);
nearbyCircles[guid] = circle;
}
/**
* Removes the 20m circle if a portal is purged
*/
function removeNearbyCircle(guid) {
const circle = nearbyCircles[guid];
if (circle != null) {
nearbyLayerGroup.removeLayer(circle);
delete nearbyCircles[guid];
}
}
function redrawNearbyCircles() {
const keys = Object.keys(nearbyCircles);
keys.forEach(guid => {
removeNearbyCircle(guid);
addNearbyCircle(guid);
});
relayerBackgroundGroups();
}
/**
* Re-orders the layerGroups within regionLayer so that foreground objects don't get hidden/obscured by background layers.
*/
function relayerBackgroundGroups() {
if (!map.hasLayer(regionLayer)) {
return;
}
if (regionLayer.hasLayer(nearbyLayerGroup)) {
nearbyLayerGroup.bringToBack();
}
if (regionLayer.hasLayer(cellLayerGroup)) {
cellLayerGroup.bringToBack();
}
if (regionLayer.hasLayer(gymCenterLayerGroup)) {
gymCenterLayerGroup.bringToFront();
}
}
function refreshNewPortalsCounter() {
if (!settings.analyzeForMissingData)
return;
// workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=961199
try
{
if (checkNewPortalsTimer) {
clearTimeout(checkNewPortalsTimer);
} else {
document.getElementById('sidebarPogo').classList.add('refreshingPortalCount');
}
} catch (e) {
// nothing
}
// workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=961199
try
{
checkNewPortalsTimer = setTimeout(checkNewPortals, 1000);
} catch (e) {
checkNewPortals();
}
}
/**
* A potential new portal has been received
*/
function checkNewPortals() {
checkNewPortalsTimer = null;
// don't try to classify if we don't have all the portal data
if (map.getZoom() < 15)
return;
document.getElementById('sidebarPogo').classList.remove('refreshingPortalCount');
newPokestops = {};
notClassifiedPokestops = [];
const allCells = groupByCell(poiCellLevel);
// Check only the items inside the screen,
// the server might provide info about remote portals if they are part of a link
// and we don't know anything else about nearby portals of that one.
// In this case (vs drawing) we want to filter only cells fully within the screen
const cells = filterWithinScreen(allCells);
// try to guess new pokestops if they are the only items in a cell
Object.keys(cells).forEach(id => {
const data = allCells[id];
checkIsPortalMissing(data.gyms, data);
checkIsPortalMissing(data.stops, data);
//checkIsPortalMissing(data.notpogo);
if (data.notClassified.length == 0)
return;
const notClassified = data.notClassified;
if (data.gyms.length > 0 || data.stops.length > 0) {
// Already has a pogo item, ignore the rest
notClassified.forEach(portal => {
skippedPortals[portal.guid] = true;
delete newPortals[portal.guid];
});
return;
}
// only one, let's guess it's a pokestop by default
if (notClassified.length == 1) {
const portal = notClassified[0];
const obj = {'guid': portal.guid, 'lat': portal.lat, 'lng': portal.lng, 'name': portal.name};
newPokestops[portal.guid] = obj;
//delete newPortals[portal.guid];
return;
}
// too many items to guess
notClassifiedPokestops.push(data.notClassified);
});
updateCounter('pokestops', Object.values(newPokestops));
updateCounter('classification', notClassifiedPokestops);
updateMissingPortalsCount();
// Now gyms
checkNewGyms();
}
/**
* Filter the missing portals detection to show only those on screen and reduce false positives
*/
function updateMissingPortalsCount() {
const keys = Object.keys(missingPortals);
if (keys.length == 0)
updateCounter('missing', []);
const bounds = map.getBounds();
const filtered = [];
keys.forEach(guid => {
const pogoData = thisPlugin.findByGuid(guid);
const item = pogoData.store[guid];
if (isPointOnScreen(bounds, item)) {
filtered.push(item);
}
});
updateCounter('missing', filtered);
}
/**
* Given an array of pogo items checks if they have been removed from Ingress
*/
function checkIsPortalMissing(array, cellData) {
array.forEach(item => {
if (item.exists || item.newGuid)
return;
const guid = item.guid;
if (findCorrectGuid(item, cellData.notClassified)) {
return;
}
if (!missingPortals[guid]) {
missingPortals[guid] = true;
}
});
}
/**
* Check if there's another real portal in the same cell (we're checking a pogo that doesn't exist in Ingress)
*/
function findCorrectGuid(pogoItem, array) {
const portal = array.find(x => x.name == pogoItem.name && x.guid != pogoItem.guid);
if (portal != null) {
pogoItem.newGuid = portal.guid;
movedPortals.push({
pogo: pogoItem,
ingress: portal
});
updateCounter('moved', movedPortals);
delete missingPortals[pogoItem.guid];
return true;
}
return false;
}
function checkNewGyms() {
const cellsWithMissingGyms = [];
const allCells = groupByCell(gymCellLevel);
// Check only the items inside the screen,
// the server might provide info about remote portals if they are part of a link
// and we don't know anything else about nearby portals of that one.
// In this case (vs drawing) we want to filter only cells fully within the screen
const cells = filterWithinScreen(allCells);
// Find the cells where new Gyms can be identified
Object.keys(cells).forEach(id => {
const data = allCells[id];
// Only cells with all the portals already analyzed
if (data.notClassified.length > 0)
return;
if (ignoredCellsMissingGyms[data.cell.toString()])
return;
const missingGyms = computeMissingGyms(data);
if (missingGyms > 0) {
cellsWithMissingGyms.push(data);
}
});
if (cellsWithMissingGyms.length > 0) {
const filtered = filterWithinScreen(cellsWithMissingGyms);
updateCounter('gyms', Object.values(filtered));
} else {
updateCounter('gyms', []);
}
}
/**
* Display new pokestops so they can be added
*/
function promptForNewPokestops(data) {
if (data.length == 0)
return;
let pending = data.length;
const div = document.createElement('div');
div.className = 'PogoClassification';
data.sort(sortByName).forEach(portal => {
const wrapper = document.createElement('div');
wrapper.setAttribute('data-guid', portal.guid);
const img = getPortalImage(portal);
wrapper.innerHTML = '<span class="PogoName">' + getPortalName(portal) +
img + '</span>' +
'<a data-type="pokestops">' + 'STOP' + '</a>' +
'<a data-type="gyms">' + 'GYM' + '</a>' +
'<a data-type="notpogo">' + 'N/A' + '</a>';
div.appendChild(wrapper);
});
const container = dialog({
id: 'classifyPokestop',
html: div,
width: '420px',
title: 'Are all of these Pokestops or Gyms?',
buttons: {
// Button to allow skip this cell
'Skip': function () {
container.dialog('close');
data.forEach(portal => {
delete newPokestops[portal.guid];
skippedPortals[portal.guid] = true;
});
updateCounter('pokestops', Object.values(newPokestops));
},
'Mark all as Pokestops': function () {
container.dialog('close');
data.forEach(portal => {
if (!newPokestops[portal.guid])
return;
delete newPokestops[portal.guid];
thisPlugin.addPortalpogo(portal.guid, portal.lat, portal.lng, portal.name, 'pokestops');
});
if (settings.highlightGymCandidateCells) {
updateMapGrid();
}
updateCounter('pokestops', Object.values(newPokestops));
}
}
});
// Remove ok button
const outer = container.parent();
outer.find('.ui-dialog-buttonset button:first').remove();
// mark the selected one as pokestop or gym
container.on('click', 'a', function (e) {
const type = this.getAttribute('data-type');
const row = this.parentNode;
const guid = row.getAttribute('data-guid');
const portal = allPortals[guid];
delete newPokestops[portal.guid];
thisPlugin.addPortalpogo(guid, portal.lat, portal.lng, portal.name, type);
if (settings.highlightGymCandidateCells) {
updateMapGrid();
}
$(row).fadeOut(200);
pending--;
if (pending == 0) {
container.dialog('close');
}
updateCounter('pokestops', Object.values(newPokestops));
});
container.on('click', 'img.photo', centerPortal);
configureHoverMarker(container);
}
/**
* In a level 17 cell there's more than one portal, ask which one is Pokestop or Gym
*/
function promptToClassifyPokestops() {
updateCounter('classification', notClassifiedPokestops);
if (notClassifiedPokestops.length == 0)
return;
const group = notClassifiedPokestops.shift();
const div = document.createElement('div');
div.className = 'PogoClassification';
group.sort(sortByName).forEach(portal => {
const wrapper = document.createElement('div');
wrapper.setAttribute('data-guid', portal.guid);
const img = getPortalImage(portal);
wrapper.innerHTML = '<span class="PogoName">' + getPortalName(portal) +
img + '</span>' +
'<a data-type="pokestops">' + 'STOP' + '</a>' +
'<a data-type="gyms">' + 'GYM' + '</a>';
div.appendChild(wrapper);
});
const container = dialog({
id: 'classifyPokestop',
html: div,
width: '360px',
title: 'Which one is in Pokemon Go?',
buttons: {
// Button to allow skip this cell
Skip: function () {
container.dialog('close');
group.forEach(portal => {
delete newPortals[portal.guid];
skippedPortals[portal.guid] = true;
});
// continue
promptToClassifyPokestops();
}
}
});
// Remove ok button
const outer = container.parent();
outer.find('.ui-dialog-buttonset button:first').remove();
// mark the selected one as pokestop or gym
container.on('click', 'a', function (e) {
const type = this.getAttribute('data-type');
const guid = this.parentNode.getAttribute('data-guid');
const portal = getPortalSummaryFromGuid(guid);
thisPlugin.addPortalpogo(guid, portal.lat, portal.lng, portal.name, type);
if (settings.highlightGymCandidateCells) {
updateMapGrid();
}
group.forEach(tmpPortal => {
delete newPortals[tmpPortal.guid];
});
container.dialog('close');
// continue
promptToClassifyPokestops();
});
container.on('click', 'img.photo', centerPortal);
configureHoverMarker(container);
}
/**
* List of portals that have been moved
*/
function promptToMovePokestops() {
if (movedPortals.length == 0)
return;
const div = document.createElement('div');
div.className = 'PogoClassification';
movedPortals.sort(sortByName).forEach(pair => {
const portal = pair.ingress;
const pogoItem = pair.pogo;
const wrapper = document.createElement('div');
wrapper.setAttribute('data-guid', portal.guid);
wrapper.dataPortal = portal;
wrapper.dataPogoGuid = pogoItem.guid;
const img = getPortalImage(portal);
wrapper.innerHTML = '<span class="PogoName">' + getPortalName(portal) +
img + '</span>' +
'<span><span class="ingressLocation">' + 'Ingress location' + '</span></span>' +
'<span><span class="pogoLocation" data-lat="' + pogoItem.lat + '" data-lng="' + pogoItem.lng + '">' + 'Pogo location' + '</span><br>' +
'<a>' + 'Update' + '</a></span>';
div.appendChild(wrapper);
});
const container = dialog({
id: 'movedPortals',
html: div,
width: '360px',
title: 'These portals have been moved in Ingress',
buttons: {
// Button to move all the portals at once
'Update all': function () {
container.dialog('close');
movedPortals.forEach(pair => {
const portal = pair.ingress;
const pogoItem = pair.pogo;
movePogo(portal, pogoItem.guid);
});
movedPortals.length = 0;
updateCounter('moved', movedPortals);
thisPlugin.saveStorage();
if (settings.highlightGymCandidateCells) {
updateMapGrid();
}
}
}
});
// Update location
container.on('click', 'a', function (e) {
const row = this.parentNode.parentNode;
const portal = row.dataPortal;
movePogo(portal, row.dataPogoGuid);
thisPlugin.saveStorage();
if (settings.highlightGymCandidateCells) {
updateMapGrid();
}
$(row).fadeOut(200);
// remove it from the list of portals
const idx = movedPortals.findIndex(pair => pair.ingress.guid == pair.ingress.guid);
movedPortals.splice(idx, 1);
updateCounter('moved', movedPortals);
if (movedPortals.length == 0)
container.dialog('close');
});
container.on('click', 'img.photo', centerPortal);
container.on('click', '.ingressLocation', centerPortal);
container.on('click', '.pogoLocation', centerPortalAlt);
configureHoverMarker(container);
configureHoverMarkerAlt(container);
}
/**
* Update location of a pogo item
*/
function movePogo(portal, pogoGuid) {
const guid = portal.guid;
const pogoData = thisPlugin.findByGuid(pogoGuid);
const existingType = pogoData.type;
let gym = null;
if (existingType == 'gyms') {
gym = pogoData.store[guid];
}
// remove marker
removePogoObject(existingType, guid);
// Draw new marker
thisPlugin.addPortalpogo(guid, portal.lat, portal.lng, portal.name || pogoData.name, existingType);
// copy gym status
if (gym != null) {
pogoData.store[guid].isEx = gym.isEx;
pogoData.store[guid].medal = gym.medal;
thisPlugin.saveStorage();
const icon = document.getElementById('gym' + guid.replace('.', ''));
// update gym marker
if (icon) {
icon.classList.add(gym.medal + 'Medal');
icon.classList[gym.isEx ? 'add' : 'remove']('exGym');
}
}
}
/**
* Pogo items that aren't in Ingress
*/
function promptToRemovePokestops(missing) {
const div = document.createElement('div');
div.className = 'PogoClassification';
missing.sort(sortByName).forEach(portal => {
const wrapper = document.createElement('div');
wrapper.setAttribute('data-guid', portal.guid);
const name = portal.name || 'Unknown';
wrapper.innerHTML = '<span class="PogoName"><span class="pogoLocation" data-lat="' + portal.lat + '" data-lng="' + portal.lng + '">' + name + '</span></span>' +
'<span><a>' + 'Remove' + '</a></span>';
div.appendChild(wrapper);
});
const container = dialog({
id: 'missingPortals',
html: div,
width: '360px',
title: 'These portals are missing in Ingress',
buttons: {
}
});
// Update location
container.on('click', 'a', function (e) {
const row = this.parentNode.parentNode;
const guid = row.getAttribute('data-guid');
const pogoData = thisPlugin.findByGuid(guid);
const existingType = pogoData.type;
// remove marker
removePogoObject(existingType, guid);
thisPlugin.saveStorage();
if (settings.highlightGymCandidateCells) {
updateMapGrid();
}
$(row).fadeOut(200);
delete missingPortals[guid];
updateMissingPortalsCount();
if (Object.keys(missingPortals).length == 0) {
container.dialog('close');
}
});
container.on('click', '.pogoLocation', centerPortalAlt);
configureHoverMarkerAlt(container);
}
function configureHoverMarker(container) {
let hoverMarker;
container.find('img.photo, .ingressLocation').hover(
function hIn() {
const row = this.parentNode.parentNode;
const guid = row.getAttribute('data-guid');
const portal = row.dataPortal || window.portals[guid];
if (!portal)
return;
const center = portal._latlng || new L.LatLng(portal.lat, portal.lng);
hoverMarker = L.marker(center, {
icon: L.divIcon({
className: 'PoGo-PortalAnimationHover',
iconSize: [40, 40],
iconAnchor: [20, 20],
html: ''
}),
interactive: false
});
map.addLayer(hoverMarker);
}, function hOut() {
if (hoverMarker)
map.removeLayer(hoverMarker);
});
}
function configureHoverMarkerAlt(container) {
let hoverMarker;
container.find('.pogoLocation').hover(
function hIn() {
const lat = this.getAttribute('data-lat');
const lng = this.getAttribute('data-lng');
const center = new L.LatLng(lat, lng);
hoverMarker = L.marker(center, {
icon: L.divIcon({
className: 'PoGo-PortalAnimationHover',
iconSize: [40, 40],
iconAnchor: [20, 20],
html: ''
}),
interactive: false
});
map.addLayer(hoverMarker);
}, function hOut() {
if (hoverMarker)
map.removeLayer(hoverMarker);
});
}
/**
* Center the map on the clicked portal to help tracking it (the user will have to manually move the dialog)
*/
function centerPortal(e) {
const row = this.parentNode.parentNode;
const guid = row.getAttribute('data-guid');
const portal = row.dataPortal || window.portals[guid];
if (!portal)
return;
const center = portal._latlng || new L.LatLng(portal.lat, portal.lng);
map.panTo(center);
drawClickAnimation(center);
}
function centerPortalAlt(e) {
const lat = this.getAttribute('data-lat');
const lng = this.getAttribute('data-lng');
const center = new L.LatLng(lat, lng);
map.panTo(center);
drawClickAnimation(center);
}
function drawClickAnimation(center) {
const marker = L.marker(center, {
icon: L.divIcon({
className: 'PoGo-PortalAnimation',
iconSize: [30, 30],
iconAnchor: [15, 15],
html: ''
}),
interactive: false
});
map.addLayer(marker);
setTimeout(function () {
map.removeLayer(marker);
}, 2000);
}
function getPortalSummaryFromGuid(guid) {
const newPortal = newPortals[guid];
if (newPortal)
return newPortal;
const portal = window.portals[guid];
if (!portal)
return {};
return {
guid: guid,
name: portal.options.data.title,
lat: portal._latlng.lat,
lng: portal._latlng.lng,
image: portal.options.data.image,
cells: {}
};
}
function getPortalImage(pokestop) {
if (pokestop.image)
return '<img src="' + pokestop.image.replace('http:', 'https:') + '" class="photo">';
const portal = window.portals[pokestop.guid];
if (!portal)
return '';
if (portal && portal.options && portal.options.data && portal.options.data.image) {
pokestop.image = portal.options.data.image;
return '<img src="' + pokestop.image.replace('http:', 'https:') + '" class="photo">';
}
return '';
}
function getPortalName(pokestop) {
if (pokestop.name)
return pokestop.name;
const portal = window.portals[pokestop.guid];
if (!portal)
return '';
if (portal && portal.options && portal.options.data && portal.options.data.title) {
pokestop.name = portal.options.data.title;
return pokestop.name;
}
return '';
}
/**
* In a level 14 cell there's some missing Gyms, prompt which ones
*/
function promptToClassifyGyms(groups) {
// don't try to classify if we don't have all the portal data
if (map.getZoom() < 15)
return;
if (!groups || groups.length == 0)
return;
const cellData = groups.shift();
updateCounter('gyms', groups);
let missingGyms = computeMissingGyms(cellData);
const div = document.createElement('div');
div.className = 'PogoClassification';
cellData.stops.sort(sortByName).forEach(portal => {
if (skippedPortals[portal.guid])
return;
const wrapper = document.createElement('div');
wrapper.setAttribute('data-guid', portal.guid);
wrapper.innerHTML =
'<span class="PogoName">' + getPortalName(portal) +
getPortalImage(portal) + '</span>' +
'<a data-type="gyms">' + 'GYM' + '</a>';
div.appendChild(wrapper);
});
// No pokestops to prompt as it has been skipped
if (!div.firstChild) {
// continue
promptToClassifyGyms(groups);
return;
}
const container = dialog({
id: 'classifyPokestop',
html: div,
width: '360px',
title: missingGyms == 1 ? 'Which one is a Gym?' : 'Which ' + missingGyms + ' are Gyms?',
buttons: {
// Button to allow skip this cell
Skip: function () {
container.dialog('close');
cellData.stops.forEach(portal => {
skippedPortals[portal.guid] = true;
});
// continue
promptToClassifyGyms(groups);
},
// Button to allow skip this cell
'There is no Gym': function () {
ignoredCellsMissingGyms[cellData.cell.toString()] = true;
if (settings.highlightGymCandidateCells) {
updateMapGrid();
}
container.dialog('close');
thisPlugin.saveStorage();
updateCounter('gyms', groups);
// continue
promptToClassifyGyms(groups);
}
}
});
// Remove ok button
const outer = container.parent();
outer.find('.ui-dialog-buttonset button:first').remove();
// mark the selected one as pokestop or gym
container.on('click', 'a', function (e) {
const type = this.getAttribute('data-type');
const row = this.parentNode;
const guid = row.getAttribute('data-guid');
const portal = pokestops[guid];
removePogoObject('pokestops', guid);
thisPlugin.addPortalpogo(guid, portal.lat, portal.lng, portal.name, type);
if (settings.highlightGymCandidateCells) {
updateMapGrid();
}
missingGyms--;
if (missingGyms == 0) {
container.dialog('close');
// continue
promptToClassifyGyms(groups);
} else {
$(row).fadeOut(200);
document.querySelector('.ui-dialog-title-active').textContent = missingGyms == 1 ? 'Which one is a Gym?' : 'Which ' + missingGyms + ' are Gyms?';
}
});
container.on('click', 'img.photo', centerPortal);
configureHoverMarker(container);
}
/**
* In a level 14 cell there are too many Gyms
*/
function promptToVerifyGyms(cellIds) {
if (!cellIds)
cellIds = Object.keys(cellsExtraGyms);
if (cellIds.length == 0)
return;
const cellId = cellIds[0];
const group = findCellItems(cellId, gymCellLevel, gyms);
const div = document.createElement('div');
div.className = 'PogoClassification';
group.sort(sortByName).forEach(portal => {
const wrapper = document.createElement('div');
wrapper.setAttribute('data-guid', portal.guid);
const img = getPortalImage(portal);
wrapper.innerHTML = '<span class="PogoName">' + getPortalName(portal) +
img + '</span>' +
'<a data-type="pokestops">' + 'STOP' + '</a>';
div.appendChild(wrapper);
});
const container = dialog({
id: 'classifyPokestop',
html: div,
width: '360px',
title: 'This cell has too many Gyms.',
buttons: {
// Button to allow skip this cell
'All are OK': function () {
ignoredCellsExtraGyms[cellId] = true;
if (settings.highlightGymCandidateCells) {
updateMapGrid();
}
container.dialog('close');
delete cellsExtraGyms[cellId];
thisPlugin.saveStorage();
updateCounter('extraGyms', Object.keys(cellsExtraGyms));
// continue
promptToVerifyGyms();
}
}
});
// Remove ok button
const outer = container.parent();
outer.find('.ui-dialog-buttonset button:first').remove();
// mark the selected one as pokestop or gym
container.on('click', 'a', function (e) {
const type = this.getAttribute('data-type');
const guid = this.parentNode.getAttribute('data-guid');
const portal = gyms[guid];
thisPlugin.addPortalpogo(guid, portal.lat, portal.lng, portal.name, type);
if (settings.highlightGymCandidateCells) {
updateMapGrid();
}
container.dialog('close');
delete cellsExtraGyms[cellId];
updateCounter('extraGyms', Object.keys(cellsExtraGyms));
// continue
promptToVerifyGyms();
});
container.on('click', 'img.photo', centerPortal);
configureHoverMarker(container);
}
function removeLayer(name) {
const layers = window.layerChooser._layers;
const layersIds = Object.keys(layers);
let layerId = null;
let leafletLayer;
let isBase;
let arrayIdx;
layersIds.forEach(id => {
const layer = layers[id];
if (layer.name == name) {
leafletLayer = layer.layer;
layerId = leafletLayer._leaflet_id;
isBase = !layer.overlay;
arrayIdx = id;
}
});
// The Beacons and Frackers are not there in Firefox, why????
if (!leafletLayer) {
return;
}
const enabled = map._layers[layerId] != null;
if (enabled) {
// Don't remove base layer if it's used
if (isBase)
return;
map.removeLayer(leafletLayer);
}
if (typeof leafletLayer.off != 'undefined')
leafletLayer.off();
// new Leaflet
if (Array.isArray(layers)) {
// remove from array
layers.splice(parseInt(arrayIdx, 10), 1);
} else {
// classic IITC, leaflet 0.7.7
// delete from object
delete layers[layerId];
}
window.layerChooser._update();
removedLayers[name] = {
layer: leafletLayer,
enabled: enabled,
isBase: isBase
};
window.updateDisplayedLayerGroup(name, enabled);
}
const removedLayers = {};
let portalsLayerGroup;
function removeIngressLayers() {
removeLayer('CartoDB Dark Matter');
removeLayer('CartoDB Positron');
removeLayer('Google Default Ingress Map');
removeLayer('Fields');
removeLayer('Links');
removeLayer('DEBUG Data Tiles');
removeLayer('Artifacts');
removeLayer('Ornaments');
removeLayer('Beacons');
removeLayer('Frackers');
removeLayer('Unclaimed/Placeholder Portals');
for (let i = 1; i <= 8; i++) {
removeLayer('Level ' + i + ' Portals');
}
//removeLayer('Resistance');
//removeLayer('Enlightened');
mergePortalLayers();
}
/**
* Put all the layers for Ingress portals under a single one
*/
function mergePortalLayers() {
portalsLayerGroup = new L.LayerGroup();
window.addLayerGroup('Ingress Portals', portalsLayerGroup, true);
portalsLayerGroup.addLayer(removedLayers['Unclaimed/Placeholder Portals'].layer);
for (let i = 1; i <= 8; i++) {
portalsLayerGroup.addLayer(removedLayers['Level ' + i + ' Portals'].layer);
}
//portalsLayerGroup.addLayer(removedLayers['Resistance'].layer);
//portalsLayerGroup.addLayer(removedLayers['Enlightened'].layer);
}
/**
* Remove the single layer for all the portals
*/
function revertPortalLayers() {
if (!portalsLayerGroup) {
return;
}
const name = 'Ingress Portals';
const layerId = portalsLayerGroup._leaflet_id;
const enabled = map._layers[layerId] != null;
const layers = window.layerChooser._layers;
if (Array.isArray(layers)) {
// remove from array
const idx = layers.findIndex(o => o.layer._leaflet_id == layerId);
layers.splice(idx, 1);
} else {
// classic IITC, leaflet 0.7.7
// delete from object
delete layers[layerId];
}
window.layerChooser._update();
window.updateDisplayedLayerGroup(name, enabled);
if (typeof portalsLayerGroup.off != 'undefined')
portalsLayerGroup.off();
if (enabled) {
map.removeLayer(portalsLayerGroup);
}
portalsLayerGroup = null;
}
function restoreIngressLayers() {
revertPortalLayers();
Object.keys(removedLayers).forEach(name => {
const info = removedLayers[name];
if (info.isBase)
window.layerChooser.addBaseLayer(info.layer, name);
else
window.addLayerGroup(name, info.layer, info.enabled);
});
}
function zoomListener() {
const zoom = map.getZoom();
document.body.classList.toggle('smallpokestops', zoom < 16);
}
const setup = function () {
thisPlugin.isSmart = window.isSmartphone();
initSvgIcon();
loadSettings();
// Create a pane just for PoGOTool markers
map.createPane('pogoPane');
// Load data from localStorage
thisPlugin.loadStorage();
thisPlugin.htmlStar = `<a class="pogoStop" accesskey="p" onclick="window.plugin.pogo.switchStarPortal('pokestops');return false;" title="Mark this portal as a pokestop [p]"><span></span></a>
<a class="pogoGym" accesskey="g" onclick="window.plugin.pogo.switchStarPortal('gyms');return false;" title="Mark this portal as a PokeGym [g]"><span></span></a>
<a class="notPogo" onclick="window.plugin.pogo.switchStarPortal('notpogo');return false;" title="Mark this portal as a removed/Not Available in Pokemon Go"><span></span></a>
`;
thisPlugin.setupCSS();
const sidebarPogo = document.createElement('div');
sidebarPogo.id = 'sidebarPogo';
sidebarPogo.style.display = 'none';
if (thisPlugin.isSmart) {
const status = document.getElementById('updatestatus');
sidebarPogo.classList.add('mobile');
status.insertBefore(sidebarPogo, status.firstElementChild);
const dStatus = document.createElement('div');
dStatus.className = 'PogoStatus';
status.insertBefore(dStatus, status.firstElementChild);
} else {
document.getElementById('sidebar').appendChild(sidebarPogo);
}
sidebarPogo.appendChild(createCounter('New pokestops', 'pokestops', promptForNewPokestops));
sidebarPogo.appendChild(createCounter('Review required', 'classification', promptToClassifyPokestops));
sidebarPogo.appendChild(createCounter('Moved portals', 'moved', promptToMovePokestops));
sidebarPogo.appendChild(createCounter('Missing portals', 'missing', promptToRemovePokestops));
sidebarPogo.appendChild(createCounter('New Gyms', 'gyms', promptToClassifyGyms));
sidebarPogo.appendChild(createCounter('Cells with extra Gyms', 'extraGyms', promptToVerifyGyms));
window.addHook('portalSelected', thisPlugin.onPortalSelected);
window.addHook('portalAdded', onPortalAdded);
window.addHook('mapDataRefreshStart', function () {
sidebarPogo.classList.add('refreshingData');
});
window.addHook('mapDataRefreshEnd', function () {
sidebarPogo.classList.remove('refreshingData');
refreshNewPortalsCounter();
});
map.on('moveend', function () {
refreshNewPortalsCounter();
});
sidebarPogo.classList.add('refreshingData');
// Layer - pokemon go portals
stopLayerGroup = L.layerGroup();
window.addLayerGroup('PokeStops', stopLayerGroup, true);
gymLayerGroup = L.layerGroup();
window.addLayerGroup('Gyms', gymLayerGroup, true);
regionLayer = L.layerGroup();
window.addLayerGroup('S2 Grid', regionLayer, true);
// this layer will group all the nearby circles that are added or removed from it when the portals are added or removed
nearbyLayerGroup = L.featureGroup();
// this layer will group all the shaded cells and cell borders
cellLayerGroup = L.featureGroup();
// this layer will contain the s2 grid
gridLayerGroup = L.layerGroup()
// this layer will contain the gym centers for checking ex eligibility
gymCenterLayerGroup = L.featureGroup();
thisPlugin.addAllMarkers();
const toolbox = document.getElementById('toolbox');
const buttonPoGo = document.createElement('a');
buttonPoGo.textContent = 'PoGo Actions';
buttonPoGo.title = 'Actions on Pokemon Go data';
buttonPoGo.addEventListener('click', thisPlugin.pogoActionsDialog);
toolbox.appendChild(buttonPoGo);
const buttonGrid = document.createElement('a');
buttonGrid.textContent = 'PoGo Settings';
buttonGrid.title = 'Settings for S2 & PokemonGo';
buttonGrid.addEventListener('click', e => {
if (thisPlugin.isSmart)
window.show('map');
showS2Dialog();
});
toolbox.appendChild(buttonGrid);
map.on('zoomend', zoomListener);
zoomListener();
map.on('moveend', updateMapGrid);
updateMapGrid();
map.on('overlayadd', function(event) {
if (event && event.name === "S2 Grid") {
updateMapGrid();
}
});
// add ids to the links that we want to be able to hide
const links = document.querySelectorAll('#toolbox > a');
links.forEach(a => {
const text = a.textContent;
if (text == 'Region scores') {
a.id = 'scoresLink';
}
if (text == 'Artifacts') {
a.id = 'artifactLink';
}
});
};
function createCounter(title, type, callback) {
const div = document.createElement('div');
div.style.display = 'none';
const sTitle = document.createElement('span');
sTitle.textContent = title;
const counter = document.createElement('a');
counter.id = 'PogoCounter-' + type;
counter.addEventListener('click', function (e) {
callback(counter.PogoData);
return false;
});
div.appendChild(sTitle);
div.appendChild(counter);
return div;
}
function updateCounter(type, data) {
const counter = document.querySelector('#PogoCounter-' + type);
counter.PogoData = data;
counter.textContent = data.length;
counter.parentNode.style.display = data.length > 0 ? '' : 'none';
// Adjust visibility of the pane to avoid the small gap due to padding
const pane = counter.parentNode.parentNode;
if (data.length > 0) {
pane.style.display = '';
return;
}
let node = pane.firstElementChild;
while (node) {
const rowData = node.lastElementChild.PogoData;
if (rowData && rowData.length > 0) {
pane.style.display = '';
return;
}
node = node.nextElementSibling;
}
pane.style.display = 'none';
}
// PLUGIN END //////////////////////////////////////////////////////////
setup.info = plugin_info; //add the script info data to the function as a property
// if IITC has already booted, immediately run the 'setup' function
if (window.iitcLoaded) {
setup();
} else {
if (!window.bootPlugins) {
window.bootPlugins = [];
}
window.bootPlugins.push(setup);
}
}
const plugin_info = {};
if (typeof GM_info !== 'undefined' && GM_info && GM_info.script) {
plugin_info.script = {
version: GM_info.script.version,
name: GM_info.script.name,
description: GM_info.script.description
};
}
// Greasemonkey. It will be quite hard to debug
if (typeof unsafeWindow != 'undefined' || typeof GM_info == 'undefined' || GM_info.scriptHandler != 'Tampermonkey') {
// inject code into site context
const script = document.createElement('script');
script.appendChild(document.createTextNode('(' + wrapperPlugin + ')(' + JSON.stringify(plugin_info) + ');'));
(document.body || document.head || document.documentElement).appendChild(script);
} else {
// Tampermonkey, run code directly
wrapperPlugin(plugin_info);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment