· 6 years ago · Nov 19, 2019, 04:02 PM
1// ==UserScript==
2// @id s2check@alfonsoml
3// @name Pogo Tools
4// @category Layer
5// @namespace https://gitlab.com/AlfonsoML/pogo-s2/
6// @downloadURL https://gitlab.com/AlfonsoML/pogo-s2/raw/master/s2check.user.js
7// @homepageURL https://gitlab.com/AlfonsoML/pogo-s2/
8// @supportURL https://twitter.com/PogoCells
9// @version 0.93.1
10// @description Pokemon Go tools over IITC. News on https://twitter.com/PogoCells
11// @author Alfonso M.
12// @match https://www.ingress.com/intel*
13// @match https://ingress.com/intel*
14// @match https://intel.ingress.com/*
15// @grant none
16// ==/UserScript==
17
18/* eslint-env es6 */
19/* eslint no-var: "error" */
20/* globals L, map */
21/* globals GM_info, $, dialog */
22/* globals renderPortalDetails, findPortalGuidByPositionE6 */
23
24
25;(function() { // eslint-disable-line no-extra-semi
26
27/** S2 Geometry functions
28
29 S2 extracted from Regions Plugin
30 https:static.iitc.me/build/release/plugins/regions.user.js
31
32 the regional scoreboard is based on a level 6 S2 Cell
33 - https:docs.google.com/presentation/d/1Hl4KapfAENAOf4gv-pSngKwvS_jwNVHRPZTTDzXXn6Q/view?pli=1#slide=id.i22
34 at the time of writing there's no actual API for the intel map to retrieve scoreboard data,
35 but it's still useful to plot the score cells on the intel map
36
37
38 the S2 geometry is based on projecting the earth sphere onto a cube, with some scaling of face coordinates to
39 keep things close to approximate equal area for adjacent cells
40 to convert a lat,lng into a cell id:
41 - convert lat,lng to x,y,z
42 - convert x,y,z into face,u,v
43 - u,v scaled to s,t with quadratic formula
44 - s,t converted to integer i,j offsets
45 - i,j converted to a position along a Hubbert space-filling curve
46 - combine face,position to get the cell id
47
48 NOTE: compared to the google S2 geometry library, we vary from their code in the following ways
49 - cell IDs: they combine face and the hilbert curve position into a single 64 bit number. this gives efficient space
50 and speed. javascript doesn't have appropriate data types, and speed is not cricical, so we use
51 as [face,[bitpair,bitpair,...]] instead
52 - i,j: they always use 30 bits, adjusting as needed. we use 0 to (1<<level)-1 instead
53 (so GetSizeIJ for a cell is always 1)
54*/
55
56function wrapperPlugin(plugin_info) {
57 'use strict';
58
59 const d2r = Math.PI / 180.0;
60 const r2d = 180.0 / Math.PI;
61
62 const S2 = {};
63
64 function LatLngToXYZ(latLng) {
65 const phi = latLng.lat * d2r;
66 const theta = latLng.lng * d2r;
67 const cosphi = Math.cos(phi);
68
69 return [Math.cos(theta) * cosphi, Math.sin(theta) * cosphi, Math.sin(phi)];
70 }
71
72 function XYZToLatLng(xyz) {
73 const lat = Math.atan2(xyz[2], Math.sqrt(xyz[0] * xyz[0] + xyz[1] * xyz[1]));
74 const lng = Math.atan2(xyz[1], xyz[0]);
75
76 return {lat: lat * r2d, lng: lng * r2d};
77 }
78
79 function largestAbsComponent(xyz) {
80 const temp = [Math.abs(xyz[0]), Math.abs(xyz[1]), Math.abs(xyz[2])];
81
82 if (temp[0] > temp[1]) {
83 if (temp[0] > temp[2]) {
84 return 0;
85 }
86 return 2;
87 }
88
89 if (temp[1] > temp[2]) {
90 return 1;
91 }
92
93 return 2;
94 }
95
96 function faceXYZToUV(face,xyz) {
97 let u, v;
98
99 switch (face) {
100 case 0: u = xyz[1] / xyz[0]; v = xyz[2] / xyz[0]; break;
101 case 1: u = -xyz[0] / xyz[1]; v = xyz[2] / xyz[1]; break;
102 case 2: u = -xyz[0] / xyz[2]; v = -xyz[1] / xyz[2]; break;
103 case 3: u = xyz[2] / xyz[0]; v = xyz[1] / xyz[0]; break;
104 case 4: u = xyz[2] / xyz[1]; v = -xyz[0] / xyz[1]; break;
105 case 5: u = -xyz[1] / xyz[2]; v = -xyz[0] / xyz[2]; break;
106 default: throw {error: 'Invalid face'};
107 }
108
109 return [u,v];
110 }
111
112 function XYZToFaceUV(xyz) {
113 let face = largestAbsComponent(xyz);
114
115 if (xyz[face] < 0) {
116 face += 3;
117 }
118
119 const uv = faceXYZToUV(face, xyz);
120
121 return [face, uv];
122 }
123
124 function FaceUVToXYZ(face, uv) {
125 const u = uv[0];
126 const v = uv[1];
127
128 switch (face) {
129 case 0: return [1, u, v];
130 case 1: return [-u, 1, v];
131 case 2: return [-u,-v, 1];
132 case 3: return [-1,-v,-u];
133 case 4: return [v,-1,-u];
134 case 5: return [v, u,-1];
135 default: throw {error: 'Invalid face'};
136 }
137 }
138
139 function STToUV(st) {
140 const singleSTtoUV = function (st) {
141 if (st >= 0.5) {
142 return (1 / 3.0) * (4 * st * st - 1);
143 }
144 return (1 / 3.0) * (1 - (4 * (1 - st) * (1 - st)));
145
146 };
147
148 return [singleSTtoUV(st[0]), singleSTtoUV(st[1])];
149 }
150
151 function UVToST(uv) {
152 const singleUVtoST = function (uv) {
153 if (uv >= 0) {
154 return 0.5 * Math.sqrt (1 + 3 * uv);
155 }
156 return 1 - 0.5 * Math.sqrt (1 - 3 * uv);
157
158 };
159
160 return [singleUVtoST(uv[0]), singleUVtoST(uv[1])];
161 }
162
163 function STToIJ(st,order) {
164 const maxSize = 1 << order;
165
166 const singleSTtoIJ = function (st) {
167 const ij = Math.floor(st * maxSize);
168 return Math.max(0, Math.min(maxSize - 1, ij));
169 };
170
171 return [singleSTtoIJ(st[0]), singleSTtoIJ(st[1])];
172 }
173
174 function IJToST(ij,order,offsets) {
175 const maxSize = 1 << order;
176
177 return [
178 (ij[0] + offsets[0]) / maxSize,
179 (ij[1] + offsets[1]) / maxSize
180 ];
181 }
182
183 // S2Cell class
184 S2.S2Cell = function () {};
185
186 //static method to construct
187 S2.S2Cell.FromLatLng = function (latLng, level) {
188 const xyz = LatLngToXYZ(latLng);
189 const faceuv = XYZToFaceUV(xyz);
190 const st = UVToST(faceuv[1]);
191 const ij = STToIJ(st,level);
192
193 return S2.S2Cell.FromFaceIJ(faceuv[0], ij, level);
194 };
195
196 S2.S2Cell.FromFaceIJ = function (face, ij, level) {
197 const cell = new S2.S2Cell();
198 cell.face = face;
199 cell.ij = ij;
200 cell.level = level;
201
202 return cell;
203 };
204
205 S2.S2Cell.prototype.toString = function () {
206 return 'F' + this.face + 'ij[' + this.ij[0] + ',' + this.ij[1] + ']@' + this.level;
207 };
208
209 S2.S2Cell.prototype.getLatLng = function () {
210 const st = IJToST(this.ij, this.level, [0.5, 0.5]);
211 const uv = STToUV(st);
212 const xyz = FaceUVToXYZ(this.face, uv);
213
214 return XYZToLatLng(xyz);
215 };
216
217 S2.S2Cell.prototype.getCornerLatLngs = function () {
218 const offsets = [
219 [0.0, 0.0],
220 [0.0, 1.0],
221 [1.0, 1.0],
222 [1.0, 0.0]
223 ];
224
225 return offsets.map(offset => {
226 const st = IJToST(this.ij, this.level, offset);
227 const uv = STToUV(st);
228 const xyz = FaceUVToXYZ(this.face, uv);
229
230 return XYZToLatLng(xyz);
231 });
232 };
233
234 S2.S2Cell.prototype.getNeighbors = function (deltas) {
235
236 const fromFaceIJWrap = function (face,ij,level) {
237 const maxSize = 1 << level;
238 if (ij[0] >= 0 && ij[1] >= 0 && ij[0] < maxSize && ij[1] < maxSize) {
239 // no wrapping out of bounds
240 return S2.S2Cell.FromFaceIJ(face,ij,level);
241 }
242
243 // the new i,j are out of range.
244 // with the assumption that they're only a little past the borders we can just take the points as
245 // just beyond the cube face, project to XYZ, then re-create FaceUV from the XYZ vector
246 let st = IJToST(ij,level,[0.5, 0.5]);
247 let uv = STToUV(st);
248 let xyz = FaceUVToXYZ(face, uv);
249 const faceuv = XYZToFaceUV(xyz);
250 face = faceuv[0];
251 uv = faceuv[1];
252 st = UVToST(uv);
253 ij = STToIJ(st,level);
254 return S2.S2Cell.FromFaceIJ(face, ij, level);
255 };
256
257 const face = this.face;
258 const i = this.ij[0];
259 const j = this.ij[1];
260 const level = this.level;
261
262 if (!deltas) {
263 deltas = [
264 {a: -1, b: 0},
265 {a: 0, b: -1},
266 {a: 1, b: 0},
267 {a: 0, b: 1}
268 ];
269 }
270 return deltas.map(function (values) {
271 return fromFaceIJWrap(face, [i + values.a, j + values.b], level);
272 });
273 };
274
275/** Our code
276* For safety, S2 must be initialized before our code
277*/
278
279 // based on https://github.com/iatkin/leaflet-svgicon
280 function initSvgIcon() {
281 L.DivIcon.SVGIcon = L.DivIcon.extend({
282 options: {
283 'className': 'svg-icon',
284 'iconAnchor': null, //defaults to [iconSize.x/2, iconSize.y] (point tip)
285 'iconSize': L.point(48, 48)
286 },
287 initialize: function (options) {
288 options = L.Util.setOptions(this, options);
289
290 //iconSize needs to be converted to a Point object if it is not passed as one
291 options.iconSize = L.point(options.iconSize);
292
293 if (!options.iconAnchor) {
294 options.iconAnchor = L.point(Number(options.iconSize.x) / 2, Number(options.iconSize.y));
295 } else {
296 options.iconAnchor = L.point(options.iconAnchor);
297 }
298 },
299
300 // https://github.com/tonekk/Leaflet-Extended-Div-Icon/blob/master/extended.divicon.js#L13
301 createIcon: function (oldIcon) {
302 let div = L.DivIcon.prototype.createIcon.call(this, oldIcon);
303
304 if (this.options.id) {
305 div.id = this.options.id;
306 }
307
308 if (this.options.style) {
309 for (let key in this.options.style) {
310 div.style[key] = this.options.style[key];
311 }
312 }
313 return div;
314 }
315 });
316
317 L.divIcon.svgIcon = function (options) {
318 return new L.DivIcon.SVGIcon(options);
319 };
320
321 L.Marker.SVGMarker = L.Marker.extend({
322 options: {
323 'iconFactory': L.divIcon.svgIcon,
324 'iconOptions': {}
325 },
326 initialize: function (latlng, options) {
327 options = L.Util.setOptions(this, options);
328 options.icon = options.iconFactory(options.iconOptions);
329 this._latlng = latlng;
330 },
331 onAdd: function (map) {
332 L.Marker.prototype.onAdd.call(this, map);
333 }
334 });
335
336 L.marker.svgMarker = function (latlng, options) {
337 return new L.Marker.SVGMarker(latlng, options);
338 };
339 }
340
341 /**
342 * Saves a file to disk with the provided text
343 * @param {string} text - The text to save
344 * @param {string} filename - Proposed filename
345 */
346 function saveToFile(text, filename) {
347 if (typeof text != 'string') {
348 text = JSON.stringify(text);
349 }
350
351 if (typeof window.android !== 'undefined' && window.android.saveFile) {
352 window.android.saveFile(filename, 'application/json', text);
353 return;
354 }
355
356 if (isIITCm()) {
357 promptForCopy(text);
358 return;
359 }
360
361 const element = document.createElement('a');
362
363 // http://stackoverflow.com/questions/13405129/javascript-create-and-save-file
364 const file = new Blob([text], {type: 'text/plain'});
365 const objectURL = URL.createObjectURL(file);
366 element.setAttribute('href', objectURL);
367
368 element.setAttribute('download', filename);
369
370 element.style.display = 'none';
371 document.body.appendChild(element);
372
373 element.click();
374
375 setTimeout(function() {
376 document.body.removeChild(element);
377 URL.revokeObjectURL(objectURL);
378 }, 0);
379 }
380
381 /**
382 * Prompts the user to select a file and then reads its contents and calls the callback function with those contents
383 * @param {Function} callback - Function that will be called when the file is read.
384 * Callback signature: function( {string} contents ) {}
385 */
386 function readFromFile(callback) {
387 // special hook from iitcm
388 if (typeof window.requestFile != 'undefined') {
389 window.requestFile(function (filename, content) {
390 callback(content);
391 });
392 return;
393 }
394
395 if (isIITCm()) {
396 promptForPaste(callback);
397 return;
398 }
399
400 const input = document.createElement('input');
401 input.type = 'file';
402 document.body.appendChild(input);
403
404 input.addEventListener('change', function () {
405 const reader = new FileReader();
406 reader.onload = function () {
407 callback(reader.result);
408 };
409 reader.readAsText(input.files[0]);
410 document.body.removeChild(input);
411 }, false);
412
413 input.click();
414 }
415
416 function promptForPaste(callback) {
417 const div = document.createElement('div');
418
419 const textarea = document.createElement('textarea');
420 textarea.style.width = '100%';
421 textarea.style.minHeight = '8em';
422 div.appendChild(textarea);
423
424 const container = dialog({
425 id: 'promptForPaste',
426 html: div,
427 width: '360px',
428 title: 'Paste here the data',
429 buttons: {
430 OK: function () {
431 container.dialog('close');
432 callback(textarea.value);
433 }
434 }
435 });
436 }
437
438 function promptForCopy(text) {
439 const div = document.createElement('div');
440
441 const textarea = document.createElement('textarea');
442 textarea.style.width = '100%';
443 textarea.style.minHeight = '8em';
444 textarea.value = text;
445 div.appendChild(textarea);
446
447 const container = dialog({
448 id: 'promptForCopy',
449 html: div,
450 width: '360px',
451 title: 'Copy this data',
452 buttons: {
453 OK: function () {
454 container.dialog('close');
455 }
456 }
457 });
458 }
459
460 const TIMERS = {};
461 function createThrottledTimer(name, callback, ms) {
462 if (TIMERS[name])
463 clearTimeout(TIMERS[name]);
464
465 // throttle if there are several calls to the functions
466 TIMERS[name] = setTimeout(function() {
467 delete TIMERS[name];
468 if (typeof window.requestIdleCallback == 'undefined')
469 callback();
470 else
471 // and even now, wait for iddle
472 requestIdleCallback(function() {
473 callback();
474 }, { timeout: 2000 });
475
476 }, ms || 100);
477 }
478
479 /**
480 * Try to identify if the browser is IITCm due to special bugs like file picker not working
481 */
482 function isIITCm() {
483 const ua = navigator.userAgent;
484 if (!ua.match(/Android.*Mobile/))
485 return false;
486
487 if (ua.match(/; wb\)/))
488 return true;
489
490 return ua.match(/ Version\//);
491 }
492
493 function is_iOS() {
494 const ua = navigator.userAgent;
495 return (ua.includes('iPhone') || ua.includes('iPad'))
496 }
497
498 let pokestops = {};
499 let gyms = {};
500 // Portals that aren't marked as PoGo items
501 let notpogo = {};
502
503 let allPortals = {};
504 let newPortals = {};
505 let checkNewPortalsTimer;
506 let relayoutTimer; // timer for relayout when portal is added
507
508 // Portals that the user hasn't classified as Pokestops (2 or more in the same Lvl17 cell)
509 let skippedPortals = {};
510 let newPokestops = {};
511 let notClassifiedPokestops = [];
512
513 // Portals that we know, but that have been moved from our stored location.
514 let movedPortals = [];
515 // Pogo items that are no longer available.
516 let missingPortals = {};
517
518 // Cells currently detected with extra gyms
519 let cellsExtraGyms = {};
520 // Cells that the user has marked to ignore extra gyms
521 let ignoredCellsExtraGyms = {};
522 // Cells with missing Gyms
523 let ignoredCellsMissingGyms = {};
524
525 // Leaflet layers
526 let regionLayer; // parent layer
527 let stopLayerGroup; // pokestops
528 let gymLayerGroup; // gyms
529 let nearbyLayerGroup; // circles to mark the too near limit
530 let gridLayerGroup; // s2 grid
531 let cellLayerGroup; // cell shading and borders
532 let gymCenterLayerGroup; // gym centers
533
534 // Group of items added to the layer
535 let stopLayers = {};
536 let gymLayers = {};
537 let nearbyCircles = {};
538
539 const gymCellLevel = 14; // the cell level which is considered when counting POIs to determine # of gyms
540 const poiCellLevel = 17; // the cell level where there can only be 1 POI translated to pogo
541
542 const defaultSettings = {
543 highlightGymCandidateCells: true,
544 highlightGymCenter: false,
545 thisIsPogo: false,
546 analyzeForMissingData: true,
547 grids: [
548 {
549 level: gymCellLevel,
550 width: 5,
551 color: '#004D40',
552 opacity: 0.5
553 },
554 {
555 level: poiCellLevel,
556 width: 2,
557 color: '#388E3C',
558 opacity: 0.5
559 }
560 ],
561 colors: {
562 cellsExtraGyms: {
563 color: '#ff0000',
564 opacity: 0.5
565 },
566 cellsMissingGyms: {
567 color: '#ffa500',
568 opacity: 0.5
569 },
570 cell17Filled: {
571 color: '#000000',
572 opacity: 0.6
573 },
574 cell14Filled: {
575 color: '#000000',
576 opacity: 0.5
577 },
578 nearbyCircleBorder: {
579 color: '#000000',
580 opacity: 0.6
581 },
582 nearbyCircleFill: {
583 color: '#000000',
584 opacity: 0.4
585 },
586 missingStops1: {
587 color: '#BF360C',
588 opacity: 1
589 },
590 missingStops2: {
591 color: '#E64A19',
592 opacity: 1
593 },
594 missingStops3: {
595 color: '#FF5722',
596 opacity: 1
597 }
598 },
599 saveDataType: 'Gyms',
600 saveDataFormat: 'CSV'
601 };
602
603 let settings = defaultSettings;
604
605 function saveSettings() {
606 createThrottledTimer('saveSettings', function() {
607 localStorage[KEY_SETTINGS] = JSON.stringify(settings);
608 });
609 }
610
611 function loadSettings() {
612 const tmp = localStorage[KEY_SETTINGS];
613 if (!tmp) {
614 loadOldSettings();
615 return;
616 }
617 try {
618 settings = JSON.parse(tmp);
619 } catch (e) { // eslint-disable-line no-empty
620 }
621
622 setThisIsPogo();
623 }
624
625 /**
626 * Migrate from old key to new one in order to avoid conflict with other plugin that reused this code.
627 */
628 function loadOldSettings() {
629 const tmp = localStorage['s2check_settings'];
630 if (!tmp)
631 return;
632 try {
633 settings = JSON.parse(tmp);
634 } catch (e) { // eslint-disable-line no-empty
635 }
636 if (typeof settings.analyzeForMissingData == 'undefined') {
637 settings.analyzeForMissingData = true;
638 }
639 if (typeof settings.promptForMissingData != 'undefined') {
640 delete settings.promptForMissingData;
641 }
642 if (!settings.colors) {
643 resetColors();
644 }
645 if (typeof settings.saveDataType == 'undefined') {
646 settings.saveDataType = 'Gyms';
647 }
648 if (typeof settings.saveDataFormat == 'undefined') {
649 settings.saveDataFormat = 'CSV';
650 }
651
652 setThisIsPogo();
653
654 // migrate key
655 localStorage.removeItem('s2check_settings');
656 thisPlugin.saveStorage();
657 }
658
659 function resetColors() {
660 settings.grids[0].color = defaultSettings.grids[0].color;
661 settings.grids[0].opacity = defaultSettings.grids[0].opacity;
662 settings.grids[1].color = defaultSettings.grids[1].color;
663 settings.grids[1].opacity = defaultSettings.grids[1].opacity;
664 settings.colors = defaultSettings.colors;
665 }
666
667 let originalHighlightPortal;
668
669 function setThisIsPogo() {
670 document.body.classList[settings.thisIsPogo ? 'add' : 'remove']('thisIsPogo');
671 // It seems that iOS has some bug in the following code, but I can't debug it.
672 if (is_iOS())
673 return;
674
675 try
676 {
677 if (settings.thisIsPogo) {
678 removeIngressLayers();
679 if (window._current_highlighter == window._no_highlighter) {
680 // extracted from IITC plugin: Hide portal ownership
681 originalHighlightPortal = window.highlightPortal;
682 window.highlightPortal = function(portal) {
683 window.portalMarkerScale();
684 const hidePortalOwnershipStyles = window.getMarkerStyleOptions({team: window.TEAM_NONE, level: 0});
685 portal.setStyle(hidePortalOwnershipStyles);
686 };
687 window.resetHighlightedPortals();
688 }
689 } else {
690 restoreIngressLayers();
691 if (originalHighlightPortal != null) {
692 window.highlightPortal = originalHighlightPortal;
693 originalHighlightPortal = null;
694 window.resetHighlightedPortals();
695 }
696 }
697 }
698 catch (e)
699 {
700 alert('Error initializing ThisIsPogo');
701 console.log(e); // eslint-disable-line no-console
702 }
703 }
704
705 function sortByName(a, b) {
706 if (!a.name)
707 return -1;
708
709 return a.name.localeCompare(b.name);
710 }
711
712 function isCellOnScreen(mapBounds, cell) {
713 const corners = cell.getCornerLatLngs();
714 const cellBounds = L.latLngBounds([corners[0],corners[1]]).extend(corners[2]).extend(corners[3]);
715 return cellBounds.intersects(mapBounds);
716 }
717
718 // return only the cells that are visible by the map bounds to ignore far away data that might not be complete
719 function filterWithinScreen(cells) {
720 const bounds = map.getBounds();
721 const filtered = {};
722 Object.keys(cells).forEach(cellId => {
723 const cellData = cells[cellId];
724 const cell = cellData.cell;
725
726 if (isCellInsideScreen(bounds, cell)) {
727 filtered[cellId] = cellData;
728 }
729 });
730 return filtered;
731 }
732
733 function isCellInsideScreen(mapBounds, cell) {
734 const corners = cell.getCornerLatLngs();
735 const cellBounds = L.latLngBounds([corners[0],corners[1]]).extend(corners[2]).extend(corners[3]);
736 return mapBounds.contains(cellBounds);
737 }
738
739 /**
740 * Filter a group of items (gyms/stops) excluding those out of the screen
741 */
742 function filterItemsByMapBounds(items) {
743 const bounds = map.getBounds();
744 const filtered = {};
745 Object.keys(items).forEach(id => {
746 const item = items[id];
747
748 if (isPointOnScreen(bounds, item)) {
749 filtered[id] = item;
750 }
751 });
752 return filtered;
753 }
754
755 function isPointOnScreen(mapBounds, point) {
756 if (point._latlng)
757 return mapBounds.contains(point._latlng);
758
759 return mapBounds.contains(L.latLng(point));
760 }
761
762 function groupByCell(level) {
763 const cells = {};
764 classifyGroup(cells, gyms, level, (cell, item) => cell.gyms.push(item));
765 classifyGroup(cells, pokestops, level, (cell, item) => cell.stops.push(item));
766 classifyGroup(cells, newPortals, level, (cell, item) => cell.notClassified.push(item));
767 classifyGroup(cells, notpogo, level, (cell, item) => {/* */});
768
769 return cells;
770 }
771
772 function classifyGroup(cells, items, level, callback) {
773 Object.keys(items).forEach(id => {
774 const item = items[id];
775 if (!item.cells) {
776 item.cells = {};
777 }
778 let cell;
779 // Compute the cell only once for each level
780 if (!item.cells[level]) {
781 cell = S2.S2Cell.FromLatLng(item, level);
782 item.cells[level] = cell.toString();
783 }
784 const cellId = item.cells[level];
785
786 // Add it to the array of gyms of that cell
787 if (!cells[cellId]) {
788 if (!cell) {
789 cell = S2.S2Cell.FromLatLng(item, level);
790 }
791 cells[cellId] = {
792 cell: cell,
793 gyms: [],
794 stops: [],
795 notClassified: []
796 };
797 }
798 callback(cells[cellId], item);
799 });
800 }
801
802 /**
803 * Returns the items that belong to the specified cell
804 */
805 function findCellItems(cellId, level, items) {
806 return Object.values(items).filter(item => item.cells[level] == cellId);
807 }
808
809 /**
810 Tries to add the portal photo when exporting from Ingress.com/intel
811 */
812 function findPhotos(items) {
813 if (!window.portals) {
814 return items;
815 }
816 Object.keys(items).forEach(id => {
817 const item = items[id];
818 if (item.image)
819 return;
820
821 const portal = window.portals[id];
822 if (portal && portal.options && portal.options.data) {
823 item.image = portal.options.data.image;
824 }
825 });
826 return items;
827 }
828
829 function configureGridLevelSelect(select, i) {
830 select.value = settings.grids[i].level;
831 select.addEventListener('change', e => {
832 settings.grids[i].level = parseInt(select.value, 10);
833 saveSettings();
834 updateMapGrid();
835 });
836 }
837
838 function showS2Dialog() {
839 const selectRow = `
840 <p>{{level}} level of grid to display: <select>
841 <option value=0>None</option>
842 <option value=6>6</option>
843 <option value=7>7</option>
844 <option value=8>8</option>
845 <option value=9>9</option>
846 <option value=10>10</option>
847 <option value=11>11</option>
848 <option value=12>12</option>
849 <option value=13>13</option>
850 <option value=14>14</option>
851 <option value=15>15</option>
852 <option value=16>16</option>
853 <option value=17>17</option>
854 <option value=18>18</option>
855 <option value=19>19</option>
856 <option value=20>20</option>
857 </select></p>`;
858
859 const html =
860 selectRow.replace('{{level}}', '1st') +
861 selectRow.replace('{{level}}', '2nd') +
862 `<p><input type="checkbox" id="chkHighlightCandidates" /><label for="chkHighlightCandidates">Highlight Cells that might get a Gym</label></p>
863 <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>
864 <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>
865 <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>
866 <p><a id='PogoEditColors'>Colors</a></p>
867 `;
868
869 const container = dialog({
870 id: 's2Settings',
871 width: 'auto',
872 html: html,
873 title: 'S2 & Pokemon Settings'
874 });
875
876 const div = container[0];
877
878 const selects = div.querySelectorAll('select');
879 for (let i = 0; i < 2; i++) {
880 configureGridLevelSelect(selects[i], i);
881 }
882
883 const chkHighlight = div.querySelector('#chkHighlightCandidates');
884 chkHighlight.checked = settings.highlightGymCandidateCells;
885
886 chkHighlight.addEventListener('change', e => {
887 settings.highlightGymCandidateCells = chkHighlight.checked;
888 saveSettings();
889 updateMapGrid();
890 });
891
892 const chkHighlightCenters = div.querySelector('#chkHighlightCenters');
893 chkHighlightCenters.checked = settings.highlightGymCenter;
894 chkHighlightCenters.addEventListener('change', e => {
895 settings.highlightGymCenter = chkHighlightCenters.checked;
896 saveSettings();
897 updateMapGrid();
898 });
899
900 const chkThisIsPogo = div.querySelector('#chkThisIsPogo');
901 chkThisIsPogo.checked = !!settings.thisIsPogo;
902 chkThisIsPogo.addEventListener('change', e => {
903 settings.thisIsPogo = chkThisIsPogo.checked;
904 saveSettings();
905 setThisIsPogo();
906 });
907
908 const chkanalyzeForMissingData = div.querySelector('#chkanalyzeForMissingData');
909 chkanalyzeForMissingData.checked = !!settings.analyzeForMissingData;
910 chkanalyzeForMissingData.addEventListener('change', e => {
911 settings.analyzeForMissingData = chkanalyzeForMissingData.checked;
912 saveSettings();
913 if (newPortals.length > 0) {
914 checkNewPortals();
915 }
916 });
917
918 const PogoEditColors = div.querySelector('#PogoEditColors');
919 PogoEditColors.addEventListener('click', function (e) {
920 editColors();
921 e.preventDefault();
922 return false;
923 });
924 }
925
926 function editColors() {
927 const selectRow = `<p class='pogo-colors'>{{title}}<br>
928 Color: <input type='color' id='{{id}}Color'> Opacity: <select id='{{id}}Opacity'>
929 <option value=0>0</option>
930 <option value=0.1>0.1</option>
931 <option value=0.2>0.2</option>
932 <option value=0.3>0.3</option>
933 <option value=0.4>0.4</option>
934 <option value=0.5>0.5</option>
935 <option value=0.6>0.6</option>
936 <option value=0.7>0.7</option>
937 <option value=0.8>0.8</option>
938 <option value=0.9>0.9</option>
939 <option value=1>1</option>
940 </select>{{width}}</p>`;
941
942 const html =
943 selectRow.replace('{{title}}', '1st Grid').replace(`{{width}}`, ` Width: <input type='number' min='1' max='8' id='{{id}}Width' size='2'> `).replace(/{{id}}/g, 'grid0') +
944 selectRow.replace('{{title}}', '2nd Grid').replace(`{{width}}`, ` Width: <input type='number' min='1' max='8' id='{{id}}Width' size='2'> `).replace(/{{id}}/g, 'grid1') +
945 selectRow.replace('{{title}}', 'Cells with extra gyms').replace(/{{id}}/g, 'cellsExtraGyms').replace(`{{width}}`, '') +
946 selectRow.replace('{{title}}', 'Cells with missing gyms').replace(/{{id}}/g, 'cellsMissingGyms').replace(`{{width}}`, '') +
947 selectRow.replace('{{title}}', `Cell ${poiCellLevel} with a gym or stop`).replace(/{{id}}/g, 'cell17Filled').replace(`{{width}}`, '') +
948 selectRow.replace('{{title}}', `Cell ${gymCellLevel} with 3 gyms`).replace(/{{id}}/g, 'cell14Filled').replace(`{{width}}`, '') +
949 selectRow.replace('{{title}}', '20m submit radius border').replace(/{{id}}/g, 'nearbyCircleBorder').replace(`{{width}}`, '') +
950 selectRow.replace('{{title}}', '20m submit radius fill').replace(/{{id}}/g, 'nearbyCircleFill').replace(`{{width}}`, '') +
951 selectRow.replace('{{title}}', '1 more stop to get a gym').replace(/{{id}}/g, 'missingStops1').replace(`{{width}}`, '') +
952 selectRow.replace('{{title}}', '2 more stops to get a gym').replace(/{{id}}/g, 'missingStops2').replace(`{{width}}`, '') +
953 selectRow.replace('{{title}}', '3 more stops to get a gym').replace(/{{id}}/g, 'missingStops3').replace(`{{width}}`, '') +
954 '<a id="resetColorsLink">Reset all colors</a>'
955 ;
956
957 const container = dialog({
958 id: 's2Colors',
959 width: 'auto',
960 html: html,
961 title: 'PoGo Grid Colors'
962 });
963
964 const div = container[0];
965
966 const updatedSetting = function (id) {
967 saveSettings();
968 if (id == 'nearbyCircleBorder' || id == 'nearbyCircleFill') {
969 redrawNearbyCircles();
970 } else {
971 updateMapGrid();
972 }
973 };
974
975 const configureItems = function (key, item, id) {
976 if (!id)
977 id = item;
978
979 const entry = settings[key][item];
980 const select = div.querySelector('#' + id + 'Opacity');
981 select.value = entry.opacity;
982 select.addEventListener('change', function (event) {
983 settings[key][item].opacity = select.value;
984 updatedSetting(id);
985 });
986
987 const input = div.querySelector('#' + id + 'Color');
988 input.value = entry.color;
989 input.addEventListener('change', function (event) {
990 settings[key][item].color = input.value;
991 updatedSetting(id);
992 });
993
994 if (entry.width != null) {
995 const widthInput = div.querySelector('#' + id + 'Width');
996 widthInput.value = entry.width;
997 widthInput.addEventListener('change', function (event) {
998 settings[key][item].width = widthInput.value;
999 updatedSetting(id);
1000 });
1001 }
1002 };
1003
1004 configureItems('grids', 0, 'grid0');
1005 configureItems('grids', 1, 'grid1');
1006 configureItems('colors', 'cellsExtraGyms');
1007 configureItems('colors', 'cellsMissingGyms');
1008 configureItems('colors', 'cell17Filled');
1009 configureItems('colors', 'cell14Filled');
1010 configureItems('colors', 'nearbyCircleBorder');
1011 configureItems('colors', 'nearbyCircleFill');
1012 configureItems('colors', 'missingStops1');
1013 configureItems('colors', 'missingStops2');
1014 configureItems('colors', 'missingStops3');
1015
1016 const resetColorsLink = div.querySelector('#resetColorsLink');
1017 resetColorsLink.addEventListener('click', function() {
1018 container.dialog('close');
1019 resetColors();
1020 updatedSetting('nearbyCircleBorder');
1021 updatedSetting();
1022 editColors();
1023 });
1024 }
1025
1026 /**
1027 * Refresh the S2 grid over the map
1028 */
1029 function updateMapGrid() {
1030 // preconditions
1031 if (!map.hasLayer(regionLayer)) {
1032 return;
1033 }
1034 const zoom = map.getZoom();
1035
1036 // first draw nearby circles at the bottom
1037 if (zoom > 16) {
1038 if (!regionLayer.hasLayer(nearbyLayerGroup)) {
1039 regionLayer.addLayer(nearbyLayerGroup);
1040 }
1041 nearbyLayerGroup.bringToBack();
1042 } else if (regionLayer.hasLayer(nearbyLayerGroup)) {
1043 regionLayer.removeLayer(nearbyLayerGroup);
1044 }
1045
1046 // shade level 14 and level 17 cells
1047 let cellsCloseToThreshold;
1048 if (settings.highlightGymCandidateCells && zoom > 14) {
1049 cellsCloseToThreshold = updateCandidateCells(zoom);
1050 if (!regionLayer.hasLayer(cellLayerGroup)) {
1051 regionLayer.addLayer(cellLayerGroup);
1052 }
1053 cellLayerGroup.bringToBack();
1054 } else if (regionLayer.hasLayer(cellLayerGroup)) {
1055 regionLayer.removeLayer(cellLayerGroup);
1056 }
1057
1058 // then draw the cell grid
1059 if (zoom > 4) {
1060 drawCellGrid(zoom);
1061
1062 // update cell grid with cells close to a threshold for a gym
1063 if (cellsCloseToThreshold) {
1064 // draw missing cells in reverse order
1065 for (let missingStops = 3; missingStops >= 1; missingStops--) {
1066 const color = settings.colors['missingStops' + missingStops].color;
1067 const opacity = settings.colors['missingStops' + missingStops].opacity;
1068 cellsCloseToThreshold[missingStops].forEach(cell => gridLayerGroup.addLayer(drawCell(cell, color, 3, opacity)));
1069 }
1070 }
1071
1072 if (!regionLayer.hasLayer(gridLayerGroup)) {
1073 regionLayer.addLayer(gridLayerGroup);
1074 }
1075 } else if (regionLayer.hasLayer(gridLayerGroup)) {
1076 regionLayer.removeLayer(gridLayerGroup);
1077 }
1078
1079 // update gym centers
1080 if (settings.highlightGymCenter && zoom > 16) {
1081 updateGymCenters();
1082 if (!regionLayer.hasLayer(gymCenterLayerGroup)) {
1083 regionLayer.addLayer(gymCenterLayerGroup);
1084 }
1085 } else if (regionLayer.hasLayer(gymCenterLayerGroup)) {
1086 regionLayer.removeLayer(gymCenterLayerGroup);
1087 }
1088 }
1089
1090 function getLatLngPoint(data) {
1091 const result = {
1092 lat: typeof data.lat == 'function' ? data.lat() : data.lat,
1093 lng: typeof data.lng == 'function' ? data.lng() : data.lng
1094 };
1095
1096 return result;
1097 }
1098
1099 /**
1100 * Highlight cells that are missing a few stops to get another gym. Also fills level 17 cells with a stop/gym.
1101 * based on data from https://www.reddit.com/r/TheSilphRoad/comments/7ppb3z/gyms_pok%C3%A9stops_and_s2_cells_followup_research/
1102 * Cut offs: 2, 6, 20
1103 */
1104 function updateCandidateCells(zoom) {
1105 cellLayerGroup.clearLayers();
1106
1107 // All cells with items
1108 const allCells = groupByCell(gymCellLevel);
1109
1110 const bounds = map.getBounds();
1111 const seenCells = {};
1112 const cellsCloseToThreshold = {
1113 1: [],
1114 2: [],
1115 3: []
1116 };
1117
1118 const drawCellAndNeighbors = function (cell) {
1119 const cellStr = cell.toString();
1120
1121 if (!seenCells[cellStr]) {
1122 // cell not visited - flag it as visited now
1123 seenCells[cellStr] = true;
1124
1125 if (isCellOnScreen(bounds, cell)) {
1126 // on screen - draw it
1127 const cellData = allCells[cellStr];
1128 if (cellData) {
1129 // check for errors
1130 const missingGyms = computeMissingGyms(cellData);
1131 if (missingGyms > 0 && !ignoredCellsMissingGyms[cellStr]) {
1132 cellLayerGroup.addLayer(fillCell(cell, settings.colors.cellsMissingGyms.color, settings.colors.cellsMissingGyms.opacity));
1133 } else if (missingGyms < 0 && !ignoredCellsExtraGyms[cellStr]) {
1134 cellLayerGroup.addLayer(fillCell(cell, settings.colors.cellsExtraGyms.color, settings.colors.cellsExtraGyms.opacity));
1135 if (!cellsExtraGyms[cellStr]) {
1136 cellsExtraGyms[cellStr] = true;
1137 updateCounter('extraGyms', Object.keys(cellsExtraGyms));
1138 }
1139 }
1140
1141 // shade filled level 17 cells
1142 if (zoom > 15) {
1143 const coverLevel17Cell = function(point) {
1144 const cell = S2.S2Cell.FromLatLng(point, poiCellLevel);
1145 cellLayerGroup.addLayer(fillCell(cell, settings.colors.cell17Filled.color, settings.colors.cell17Filled.opacity));
1146 };
1147
1148 cellData.gyms.forEach(coverLevel17Cell);
1149 cellData.stops.forEach(coverLevel17Cell);
1150 }
1151
1152 // number of stops to next gym
1153 const missingStops = computeMissingStops(cellData);
1154 switch (missingStops) {
1155 case 0:
1156 if (missingGyms <= 0) {
1157 cellLayerGroup.addLayer(fillCell(cell, settings.colors.cell14Filled.color, settings.colors.cell14Filled.opacity));
1158 }
1159 break;
1160 case 1:
1161 case 2:
1162 case 3:
1163 cellsCloseToThreshold[missingStops].push(cell);
1164 cellLayerGroup.addLayer(writeInCell(cell, missingStops));
1165 break;
1166 default:
1167 cellLayerGroup.addLayer(writeInCell(cell, missingStops));
1168 break;
1169 }
1170 }
1171
1172 // and recurse to our neighbors
1173 const neighbors = cell.getNeighbors();
1174 for (let i = 0; i < neighbors.length; i++) {
1175 drawCellAndNeighbors(neighbors[i]);
1176 }
1177 }
1178 }
1179 };
1180
1181 const cell = S2.S2Cell.FromLatLng(getLatLngPoint(map.getCenter()), gymCellLevel);
1182 drawCellAndNeighbors(cell);
1183
1184 return cellsCloseToThreshold;
1185 }
1186
1187 function drawCellGrid(zoom) {
1188 // clear, to redraw
1189 gridLayerGroup.clearLayers();
1190
1191 const bounds = map.getBounds();
1192 const seenCells = {};
1193 const drawCellAndNeighbors = function (cell, color, width, opacity) {
1194 const cellStr = cell.toString();
1195
1196 if (!seenCells[cellStr]) {
1197 // cell not visited - flag it as visited now
1198 seenCells[cellStr] = true;
1199
1200 if (isCellOnScreen(bounds, cell)) {
1201 // on screen - draw it
1202 gridLayerGroup.addLayer(drawCell(cell, color, width, opacity));
1203
1204 // and recurse to our neighbors
1205 const neighbors = cell.getNeighbors();
1206 for (let i = 0; i < neighbors.length; i++) {
1207 drawCellAndNeighbors(neighbors[i], color, width, opacity);
1208 }
1209 }
1210 }
1211 };
1212
1213 for (let i = settings.grids.length - 1; i >= 0; --i) {
1214 const grid = settings.grids[i];
1215 const gridLevel = grid.level;
1216 if (gridLevel >= 6 && gridLevel < (zoom + 2)) {
1217 const cell = S2.S2Cell.FromLatLng(getLatLngPoint(map.getCenter()), gridLevel);
1218 drawCellAndNeighbors(cell, grid.color, grid.width, grid.opacity);
1219 }
1220 }
1221
1222 return gridLayerGroup;
1223 }
1224
1225 /**
1226 * Draw a cross to the center of level 20 cells that have a Gym to check better EX locations
1227 */
1228 function updateGymCenters() {
1229 // clear
1230 gymCenterLayerGroup.clearLayers();
1231
1232 const visibleGyms = filterItemsByMapBounds(gyms);
1233 const level = 20;
1234
1235 Object.keys(visibleGyms).forEach(id => {
1236 const gym = gyms[id];
1237 const cell = S2.S2Cell.FromLatLng(gym, level);
1238 const corners = cell.getCornerLatLngs();
1239 // center point
1240 const center = cell.getLatLng();
1241
1242 const style = {fill: false, color: 'red', opacity: 0.8, weight: 1, clickable: false, interactive: false};
1243 const line1 = L.polyline([corners[0], corners[2]], style);
1244 gymCenterLayerGroup.addLayer(line1);
1245
1246 const line2 = L.polyline([corners[1], corners[3]], style);
1247 gymCenterLayerGroup.addLayer(line2);
1248
1249 const circle = L.circle(center, 1, style);
1250 gymCenterLayerGroup.addLayer(circle);
1251 });
1252 }
1253
1254 // Computes how many new stops must be added to the L14 Cell to get a new Gym
1255 function computeMissingStops(cellData) {
1256 const gyms = cellData.gyms.length;
1257 const stops = cellData.stops.length;
1258 const sum = gyms + stops;
1259 if (sum < 2 && gyms == 0)
1260 return 2 - sum;
1261
1262 if (sum < 6 && gyms < 2)
1263 return 6 - sum;
1264
1265 if (sum < 20 && gyms < 3)
1266 return 20 - sum;
1267
1268 // No options to more gyms ATM.
1269 return 0;
1270 }
1271
1272 // Checks if the L14 cell has enough Gyms and Stops and one of the stops should be marked as a Gym
1273 // If the result is negative then it has extra gyms
1274 function computeMissingGyms(cellData) {
1275 const totalGyms = cellData.gyms.length;
1276 const sum = totalGyms + cellData.stops.length;
1277 if (sum < 2)
1278 return 0 - totalGyms;
1279
1280 if (sum < 6)
1281 return 1 - totalGyms;
1282
1283 if (sum < 20)
1284 return 2 - totalGyms;
1285
1286 return 3 - totalGyms;
1287 }
1288
1289 function drawCell(cell, color, weight, opacity) {
1290 // corner points
1291 const corners = cell.getCornerLatLngs();
1292
1293 // the level 6 cells have noticible errors with non-geodesic lines - and the larger level 4 cells are worse
1294 // NOTE: we only draw two of the edges. as we draw all cells on screen, the other two edges will either be drawn
1295 // from the other cell, or be off screen so we don't care
1296 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});
1297
1298 return region;
1299 }
1300
1301 function fillCell(cell, color, opacity) {
1302 // corner points
1303 const corners = cell.getCornerLatLngs();
1304
1305 const region = L.polygon(corners, {color: color, fillOpacity: opacity, weight: 0, clickable: false, interactive: false});
1306
1307 return region;
1308 }
1309
1310 /**
1311 * Writes a text in the center of a cell
1312 */
1313 function writeInCell(cell, text) {
1314 // center point
1315 let center = cell.getLatLng();
1316
1317 let marker = L.marker(center, {
1318 icon: L.divIcon({
1319 className: 'pogo-text',
1320 iconAnchor: [25, 5],
1321 iconSize: [50, 10],
1322 html: text
1323 }),
1324 interactive: false
1325 });
1326 // fixme, maybe add some click handler
1327
1328 return marker;
1329 }
1330
1331 // ***************************
1332 // IITC code
1333 // ***************************
1334
1335
1336 // ensure plugin framework is there, even if iitc is not yet loaded
1337 if (typeof window.plugin !== 'function') {
1338 window.plugin = function () {};
1339 }
1340
1341 // PLUGIN START ////////////////////////////////////////////////////////
1342
1343 // use own namespace for plugin
1344 window.plugin.pogo = function () {};
1345
1346 const thisPlugin = window.plugin.pogo;
1347 const KEY_STORAGE = 'plugin-pogo';
1348 const KEY_SETTINGS = 'plugin-pogo-settings';
1349 /*********************************************************************************************************************/
1350
1351 // Update the localStorage
1352 thisPlugin.saveStorage = function () {
1353 createThrottledTimer('saveStorage', function() {
1354 localStorage[KEY_STORAGE] = JSON.stringify({
1355 gyms: cleanUpExtraData(gyms),
1356 pokestops: cleanUpExtraData(pokestops),
1357 notpogo: cleanUpExtraData(notpogo),
1358 ignoredCellsExtraGyms: ignoredCellsExtraGyms,
1359 ignoredCellsMissingGyms: ignoredCellsMissingGyms
1360 });
1361 });
1362 };
1363
1364 /**
1365 * Create a new object where the extra properties of each pokestop/gym have been removed. Store only the minimum.
1366 */
1367 function cleanUpExtraData(group) {
1368 let newGroup = {};
1369 Object.keys(group).forEach(id => {
1370 const data = group[id];
1371 const newData = {
1372 guid: data.guid,
1373 lat: data.lat,
1374 lng: data.lng,
1375 name: data.name
1376 };
1377
1378 if (data.isEx)
1379 newData.isEx = data.isEx;
1380
1381 if (data.medal)
1382 newData.medal = data.medal;
1383
1384 newGroup[id] = newData;
1385 });
1386 return newGroup;
1387 }
1388
1389 // Load the localStorage
1390 thisPlugin.loadStorage = function () {
1391 const tmp = JSON.parse(localStorage[KEY_STORAGE] || '{}');
1392 gyms = tmp.gyms || {};
1393 pokestops = tmp.pokestops || {};
1394 notpogo = tmp.notpogo || {};
1395 ignoredCellsExtraGyms = tmp.ignoredCellsExtraGyms || {};
1396 ignoredCellsMissingGyms = tmp.ignoredCellsMissingGyms || {};
1397 };
1398
1399 thisPlugin.createEmptyStorage = function () {
1400 gyms = {};
1401 pokestops = {};
1402 notpogo = {};
1403 ignoredCellsExtraGyms = {};
1404 ignoredCellsMissingGyms = {};
1405 thisPlugin.saveStorage();
1406
1407 allPortals = {};
1408 newPortals = {};
1409
1410 movedPortals = [];
1411 missingPortals = {};
1412 };
1413
1414 /*************************************************************************/
1415
1416 thisPlugin.findByGuid = function (guid) {
1417 if (gyms[guid]) {
1418 return {'type': 'gyms', 'store': gyms};
1419 }
1420 if (pokestops[guid]) {
1421 return {'type': 'pokestops', 'store': pokestops};
1422 }
1423 if (notpogo[guid]) {
1424 return {'type': 'notpogo', 'store': notpogo};
1425 }
1426 return null;
1427 };
1428
1429 // Append a 'star' flag in sidebar.
1430 thisPlugin.onPortalSelectedPending = false;
1431 thisPlugin.onPortalSelected = function () {
1432 $('.pogoStop').remove();
1433 $('.pogoGym').remove();
1434 $('.notPogo').remove();
1435 const portalDetails = document.getElementById('portaldetails');
1436 portalDetails.classList.remove('isGym');
1437
1438 if (window.selectedPortal == null) {
1439 return;
1440 }
1441
1442 if (!thisPlugin.onPortalSelectedPending) {
1443 thisPlugin.onPortalSelectedPending = true;
1444
1445 setTimeout(function () { // the sidebar is constructed after firing the hook
1446 thisPlugin.onPortalSelectedPending = false;
1447
1448 $('.pogoStop').remove();
1449 $('.pogoGym').remove();
1450 $('.notPogo').remove();
1451
1452 // Show PoGo icons in the mobile status-bar
1453 if (thisPlugin.isSmart) {
1454 document.querySelector('.PogoStatus').innerHTML = thisPlugin.htmlStar;
1455 $('.PogoStatus > a').attr('title', '');
1456 }
1457
1458 $(portalDetails).append('<div class="PogoButtons">Pokemon Go: ' + thisPlugin.htmlStar + '</div>' +
1459 `<div id="PogoGymInfo">
1460 <label for='PogoGymMedal'>Medal:</label> <select id='PogoGymMedal'>
1461 <option value='None'>None</option>
1462 <option value='Bronze'>Bronze</option>
1463 <option value='Silver'>Silver</option>
1464 <option value='Gold'>Gold</option>
1465 </select><br>
1466 <label>Is this an EX gym? <input type='checkbox' id='PogoGymEx'> Yes</label><br>
1467 </div>`);
1468
1469 document.getElementById('PogoGymMedal').addEventListener('change', ev => {
1470 const guid = window.selectedPortal;
1471 const icon = document.getElementById('gym' + guid.replace('.', ''));
1472 // remove styling of gym marker
1473 if (icon) {
1474 icon.classList.remove(gyms[guid].medal + 'Medal');
1475 }
1476 gyms[guid].medal = ev.target.value;
1477 thisPlugin.saveStorage();
1478 // update gym marker
1479 if (icon) {
1480 icon.classList.add(gyms[guid].medal + 'Medal');
1481 }
1482 });
1483
1484 document.getElementById('PogoGymEx').addEventListener('change', ev => {
1485 const guid = window.selectedPortal;
1486 const icon = document.getElementById('gym' + guid.replace('.', ''));
1487 gyms[guid].isEx = ev.target.checked;
1488 thisPlugin.saveStorage();
1489 // update gym marker
1490 if (icon) {
1491 icon.classList[gyms[guid].isEx ? 'add' : 'remove']('exGym');
1492 }
1493 });
1494
1495 thisPlugin.updateStarPortal();
1496 }, 0);
1497 }
1498 };
1499
1500 // Update the status of the star (when a portal is selected from the map/pogo-list)
1501 thisPlugin.updateStarPortal = function () {
1502 $('.pogoStop').removeClass('favorite');
1503 $('.pogoGym').removeClass('favorite');
1504 $('.notPogo').removeClass('favorite');
1505 document.getElementById('portaldetails').classList.remove('isGym');
1506
1507 const guid = window.selectedPortal;
1508 // If current portal is into pogo: select pogo portal from portals list and select the star
1509 const pogoData = thisPlugin.findByGuid(guid);
1510 if (pogoData) {
1511 if (pogoData.type === 'pokestops') {
1512 $('.pogoStop').addClass('favorite');
1513 }
1514 if (pogoData.type === 'gyms') {
1515 $('.pogoGym').addClass('favorite');
1516 document.getElementById('portaldetails').classList.add('isGym');
1517 const gym = gyms[guid];
1518 if (gym.medal) {
1519 document.getElementById('PogoGymMedal').value = gym.medal;
1520 }
1521 document.getElementById('PogoGymEx').checked = gym.isEx;
1522
1523 }
1524 if (pogoData.type === 'notpogo') {
1525 $('.notPogo').addClass('favorite');
1526 }
1527 }
1528 };
1529
1530 function removePogoObject(type, guid) {
1531 if (type === 'pokestops') {
1532 delete pokestops[guid];
1533 const starInLayer = stopLayers[guid];
1534 stopLayerGroup.removeLayer(starInLayer);
1535 delete stopLayers[guid];
1536 }
1537 if (type === 'gyms') {
1538 delete gyms[guid];
1539 const gymInLayer = gymLayers[guid];
1540 gymLayerGroup.removeLayer(gymInLayer);
1541 delete gymLayers[guid];
1542 }
1543 if (type === 'notpogo') {
1544 delete notpogo[guid];
1545 }
1546 }
1547
1548 // Switch the status of the star
1549 thisPlugin.switchStarPortal = function (type) {
1550 const guid = window.selectedPortal;
1551
1552 // It has been manually classified, remove from the detection
1553 if (newPortals[guid])
1554 delete newPortals[guid];
1555
1556 // If portal is saved in pogo: Remove this pogo
1557 const pogoData = thisPlugin.findByGuid(guid);
1558 if (pogoData) {
1559 const existingType = pogoData.type;
1560 removePogoObject(existingType, guid);
1561
1562 thisPlugin.saveStorage();
1563 thisPlugin.updateStarPortal();
1564
1565 // Get portal name and coordinates
1566 const p = window.portals[guid];
1567 const ll = p.getLatLng();
1568 if (existingType !== type) {
1569 thisPlugin.addPortalpogo(guid, ll.lat, ll.lng, p.options.data.title, type);
1570 }
1571 // we've changed one item from pogo, if the cell was marked as ignored, reset it.
1572 if (updateExtraGymsCells(ll.lat, ll.lng))
1573 thisPlugin.saveStorage();
1574 } else {
1575 // If portal isn't saved in pogo: Add this pogo
1576
1577 // Get portal name and coordinates
1578 const portal = window.portals[guid];
1579 const latlng = portal.getLatLng();
1580 thisPlugin.addPortalpogo(guid, latlng.lat, latlng.lng, portal.options.data.title, type);
1581 }
1582
1583 if (settings.highlightGymCandidateCells) {
1584 updateMapGrid();
1585 }
1586 };
1587
1588 // Add portal
1589 thisPlugin.addPortalpogo = function (guid, lat, lng, name, type) {
1590 // Add pogo in the localStorage
1591 const obj = {'guid': guid, 'lat': lat, 'lng': lng, 'name': name};
1592
1593 // prevent that it would trigger the missing portal detection if it's in our data
1594 if (window.portals[guid]) {
1595 obj.exists = true;
1596 }
1597
1598 if (type == 'gyms') {
1599 gyms[guid] = obj;
1600 }
1601 if (type == 'pokestops') {
1602 pokestops[guid] = obj;
1603 }
1604 if (type == 'notpogo') {
1605 notpogo[guid] = obj;
1606 }
1607
1608 updateExtraGymsCells(lat, lng);
1609 thisPlugin.saveStorage();
1610 thisPlugin.updateStarPortal();
1611
1612 thisPlugin.addStar(guid, lat, lng, name, type);
1613 };
1614
1615 /**
1616 * An item has been changed in a cell, check if the cell should no longer be ignored
1617 */
1618 function updateExtraGymsCells(lat, lng) {
1619 if (Object.keys(ignoredCellsExtraGyms).length == 0 && Object.keys(ignoredCellsMissingGyms).length == 0)
1620 return false;
1621
1622 const cell = S2.S2Cell.FromLatLng(new L.LatLng(lat, lng), gymCellLevel);
1623 const cellId = cell.toString();
1624 if (ignoredCellsExtraGyms[cellId]) {
1625 delete ignoredCellsExtraGyms[cellId];
1626 return true;
1627 }
1628 if (ignoredCellsMissingGyms[cellId]) {
1629 delete ignoredCellsMissingGyms[cellId];
1630 return true;
1631 }
1632 return false;
1633 }
1634
1635 /*
1636 OPTIONS
1637 */
1638 // Manual import, export and reset data
1639 thisPlugin.pogoActionsDialog = function () {
1640 const content = `<div id="pogoSetbox">
1641 <a id="save-dialog" title="Select the data to save from the info on screen">Save...</a>
1642 <a onclick="window.plugin.pogo.optReset();return false;" title="Deletes all Pokemon Go markers">Reset PoGo portals</a>
1643 <a onclick="window.plugin.pogo.optImport();return false;" title="Import a JSON file with all the PoGo data">Import Gyms & Pokestops</a>
1644 <a onclick="window.plugin.pogo.optExport();return false;" title="Exports a JSON file with all the PoGo data">Export Gyms & Pokestops</a>
1645 </div>`;
1646
1647 const container = dialog({
1648 html: content,
1649 title: 'S2 & Pokemon Actions'
1650 });
1651
1652 const div = container[0];
1653 div.querySelector('#save-dialog').addEventListener('click', e => saveDialog());
1654 };
1655
1656 function saveDialog() {
1657 const content = `<div>
1658 <p>Select the data to save from the info on screen</p>
1659 <fieldset><legend>Which data?</legend>
1660 <input type='radio' name='PogoSaveDataType' value='Gyms' id='PogoSaveDataTypeGyms'><label for='PogoSaveDataTypeGyms'>Gyms</label><br>
1661 <input type='radio' name='PogoSaveDataType' value='PokeStopsGyms' id='PogoSaveDataTypePokeStopsGyms'><label for='PogoSaveDataTypePokeStopsGyms'>Pokestops + Gyms</label>
1662 </fieldset>
1663 <fieldset><legend>Format</legend>
1664 <input type='radio' name='PogoSaveDataFormat' value='CSV' id='PogoSaveDataFormatCSV'><label for='PogoSaveDataFormatCSV'>CSV</label><br>
1665 <input type='radio' name='PogoSaveDataFormat' value='JSON' id='PogoSaveDataFormatJSON'><label for='PogoSaveDataFormatJSON'>JSON</label>
1666 </fieldset>
1667 </div>`;
1668
1669 const container = dialog({
1670 html: content,
1671 title: 'Save visible data',
1672 buttons: {
1673 'Save': function () {
1674 const SaveDataType = document.querySelector('input[name="PogoSaveDataType"]:checked').value;
1675 const SaveDataFormat = document.querySelector('input[name="PogoSaveDataFormat"]:checked').value;
1676
1677 settings.saveDataType = SaveDataType;
1678 settings.saveDataFormat = SaveDataFormat;
1679 saveSettings();
1680
1681 container.dialog('close');
1682
1683 let filename = (SaveDataType == 'Gyms' ? 'gyms_' : 'gyms+stops_') + (new Date()).toISOString().substr(0, 19).replace(/[\D]/g, '_');
1684 if (SaveDataFormat == 'CSV') {
1685 filename += '.csv';
1686 const allData = SaveDataType == 'Gyms' ? gyms : Object.assign({}, gyms, pokestops);
1687 const data = filterItemsByMapBounds(allData);
1688 const keys = Object.keys(data);
1689 const contents = keys.map(id => {
1690 const gym = data[id];
1691 return (gym.name ? gym.name.replace(/,/g, ' ') + ',' : '') + gym.lat + ',' + gym.lng;
1692 });
1693
1694 saveToFile(contents.join('\n'), filename);
1695 } else {
1696 filename += '.json';
1697 const data = {
1698 gyms: findPhotos(cleanUpExtraData(filterItemsByMapBounds(gyms))),
1699 };
1700 if (SaveDataType != 'Gyms')
1701 data.pokestops = findPhotos(cleanUpExtraData(filterItemsByMapBounds(pokestops)));
1702
1703 saveToFile(JSON.stringify(data), filename);
1704 }
1705 }
1706 }
1707
1708 });
1709
1710 // Remove ok button
1711 const outer = container.parent();
1712 outer.find('.ui-dialog-buttonset button:first').remove();
1713
1714 const div = container[0];
1715 div.querySelector('#PogoSaveDataType' + settings.saveDataType).checked = true;
1716 div.querySelector('#PogoSaveDataFormat' + settings.saveDataFormat).checked = true;
1717
1718 };
1719
1720 thisPlugin.optAlert = function (message) {
1721 $('.ui-dialog .ui-dialog-buttonset').prepend('<p class="pogo-alert" style="float:left;margin-top:4px;">' + message + '</p>');
1722 $('.pogo-alert').delay(2500).fadeOut();
1723 };
1724
1725 thisPlugin.optExport = function () {
1726 saveToFile(localStorage[KEY_STORAGE], 'IITC-pogo.json');
1727 };
1728
1729 thisPlugin.optImport = function () {
1730 readFromFile(function (content) {
1731 try {
1732 const list = JSON.parse(content); // try to parse JSON first
1733 let importExStatus = true;
1734 let importGymMedal = true;
1735 Object.keys(list).forEach(type => {
1736 for (let idpogo in list[type]) {
1737 const item = list[type][idpogo];
1738 const lat = item.lat;
1739 const lng = item.lng;
1740 const name = item.name;
1741 let guid = item.guid;
1742 if (!guid) {
1743 guid = findPortalGuidByPositionE6(lat * 1E6, lng * 1E6);
1744 if (!guid) {
1745 console.log('portal guid not found', name, lat, lng); // eslint-disable-line no-console
1746 guid = idpogo;
1747 }
1748 }
1749
1750 if (typeof lat !== "undefined" && typeof lng !== "undefined" && name && !thisPlugin.findByGuid(guid)) {
1751 thisPlugin.addPortalpogo(guid, lat, lng, name, type);
1752 if (type == 'gyms') {
1753 if (importExStatus && item.isEx) {
1754 gyms[guid].isEx = true;
1755 }
1756 // don't overwrite existing medals
1757 if (importGymMedal && !gyms[guid].medal) {
1758 gyms[guid].medal = item.medal;
1759 }
1760 }
1761 }
1762 }
1763 });
1764
1765 thisPlugin.updateStarPortal();
1766 thisPlugin.resetAllMarkers();
1767 thisPlugin.optAlert('Successful.');
1768 } catch (e) {
1769 console.warn('pogo: failed to import data: ' + e); // eslint-disable-line no-console
1770 thisPlugin.optAlert('<span style="color: #f88">Import failed</span>');
1771 }
1772 });
1773 };
1774
1775 thisPlugin.optReset = function () {
1776 if (confirm('All pogo will be deleted. Are you sure?', '')) {
1777 delete localStorage[KEY_STORAGE];
1778 thisPlugin.createEmptyStorage();
1779 thisPlugin.updateStarPortal();
1780 thisPlugin.resetAllMarkers();
1781 if (settings.highlightGymCandidateCells) {
1782 updateMapGrid();
1783 }
1784 thisPlugin.optAlert('Successful.');
1785 }
1786 };
1787
1788 /* POKEMON GO PORTALS LAYER */
1789 thisPlugin.addAllMarkers = function () {
1790 function iterateStore(store, type) {
1791 for (let idpogo in store) {
1792 const item = store[idpogo];
1793 const lat = item.lat;
1794 const lng = item.lng;
1795 const guid = item.guid;
1796 const name = item.name;
1797 thisPlugin.addStar(guid, lat, lng, name, type);
1798 }
1799 }
1800
1801 iterateStore(gyms, 'gyms');
1802 iterateStore(pokestops, 'pokestops');
1803 };
1804
1805 thisPlugin.resetAllMarkers = function () {
1806 for (let guid in stopLayers) {
1807 const starInLayer = stopLayers[guid];
1808 stopLayerGroup.removeLayer(starInLayer);
1809 delete stopLayers[guid];
1810 }
1811 for (let gymGuid in gymLayers) {
1812 const gymInLayer = gymLayers[gymGuid];
1813 gymLayerGroup.removeLayer(gymInLayer);
1814 delete gymLayers[gymGuid];
1815 }
1816 thisPlugin.addAllMarkers();
1817 };
1818
1819 thisPlugin.addStar = function (guid, lat, lng, name, type) {
1820 let star;
1821 if (type === 'pokestops') {
1822 star = new L.Marker.SVGMarker([lat, lng], {
1823 title: name,
1824 iconOptions: {
1825 className: 'pokestop',
1826 html: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 821.52 1461.152">
1827 <path class="pokestop-circle" d="M410.76 0C203.04.14 30.93 152.53 0 351.61l211.27.39c26.99-84.43 106.09-145.55 199.49-145.6 93.25.11 172.24 61.13 199.33 145.41l211.2.19C790.58 152.8 618.51.26 410.76 0zm0 280c-75.11 0-136 60.89-136 136s60.89 136 136 136 136-60.89 136-136-60.89-136-136-136zM.23 480c30.71 199.2 202.78 351.74 410.53 352 207.72-.14 379.83-152.53 410.76-351.61L610.25 480c-26.99 84.43-106.09 145.55-199.49 145.6-93.25-.11-172.24-61.13-199.33-145.41z"/>
1828 <path class="pokestop-pole" d="M380.387 818.725h65.085v465.159h-65.085z" stroke-width="4.402"/>
1829 <ellipse class="pokestop-base" cx="415.185" cy="1345.949" rx="305.686" ry="115.202" stroke-width="6"/>
1830 </svg>`,
1831 iconSize: L.point(24, 32),
1832 iconAnchor: [12, 38]
1833 }
1834 });
1835
1836 }
1837 if (type === 'gyms') {
1838 // icon from https://github.com/FortAwesome/Font-Awesome/issues/9685#issuecomment-239238656
1839 const gym = gyms[guid];
1840 const medal = gym.medal || 'None';
1841 const className = medal + 'Medal' + (gym.isEx ? ' exGym' : '');
1842 star = new L.Marker.SVGMarker([lat, lng], {
1843 title: name,
1844 iconOptions: {
1845 id: 'gym' + guid.replace('.', ''),
1846 className: className,
1847 html: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 375 410"><g transform="translate(-62 -45)">
1848 <path class="gym-main-outline" d="M436.23 45.87C368.38 181.94 300.54 318.02 232.7 454.09c-12.48-46.6-24.97-93.19-37.45-139.78l-1.67-6.2s-7.37-.21-12.03-.72c-57.77-3.97-109.7-50.53-117.27-107.86-11.31-57.8 25.24-118.19 79.1-139.79 57.74-24.6 130.02 2.07 160 56.72 39.96-20.87 80.14-42.63 120.19-63.84z" />
1849 <g class='gym-inner'><path class="ball-outline-top" d="M286.17 115.42l-59.41 31.59a48.157 48.157 0 0 0-35.7-15.96c-26.61 0-48.17 21.57-48.17 48.17.02 3.91.51 7.81 1.47 11.6l-59.45 31.62c-5.61-13.72-8.51-28.4-8.53-43.22 0-63.34 51.34-114.68 114.68-114.68 38.2.06 73.86 19.13 95.11 50.88z"/>
1850 <path d="M404.7 78.26L297.06 135.6l-59.42 31.6a48.252 48.252 0 0 1 1.58 12.02c0 26.6-21.56 48.16-48.16 48.16a48.138 48.138 0 0 1-36-16.27l-59.35 31.56c21.21 31.94 57 51.17 95.35 51.23 4.26-.02 8.52-.28 12.76-.77l32.78 122.31z" class="ball-outline-bottom"/>
1851 <path class="ball-outline-center" d="M191.06 144.82c19 0 34.4 15.4 34.4 34.4s-15.4 34.4-34.4 34.4c-19.01 0-34.41-15.4-34.41-34.4s15.4-34.4 34.41-34.4z"/>
1852 </g></g></svg>`,
1853 iconSize: L.point(36, 36)
1854 }
1855 });
1856 }
1857
1858 if (!star)
1859 return;
1860
1861 window.registerMarkerForOMS(star);
1862 star.on('spiderfiedclick', function () {
1863 // don't try to render fake portals
1864 if (guid.indexOf('.') > -1) {
1865 renderPortalDetails(guid);
1866 }
1867 });
1868
1869 if (type === 'pokestops') {
1870 stopLayers[guid] = star;
1871 star.addTo(stopLayerGroup);
1872 }
1873 if (type === 'gyms') {
1874 gymLayers[guid] = star;
1875 star.addTo(gymLayerGroup);
1876 }
1877 };
1878
1879 thisPlugin.setupCSS = function () {
1880 $('<style>').prop('type', 'text/css').html(`
1881#sidebar #portaldetails h3.title{
1882 width:auto;
1883}
1884.pogoStop span,
1885.pogoGym span {
1886 display:inline-block;
1887 float:left;
1888 margin:3px 1px 0 4px;
1889 width:16px;
1890 height:15px;
1891 overflow:hidden;
1892 background-repeat:no-repeat;
1893}
1894.pogoStop span, .pogoStop.favorite:focus span,
1895.pogoGym span, .pogoGym.favorite:focus span {
1896 background-position:left top;
1897}
1898.pogoStop:focus span, .pogoStop.favorite span,
1899.pogoGym:focus span, .pogoGym.favorite span {
1900 background-position:right top;
1901}
1902
1903/**********************************************
1904 DIALOG BOX
1905**********************************************/
1906
1907/*---- Options panel -----*/
1908#pogoSetbox a{
1909 display:block;
1910 color:#ffce00;
1911 border:1px solid #ffce00;
1912 padding:3px 0;
1913 margin:10px auto;
1914 width:80%;
1915 text-align:center;
1916 background:rgba(8,48,78,.9);
1917}
1918#pogoSetbox a.disabled,
1919#pogoSetbox a.disabled:hover{
1920 color:#666;
1921 border-color:#666;
1922 text-decoration:none;
1923}
1924
1925#pogoSetbox{
1926 text-align:center;
1927}
1928.pogoStop span {
1929 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=);
1930}
1931.pogoGym span {
1932 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==);
1933}
1934
1935.PogoButtons {
1936 color: #fff;
1937 padding: 3px;
1938}
1939
1940.PogoButtons span {
1941 float: none;
1942}
1943
1944.notPogo span {
1945 color: #FFF;
1946 background: #000;
1947 border-radius: 50%;
1948 font-size: 10px;
1949 letter-spacing: -0.15em;
1950 display: inline-block;
1951 padding: 2px;
1952 opacity: 0.6;
1953 margin: 3px 1px 0 2px;
1954 height: 15px;
1955 width: 16px;
1956 box-sizing: border-box;
1957 line-height: 1;
1958}
1959
1960.notPogo span:after {
1961 display: inline-block;
1962 content: "N/A";
1963 position: absolute;
1964}
1965
1966.notPogo:focus span, .notPogo.favorite span {
1967 opacity: 1;
1968}
1969
1970.pogo-text {
1971 text-align: center;
1972 font-weight: bold;
1973 border: none !important;
1974 background: none !important;
1975 font-size: 130%;
1976 color: #000;
1977 text-shadow: 1px 1px #FFF, 2px 2px 6px #fff, -1px -1px #fff, -2px -2px 6px #fff;
1978}
1979
1980#PogoGymInfo {
1981 display: none;
1982 padding: 3px;
1983}
1984
1985.isGym #PogoGymInfo {
1986 display: block;
1987}
1988
1989.thisIsPogo .layer_off_warning,
1990.thisIsPogo .mods,
1991.thisIsPogo #randdetails,
1992.thisIsPogo #resodetails,
1993.thisIsPogo #level {
1994 display: none;
1995}
1996
1997.thisIsPogo #playerstat,
1998.thisIsPogo #gamestat,
1999.thisIsPogo #redeem,
2000.thisIsPogo #chat,
2001.thisIsPogo #artifactLink,
2002.thisIsPogo #scoresLink,
2003.thisIsPogo #chatinput,
2004.thisIsPogo #chatcontrols {
2005 display: none;
2006}
2007
2008.thisIsPogo #mobileinfo .portallevel,
2009.thisIsPogo #mobileinfo .resonator {
2010 display: none;
2011}
2012
2013.thisIsPogo #sidebar #portaldetails h3.title {
2014 color: #fff;
2015}
2016
2017.gym-main-outline {
2018 fill: #FFF;
2019 stroke: #000;
2020 stroke-width: 5;
2021}
2022
2023.gym-inner path {
2024 fill: #fff;
2025 stroke: #000;
2026 stroke-width: 2;
2027}
2028
2029.GoldMedal .gym-main-outline,
2030.GoldMedal .ball-outline-center {
2031 fill: #FEED55;
2032}
2033.SilverMedal .gym-main-outline,
2034.SilverMedal .ball-outline-center {
2035 fill: #CEDFE6;
2036}
2037.BronzeMedal .gym-main-outline,
2038.BronzeMedal .ball-outline-center {
2039 fill: #F0B688;
2040}
2041
2042.GoldMedal .gym-inner path {
2043 stroke: #EDC13C;
2044 stroke-width: 20;
2045}
2046.SilverMedal .gym-inner path {
2047 stroke: #A4C1C7;
2048 stroke-width: 20;
2049}
2050.BronzeMedal .gym-inner path {
2051 stroke: #DD9D71;
2052 stroke-width: 10;
2053}
2054
2055.gym-inner .ball-outline-top {
2056 fill: #f71208;
2057}
2058
2059.exGym {
2060 position: relative;
2061}
2062
2063.exGym:after {
2064 content: "EX";
2065 font-weight: bold;
2066 text-shadow: 1px 1px 3px #BED1D5, -1px -1px 3px #BED1D5;
2067 color: #09131D;
2068 font-size: 130%;
2069 position: absolute;
2070 top: 0;
2071 right: 0;
2072}
2073
2074.pokestop {
2075 opacity: 0.75;
2076}
2077
2078.pokestop path,
2079.pokestop ellipse {
2080 fill: #2370DA;
2081}
2082
2083path.pokestop-circle {
2084 fill: #23FEF8;
2085 stroke-width: 30px;
2086 stroke: #2370DA;
2087}
2088
2089.smallpokestops .pokestop {
2090 opacity: 0.85;
2091}
2092
2093.smallpokestops path.pokestop-pole,
2094.smallpokestops ellipse.pokestop-base {
2095 display: none;
2096}
2097
2098.smallpokestops .pokestop svg {
2099 transform: translateY(25px) scale(0.8);
2100}
2101
2102.PogoClassification div {
2103 display: grid;
2104 grid-template-columns: 200px 60px 60px 60px;
2105 text-align: center;
2106 align-items: center;
2107 height: 140px;
2108 overflow: hidden;
2109 margin-bottom: 10px;
2110}
2111
2112.PogoClassification div:nth-child(odd) {
2113 background: rgba(7, 42, 69, 0.9);
2114}
2115
2116.PogoClassification img {
2117 max-width: 200px;
2118 max-height: 140px;
2119 display: block;
2120 margin: 0 auto;
2121}
2122
2123#dialog-missingPortals .PogoClassification div {
2124 height: 50px;
2125}
2126
2127img.photo,
2128.ingressLocation,
2129.pogoLocation {
2130 cursor: zoom-in;
2131}
2132
2133.PoGo-PortalAnimation {
2134 width: 30px;
2135 height: 30px;
2136 background-color: rgba(255, 255, 255, 0.5);
2137 border-radius: 50%;
2138 box-shadow: 0px 0px 4px white;
2139 animation-duration: 1s;
2140 animation-name: shrink;
2141}
2142
2143@keyframes shrink {
2144 from {
2145 width: 30px;
2146 height: 30px;
2147 top: 0px;
2148 left: 0px;
2149 }
2150
2151 to {
2152 width: 10px;
2153 height: 10px;
2154 top: 10px;
2155 left: 10px;
2156 }
2157}
2158
2159.PoGo-PortalAnimationHover {
2160 background-color: rgb(255, 102, 0, 0.8);
2161 border-radius: 50%;
2162 animation-duration: 1s;
2163 animation-name: shrinkHover;
2164 animation-iteration-count: infinite;
2165}
2166
2167@keyframes shrinkHover {
2168 from {
2169 width: 40px;
2170 height: 40px;
2171 top: 0px;
2172 left: 0px;
2173 }
2174
2175 to {
2176 width: 20px;
2177 height: 20px;
2178 top: 10px;
2179 left: 10px;
2180 }
2181}
2182
2183#sidebarPogo {
2184 color: #eee;
2185 padding: 2px 5px;
2186}
2187
2188#sidebarPogo span {
2189 margin-right: 5px;
2190}
2191
2192.refreshingData,
2193.refreshingPortalCount {
2194 opacity: 0.5;
2195 pointer-events: none;
2196}
2197
2198#sidebarPogo.mobile {
2199 width: 100%;
2200 background: rebeccapurple;
2201 display: flex;
2202}
2203
2204#sidebarPogo.mobile > div {
2205 margin-right: 1em;
2206}
2207
2208.pogo-colors input[type=color] {
2209 border: 0;
2210 padding: 0;
2211}
2212
2213`).appendTo('head');
2214 };
2215
2216 // A portal has been received.
2217 function onPortalAdded(data) {
2218 const guid = data.portal.options.guid;
2219
2220 data.portal.on('add', function () {
2221 addNearbyCircle(guid);
2222 window.clearTimeout(relayoutTimer);
2223 relayoutTimer = window.setTimeout(relayerBackgroundGroups, 100);
2224 });
2225
2226 data.portal.on('remove', function () {
2227 removeNearbyCircle(guid);
2228 });
2229
2230 // analyze each portal only once, but sometimes the first time there's no additional data of the portal
2231 if (allPortals[guid] && allPortals[guid].name)
2232 return;
2233
2234 const portal = {
2235 guid: guid,
2236 name: data.portal.options.data.title,
2237 lat: data.portal._latlng.lat,
2238 lng: data.portal._latlng.lng,
2239 image: data.portal.options.data.image,
2240 cells: {}
2241 };
2242
2243 allPortals[guid] = portal;
2244
2245 // If it's already classified in Pokemon, get out
2246 const pogoData = thisPlugin.findByGuid(guid);
2247 if (pogoData) {
2248 const pogoItem = pogoData.store[guid];
2249 if (!pogoItem.exists) {
2250 // Mark that it still exists in Ingress
2251 pogoItem.exists = true;
2252
2253 if (missingPortals[guid]) {
2254 delete missingPortals[guid];
2255 updateMissingPortalsCount();
2256 }
2257
2258 // Check if it has been moved
2259 if (pogoItem.lat != portal.lat || pogoItem.lng != portal.lng) {
2260 movedPortals.push({
2261 pogo: pogoItem,
2262 ingress: portal
2263 });
2264 updateCounter('moved', movedPortals);
2265 }
2266 }
2267 if (portal.name && pogoItem.name !== portal.name) {
2268 pogoData.store[guid].name = portal.name;
2269 }
2270 return;
2271 }
2272
2273 if (skippedPortals[guid] || newPokestops[guid])
2274 return;
2275
2276 newPortals[guid] = portal;
2277
2278 refreshNewPortalsCounter();
2279 }
2280
2281 /**
2282 * Draw a 20m circle around a portal
2283 */
2284 function addNearbyCircle(guid) {
2285 const portal = window.portals[guid];
2286 if (!portal)
2287 return;
2288
2289 const circleSettings = {
2290 color: settings.colors.nearbyCircleBorder.color,
2291 opacity: settings.colors.nearbyCircleBorder.opacity,
2292 fillColor: settings.colors.nearbyCircleFill.color,
2293 fillOpacity: settings.colors.nearbyCircleFill.opacity,
2294 weight: 1,
2295 clickable: false,
2296 interactive: false
2297 };
2298
2299 const center = portal._latlng;
2300 const circle = L.circle(center, 20, circleSettings);
2301 nearbyLayerGroup.addLayer(circle);
2302 nearbyCircles[guid] = circle;
2303 }
2304
2305 /**
2306 * Removes the 20m circle if a portal is purged
2307 */
2308 function removeNearbyCircle(guid) {
2309 const circle = nearbyCircles[guid];
2310 if (circle != null) {
2311 nearbyLayerGroup.removeLayer(circle);
2312 delete nearbyCircles[guid];
2313 }
2314 }
2315
2316 function redrawNearbyCircles() {
2317 const keys = Object.keys(nearbyCircles);
2318 keys.forEach(guid => {
2319 removeNearbyCircle(guid);
2320 addNearbyCircle(guid);
2321 });
2322 relayerBackgroundGroups();
2323 }
2324
2325 /**
2326 * Re-orders the layerGroups within regionLayer so that foreground objects don't get hidden/obscured by background layers.
2327 */
2328 function relayerBackgroundGroups() {
2329 if (!map.hasLayer(regionLayer)) {
2330 return;
2331 }
2332 if (regionLayer.hasLayer(nearbyLayerGroup)) {
2333 nearbyLayerGroup.bringToBack();
2334 }
2335 if (regionLayer.hasLayer(cellLayerGroup)) {
2336 cellLayerGroup.bringToBack();
2337 }
2338 if (regionLayer.hasLayer(gymCenterLayerGroup)) {
2339 gymCenterLayerGroup.bringToFront();
2340 }
2341 }
2342
2343 function refreshNewPortalsCounter() {
2344 if (!settings.analyzeForMissingData)
2345 return;
2346
2347 // workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=961199
2348 try
2349 {
2350 if (checkNewPortalsTimer) {
2351 clearTimeout(checkNewPortalsTimer);
2352 } else {
2353 document.getElementById('sidebarPogo').classList.add('refreshingPortalCount');
2354 }
2355 } catch (e) {
2356 // nothing
2357 }
2358
2359 // workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=961199
2360 try
2361 {
2362 checkNewPortalsTimer = setTimeout(checkNewPortals, 1000);
2363 } catch (e) {
2364 checkNewPortals();
2365 }
2366 }
2367
2368 /**
2369 * A potential new portal has been received
2370 */
2371 function checkNewPortals() {
2372 checkNewPortalsTimer = null;
2373
2374 // don't try to classify if we don't have all the portal data
2375 if (map.getZoom() < 15)
2376 return;
2377
2378 document.getElementById('sidebarPogo').classList.remove('refreshingPortalCount');
2379
2380 newPokestops = {};
2381 notClassifiedPokestops = [];
2382
2383 const allCells = groupByCell(poiCellLevel);
2384
2385 // Check only the items inside the screen,
2386 // the server might provide info about remote portals if they are part of a link
2387 // and we don't know anything else about nearby portals of that one.
2388 // In this case (vs drawing) we want to filter only cells fully within the screen
2389 const cells = filterWithinScreen(allCells);
2390
2391 // try to guess new pokestops if they are the only items in a cell
2392 Object.keys(cells).forEach(id => {
2393 const data = allCells[id];
2394 checkIsPortalMissing(data.gyms, data);
2395 checkIsPortalMissing(data.stops, data);
2396 //checkIsPortalMissing(data.notpogo);
2397
2398 if (data.notClassified.length == 0)
2399 return;
2400 const notClassified = data.notClassified;
2401
2402 if (data.gyms.length > 0 || data.stops.length > 0) {
2403 // Already has a pogo item, ignore the rest
2404 notClassified.forEach(portal => {
2405 skippedPortals[portal.guid] = true;
2406 delete newPortals[portal.guid];
2407 });
2408 return;
2409 }
2410 // only one, let's guess it's a pokestop by default
2411 if (notClassified.length == 1) {
2412 const portal = notClassified[0];
2413 const obj = {'guid': portal.guid, 'lat': portal.lat, 'lng': portal.lng, 'name': portal.name};
2414
2415 newPokestops[portal.guid] = obj;
2416 //delete newPortals[portal.guid];
2417 return;
2418 }
2419
2420 // too many items to guess
2421 notClassifiedPokestops.push(data.notClassified);
2422 });
2423
2424 updateCounter('pokestops', Object.values(newPokestops));
2425 updateCounter('classification', notClassifiedPokestops);
2426 updateMissingPortalsCount();
2427
2428 // Now gyms
2429 checkNewGyms();
2430 }
2431
2432 /**
2433 * Filter the missing portals detection to show only those on screen and reduce false positives
2434 */
2435 function updateMissingPortalsCount() {
2436 const keys = Object.keys(missingPortals);
2437 if (keys.length == 0)
2438 updateCounter('missing', []);
2439
2440 const bounds = map.getBounds();
2441 const filtered = [];
2442 keys.forEach(guid => {
2443 const pogoData = thisPlugin.findByGuid(guid);
2444 const item = pogoData.store[guid];
2445 if (isPointOnScreen(bounds, item)) {
2446 filtered.push(item);
2447 }
2448 });
2449 updateCounter('missing', filtered);
2450 }
2451
2452 /**
2453 * Given an array of pogo items checks if they have been removed from Ingress
2454 */
2455 function checkIsPortalMissing(array, cellData) {
2456 array.forEach(item => {
2457 if (item.exists || item.newGuid)
2458 return;
2459 const guid = item.guid;
2460
2461 if (findCorrectGuid(item, cellData.notClassified)) {
2462 return;
2463 }
2464 if (!missingPortals[guid]) {
2465 missingPortals[guid] = true;
2466 }
2467 });
2468 }
2469
2470 /**
2471 * Check if there's another real portal in the same cell (we're checking a pogo that doesn't exist in Ingress)
2472 */
2473 function findCorrectGuid(pogoItem, array) {
2474 const portal = array.find(x => x.name == pogoItem.name && x.guid != pogoItem.guid);
2475 if (portal != null) {
2476 pogoItem.newGuid = portal.guid;
2477 movedPortals.push({
2478 pogo: pogoItem,
2479 ingress: portal
2480 });
2481 updateCounter('moved', movedPortals);
2482
2483 delete missingPortals[pogoItem.guid];
2484
2485 return true;
2486 }
2487 return false;
2488 }
2489
2490 function checkNewGyms() {
2491 const cellsWithMissingGyms = [];
2492
2493 const allCells = groupByCell(gymCellLevel);
2494
2495 // Check only the items inside the screen,
2496 // the server might provide info about remote portals if they are part of a link
2497 // and we don't know anything else about nearby portals of that one.
2498 // In this case (vs drawing) we want to filter only cells fully within the screen
2499 const cells = filterWithinScreen(allCells);
2500
2501 // Find the cells where new Gyms can be identified
2502 Object.keys(cells).forEach(id => {
2503 const data = allCells[id];
2504 // Only cells with all the portals already analyzed
2505 if (data.notClassified.length > 0)
2506 return;
2507 if (ignoredCellsMissingGyms[data.cell.toString()])
2508 return;
2509 const missingGyms = computeMissingGyms(data);
2510 if (missingGyms > 0) {
2511 cellsWithMissingGyms.push(data);
2512 }
2513 });
2514
2515 if (cellsWithMissingGyms.length > 0) {
2516 const filtered = filterWithinScreen(cellsWithMissingGyms);
2517 updateCounter('gyms', Object.values(filtered));
2518 } else {
2519 updateCounter('gyms', []);
2520 }
2521 }
2522
2523 /**
2524 * Display new pokestops so they can be added
2525 */
2526 function promptForNewPokestops(data) {
2527 if (data.length == 0)
2528 return;
2529 let pending = data.length;
2530
2531 const div = document.createElement('div');
2532 div.className = 'PogoClassification';
2533 data.sort(sortByName).forEach(portal => {
2534 const wrapper = document.createElement('div');
2535 wrapper.setAttribute('data-guid', portal.guid);
2536 const img = getPortalImage(portal);
2537 wrapper.innerHTML = '<span class="PogoName">' + getPortalName(portal) +
2538 img + '</span>' +
2539 '<a data-type="pokestops">' + 'STOP' + '</a>' +
2540 '<a data-type="gyms">' + 'GYM' + '</a>' +
2541 '<a data-type="notpogo">' + 'N/A' + '</a>';
2542 div.appendChild(wrapper);
2543 });
2544 const container = dialog({
2545 id: 'classifyPokestop',
2546 html: div,
2547 width: '420px',
2548 title: 'Are all of these Pokestops or Gyms?',
2549 buttons: {
2550 // Button to allow skip this cell
2551 'Skip': function () {
2552 container.dialog('close');
2553 data.forEach(portal => {
2554 delete newPokestops[portal.guid];
2555 skippedPortals[portal.guid] = true;
2556 });
2557 updateCounter('pokestops', Object.values(newPokestops));
2558 },
2559 'Mark all as Pokestops': function () {
2560 container.dialog('close');
2561 data.forEach(portal => {
2562 if (!newPokestops[portal.guid])
2563 return;
2564
2565 delete newPokestops[portal.guid];
2566 thisPlugin.addPortalpogo(portal.guid, portal.lat, portal.lng, portal.name, 'pokestops');
2567 });
2568 if (settings.highlightGymCandidateCells) {
2569 updateMapGrid();
2570 }
2571 updateCounter('pokestops', Object.values(newPokestops));
2572 }
2573 }
2574 });
2575 // Remove ok button
2576 const outer = container.parent();
2577 outer.find('.ui-dialog-buttonset button:first').remove();
2578
2579 // mark the selected one as pokestop or gym
2580 container.on('click', 'a', function (e) {
2581 const type = this.getAttribute('data-type');
2582 const row = this.parentNode;
2583 const guid = row.getAttribute('data-guid');
2584 const portal = allPortals[guid];
2585 delete newPokestops[portal.guid];
2586 thisPlugin.addPortalpogo(guid, portal.lat, portal.lng, portal.name, type);
2587 if (settings.highlightGymCandidateCells) {
2588 updateMapGrid();
2589 }
2590 $(row).fadeOut(200);
2591 pending--;
2592 if (pending == 0) {
2593 container.dialog('close');
2594 }
2595 updateCounter('pokestops', Object.values(newPokestops));
2596 });
2597
2598 container.on('click', 'img.photo', centerPortal);
2599 configureHoverMarker(container);
2600 }
2601
2602 /**
2603 * In a level 17 cell there's more than one portal, ask which one is Pokestop or Gym
2604 */
2605 function promptToClassifyPokestops() {
2606 updateCounter('classification', notClassifiedPokestops);
2607 if (notClassifiedPokestops.length == 0)
2608 return;
2609
2610 const group = notClassifiedPokestops.shift();
2611 const div = document.createElement('div');
2612 div.className = 'PogoClassification';
2613 group.sort(sortByName).forEach(portal => {
2614 const wrapper = document.createElement('div');
2615 wrapper.setAttribute('data-guid', portal.guid);
2616 const img = getPortalImage(portal);
2617 wrapper.innerHTML = '<span class="PogoName">' + getPortalName(portal) +
2618 img + '</span>' +
2619 '<a data-type="pokestops">' + 'STOP' + '</a>' +
2620 '<a data-type="gyms">' + 'GYM' + '</a>';
2621 div.appendChild(wrapper);
2622 });
2623 const container = dialog({
2624 id: 'classifyPokestop',
2625 html: div,
2626 width: '360px',
2627 title: 'Which one is in Pokemon Go?',
2628 buttons: {
2629 // Button to allow skip this cell
2630 Skip: function () {
2631 container.dialog('close');
2632 group.forEach(portal => {
2633 delete newPortals[portal.guid];
2634 skippedPortals[portal.guid] = true;
2635 });
2636 // continue
2637 promptToClassifyPokestops();
2638 }
2639 }
2640 });
2641 // Remove ok button
2642 const outer = container.parent();
2643 outer.find('.ui-dialog-buttonset button:first').remove();
2644
2645 // mark the selected one as pokestop or gym
2646 container.on('click', 'a', function (e) {
2647 const type = this.getAttribute('data-type');
2648 const guid = this.parentNode.getAttribute('data-guid');
2649 const portal = getPortalSummaryFromGuid(guid);
2650 thisPlugin.addPortalpogo(guid, portal.lat, portal.lng, portal.name, type);
2651 if (settings.highlightGymCandidateCells) {
2652 updateMapGrid();
2653 }
2654
2655 group.forEach(tmpPortal => {
2656 delete newPortals[tmpPortal.guid];
2657 });
2658
2659 container.dialog('close');
2660 // continue
2661 promptToClassifyPokestops();
2662 });
2663 container.on('click', 'img.photo', centerPortal);
2664 configureHoverMarker(container);
2665 }
2666
2667 /**
2668 * List of portals that have been moved
2669 */
2670 function promptToMovePokestops() {
2671 if (movedPortals.length == 0)
2672 return;
2673
2674 const div = document.createElement('div');
2675 div.className = 'PogoClassification';
2676 movedPortals.sort(sortByName).forEach(pair => {
2677 const portal = pair.ingress;
2678 const pogoItem = pair.pogo;
2679 const wrapper = document.createElement('div');
2680 wrapper.setAttribute('data-guid', portal.guid);
2681 wrapper.dataPortal = portal;
2682 wrapper.dataPogoGuid = pogoItem.guid;
2683 const img = getPortalImage(portal);
2684 wrapper.innerHTML = '<span class="PogoName">' + getPortalName(portal) +
2685 img + '</span>' +
2686 '<span><span class="ingressLocation">' + 'Ingress location' + '</span></span>' +
2687 '<span><span class="pogoLocation" data-lat="' + pogoItem.lat + '" data-lng="' + pogoItem.lng + '">' + 'Pogo location' + '</span><br>' +
2688 '<a>' + 'Update' + '</a></span>';
2689 div.appendChild(wrapper);
2690 });
2691 const container = dialog({
2692 id: 'movedPortals',
2693 html: div,
2694 width: '360px',
2695 title: 'These portals have been moved in Ingress',
2696 buttons: {
2697 // Button to move all the portals at once
2698 'Update all': function () {
2699 container.dialog('close');
2700 movedPortals.forEach(pair => {
2701 const portal = pair.ingress;
2702 const pogoItem = pair.pogo;
2703 movePogo(portal, pogoItem.guid);
2704 });
2705 movedPortals.length = 0;
2706 updateCounter('moved', movedPortals);
2707
2708 thisPlugin.saveStorage();
2709 if (settings.highlightGymCandidateCells) {
2710 updateMapGrid();
2711 }
2712
2713 }
2714 }
2715 });
2716
2717 // Update location
2718 container.on('click', 'a', function (e) {
2719 const row = this.parentNode.parentNode;
2720 const portal = row.dataPortal;
2721 movePogo(portal, row.dataPogoGuid);
2722
2723 thisPlugin.saveStorage();
2724 if (settings.highlightGymCandidateCells) {
2725 updateMapGrid();
2726 }
2727
2728 $(row).fadeOut(200);
2729
2730 // remove it from the list of portals
2731 const idx = movedPortals.findIndex(pair => pair.ingress.guid == pair.ingress.guid);
2732 movedPortals.splice(idx, 1);
2733 updateCounter('moved', movedPortals);
2734
2735 if (movedPortals.length == 0)
2736 container.dialog('close');
2737 });
2738 container.on('click', 'img.photo', centerPortal);
2739 container.on('click', '.ingressLocation', centerPortal);
2740 container.on('click', '.pogoLocation', centerPortalAlt);
2741 configureHoverMarker(container);
2742 configureHoverMarkerAlt(container);
2743 }
2744
2745 /**
2746 * Update location of a pogo item
2747 */
2748 function movePogo(portal, pogoGuid) {
2749 const guid = portal.guid;
2750 const pogoData = thisPlugin.findByGuid(pogoGuid);
2751
2752 const existingType = pogoData.type;
2753 let gym = null;
2754 if (existingType == 'gyms') {
2755 gym = pogoData.store[guid];
2756 }
2757
2758 // remove marker
2759 removePogoObject(existingType, guid);
2760
2761 // Draw new marker
2762 thisPlugin.addPortalpogo(guid, portal.lat, portal.lng, portal.name || pogoData.name, existingType);
2763
2764 // copy gym status
2765 if (gym != null) {
2766 pogoData.store[guid].isEx = gym.isEx;
2767 pogoData.store[guid].medal = gym.medal;
2768
2769 thisPlugin.saveStorage();
2770
2771 const icon = document.getElementById('gym' + guid.replace('.', ''));
2772 // update gym marker
2773 if (icon) {
2774 icon.classList.add(gym.medal + 'Medal');
2775 icon.classList[gym.isEx ? 'add' : 'remove']('exGym');
2776 }
2777
2778 }
2779 }
2780
2781 /**
2782 * Pogo items that aren't in Ingress
2783 */
2784 function promptToRemovePokestops(missing) {
2785 const div = document.createElement('div');
2786 div.className = 'PogoClassification';
2787 missing.sort(sortByName).forEach(portal => {
2788 const wrapper = document.createElement('div');
2789 wrapper.setAttribute('data-guid', portal.guid);
2790 const name = portal.name || 'Unknown';
2791 wrapper.innerHTML = '<span class="PogoName"><span class="pogoLocation" data-lat="' + portal.lat + '" data-lng="' + portal.lng + '">' + name + '</span></span>' +
2792 '<span><a>' + 'Remove' + '</a></span>';
2793 div.appendChild(wrapper);
2794 });
2795 const container = dialog({
2796 id: 'missingPortals',
2797 html: div,
2798 width: '360px',
2799 title: 'These portals are missing in Ingress',
2800 buttons: {
2801 }
2802 });
2803
2804 // Update location
2805 container.on('click', 'a', function (e) {
2806 const row = this.parentNode.parentNode;
2807 const guid = row.getAttribute('data-guid');
2808 const pogoData = thisPlugin.findByGuid(guid);
2809 const existingType = pogoData.type;
2810
2811 // remove marker
2812 removePogoObject(existingType, guid);
2813 thisPlugin.saveStorage();
2814
2815 if (settings.highlightGymCandidateCells) {
2816 updateMapGrid();
2817 }
2818
2819 $(row).fadeOut(200);
2820
2821 delete missingPortals[guid];
2822 updateMissingPortalsCount();
2823
2824 if (Object.keys(missingPortals).length == 0) {
2825 container.dialog('close');
2826 }
2827 });
2828 container.on('click', '.pogoLocation', centerPortalAlt);
2829 configureHoverMarkerAlt(container);
2830 }
2831
2832 function configureHoverMarker(container) {
2833 let hoverMarker;
2834 container.find('img.photo, .ingressLocation').hover(
2835 function hIn() {
2836 const row = this.parentNode.parentNode;
2837 const guid = row.getAttribute('data-guid');
2838 const portal = row.dataPortal || window.portals[guid];
2839 if (!portal)
2840 return;
2841 const center = portal._latlng || new L.LatLng(portal.lat, portal.lng);
2842 hoverMarker = L.marker(center, {
2843 icon: L.divIcon({
2844 className: 'PoGo-PortalAnimationHover',
2845 iconSize: [40, 40],
2846 iconAnchor: [20, 20],
2847 html: ''
2848 }),
2849 interactive: false
2850 });
2851 map.addLayer(hoverMarker);
2852 }, function hOut() {
2853 if (hoverMarker)
2854 map.removeLayer(hoverMarker);
2855 });
2856 }
2857
2858 function configureHoverMarkerAlt(container) {
2859 let hoverMarker;
2860 container.find('.pogoLocation').hover(
2861 function hIn() {
2862 const lat = this.getAttribute('data-lat');
2863 const lng = this.getAttribute('data-lng');
2864 const center = new L.LatLng(lat, lng);
2865 hoverMarker = L.marker(center, {
2866 icon: L.divIcon({
2867 className: 'PoGo-PortalAnimationHover',
2868 iconSize: [40, 40],
2869 iconAnchor: [20, 20],
2870 html: ''
2871 }),
2872 interactive: false
2873 });
2874 map.addLayer(hoverMarker);
2875 }, function hOut() {
2876 if (hoverMarker)
2877 map.removeLayer(hoverMarker);
2878 });
2879 }
2880
2881 /**
2882 * Center the map on the clicked portal to help tracking it (the user will have to manually move the dialog)
2883 */
2884 function centerPortal(e) {
2885 const row = this.parentNode.parentNode;
2886 const guid = row.getAttribute('data-guid');
2887 const portal = row.dataPortal || window.portals[guid];
2888 if (!portal)
2889 return;
2890 const center = portal._latlng || new L.LatLng(portal.lat, portal.lng);
2891 map.panTo(center);
2892 drawClickAnimation(center);
2893 }
2894
2895 function centerPortalAlt(e) {
2896 const lat = this.getAttribute('data-lat');
2897 const lng = this.getAttribute('data-lng');
2898 const center = new L.LatLng(lat, lng);
2899 map.panTo(center);
2900 drawClickAnimation(center);
2901 }
2902
2903 function drawClickAnimation(center) {
2904 const marker = L.marker(center, {
2905 icon: L.divIcon({
2906 className: 'PoGo-PortalAnimation',
2907 iconSize: [30, 30],
2908 iconAnchor: [15, 15],
2909 html: ''
2910 }),
2911 interactive: false
2912 });
2913 map.addLayer(marker);
2914
2915 setTimeout(function () {
2916 map.removeLayer(marker);
2917 }, 2000);
2918 }
2919
2920 function getPortalSummaryFromGuid(guid) {
2921 const newPortal = newPortals[guid];
2922 if (newPortal)
2923 return newPortal;
2924
2925 const portal = window.portals[guid];
2926 if (!portal)
2927 return {};
2928
2929 return {
2930 guid: guid,
2931 name: portal.options.data.title,
2932 lat: portal._latlng.lat,
2933 lng: portal._latlng.lng,
2934 image: portal.options.data.image,
2935 cells: {}
2936 };
2937 }
2938
2939 function getPortalImage(pokestop) {
2940 if (pokestop.image)
2941 return '<img src="' + pokestop.image.replace('http:', 'https:') + '" class="photo">';
2942
2943 const portal = window.portals[pokestop.guid];
2944 if (!portal)
2945 return '';
2946
2947 if (portal && portal.options && portal.options.data && portal.options.data.image) {
2948 pokestop.image = portal.options.data.image;
2949 return '<img src="' + pokestop.image.replace('http:', 'https:') + '" class="photo">';
2950 }
2951 return '';
2952 }
2953
2954 function getPortalName(pokestop) {
2955 if (pokestop.name)
2956 return pokestop.name;
2957
2958 const portal = window.portals[pokestop.guid];
2959 if (!portal)
2960 return '';
2961
2962 if (portal && portal.options && portal.options.data && portal.options.data.title) {
2963 pokestop.name = portal.options.data.title;
2964 return pokestop.name;
2965 }
2966 return '';
2967 }
2968
2969 /**
2970 * In a level 14 cell there's some missing Gyms, prompt which ones
2971 */
2972 function promptToClassifyGyms(groups) {
2973 // don't try to classify if we don't have all the portal data
2974 if (map.getZoom() < 15)
2975 return;
2976
2977 if (!groups || groups.length == 0)
2978 return;
2979
2980 const cellData = groups.shift();
2981 updateCounter('gyms', groups);
2982
2983 let missingGyms = computeMissingGyms(cellData);
2984
2985 const div = document.createElement('div');
2986 div.className = 'PogoClassification';
2987 cellData.stops.sort(sortByName).forEach(portal => {
2988 if (skippedPortals[portal.guid])
2989 return;
2990
2991 const wrapper = document.createElement('div');
2992 wrapper.setAttribute('data-guid', portal.guid);
2993 wrapper.innerHTML =
2994 '<span class="PogoName">' + getPortalName(portal) +
2995 getPortalImage(portal) + '</span>' +
2996 '<a data-type="gyms">' + 'GYM' + '</a>';
2997 div.appendChild(wrapper);
2998 });
2999 // No pokestops to prompt as it has been skipped
3000 if (!div.firstChild) {
3001 // continue
3002 promptToClassifyGyms(groups);
3003 return;
3004 }
3005
3006 const container = dialog({
3007 id: 'classifyPokestop',
3008 html: div,
3009 width: '360px',
3010 title: missingGyms == 1 ? 'Which one is a Gym?' : 'Which ' + missingGyms + ' are Gyms?',
3011 buttons: {
3012 // Button to allow skip this cell
3013 Skip: function () {
3014 container.dialog('close');
3015 cellData.stops.forEach(portal => {
3016 skippedPortals[portal.guid] = true;
3017 });
3018 // continue
3019 promptToClassifyGyms(groups);
3020 },
3021 // Button to allow skip this cell
3022 'There is no Gym': function () {
3023 ignoredCellsMissingGyms[cellData.cell.toString()] = true;
3024
3025 if (settings.highlightGymCandidateCells) {
3026 updateMapGrid();
3027 }
3028 container.dialog('close');
3029
3030 thisPlugin.saveStorage();
3031
3032 updateCounter('gyms', groups);
3033 // continue
3034 promptToClassifyGyms(groups);
3035 }
3036 }
3037 });
3038 // Remove ok button
3039 const outer = container.parent();
3040 outer.find('.ui-dialog-buttonset button:first').remove();
3041
3042 // mark the selected one as pokestop or gym
3043 container.on('click', 'a', function (e) {
3044 const type = this.getAttribute('data-type');
3045 const row = this.parentNode;
3046 const guid = row.getAttribute('data-guid');
3047 const portal = pokestops[guid];
3048
3049 removePogoObject('pokestops', guid);
3050
3051 thisPlugin.addPortalpogo(guid, portal.lat, portal.lng, portal.name, type);
3052 if (settings.highlightGymCandidateCells) {
3053 updateMapGrid();
3054 }
3055 missingGyms--;
3056 if (missingGyms == 0) {
3057 container.dialog('close');
3058 // continue
3059 promptToClassifyGyms(groups);
3060 } else {
3061 $(row).fadeOut(200);
3062 document.querySelector('.ui-dialog-title-active').textContent = missingGyms == 1 ? 'Which one is a Gym?' : 'Which ' + missingGyms + ' are Gyms?';
3063 }
3064 });
3065
3066 container.on('click', 'img.photo', centerPortal);
3067 configureHoverMarker(container);
3068 }
3069
3070 /**
3071 * In a level 14 cell there are too many Gyms
3072 */
3073 function promptToVerifyGyms(cellIds) {
3074 if (!cellIds)
3075 cellIds = Object.keys(cellsExtraGyms);
3076
3077 if (cellIds.length == 0)
3078 return;
3079
3080 const cellId = cellIds[0];
3081 const group = findCellItems(cellId, gymCellLevel, gyms);
3082
3083 const div = document.createElement('div');
3084 div.className = 'PogoClassification';
3085 group.sort(sortByName).forEach(portal => {
3086 const wrapper = document.createElement('div');
3087 wrapper.setAttribute('data-guid', portal.guid);
3088 const img = getPortalImage(portal);
3089 wrapper.innerHTML = '<span class="PogoName">' + getPortalName(portal) +
3090 img + '</span>' +
3091 '<a data-type="pokestops">' + 'STOP' + '</a>';
3092 div.appendChild(wrapper);
3093 });
3094 const container = dialog({
3095 id: 'classifyPokestop',
3096 html: div,
3097 width: '360px',
3098 title: 'This cell has too many Gyms.',
3099 buttons: {
3100 // Button to allow skip this cell
3101 'All are OK': function () {
3102 ignoredCellsExtraGyms[cellId] = true;
3103
3104 if (settings.highlightGymCandidateCells) {
3105 updateMapGrid();
3106 }
3107 container.dialog('close');
3108 delete cellsExtraGyms[cellId];
3109
3110 thisPlugin.saveStorage();
3111
3112 updateCounter('extraGyms', Object.keys(cellsExtraGyms));
3113 // continue
3114 promptToVerifyGyms();
3115 }
3116 }
3117 });
3118 // Remove ok button
3119 const outer = container.parent();
3120 outer.find('.ui-dialog-buttonset button:first').remove();
3121
3122 // mark the selected one as pokestop or gym
3123 container.on('click', 'a', function (e) {
3124 const type = this.getAttribute('data-type');
3125 const guid = this.parentNode.getAttribute('data-guid');
3126 const portal = gyms[guid];
3127 thisPlugin.addPortalpogo(guid, portal.lat, portal.lng, portal.name, type);
3128 if (settings.highlightGymCandidateCells) {
3129 updateMapGrid();
3130 }
3131
3132 container.dialog('close');
3133 delete cellsExtraGyms[cellId];
3134 updateCounter('extraGyms', Object.keys(cellsExtraGyms));
3135 // continue
3136 promptToVerifyGyms();
3137 });
3138 container.on('click', 'img.photo', centerPortal);
3139 configureHoverMarker(container);
3140 }
3141
3142
3143 function removeLayer(name) {
3144 const layers = window.layerChooser._layers;
3145 const layersIds = Object.keys(layers);
3146
3147 let layerId = null;
3148 let leafletLayer;
3149 let isBase;
3150 let arrayIdx;
3151 layersIds.forEach(id => {
3152 const layer = layers[id];
3153 if (layer.name == name) {
3154 leafletLayer = layer.layer;
3155 layerId = leafletLayer._leaflet_id;
3156 isBase = !layer.overlay;
3157 arrayIdx = id;
3158 }
3159 });
3160
3161 // The Beacons and Frackers are not there in Firefox, why????
3162 if (!leafletLayer) {
3163 return;
3164 }
3165
3166 const enabled = map._layers[layerId] != null;
3167 if (enabled) {
3168 // Don't remove base layer if it's used
3169 if (isBase)
3170 return;
3171
3172 map.removeLayer(leafletLayer);
3173 }
3174 if (typeof leafletLayer.off != 'undefined')
3175 leafletLayer.off();
3176
3177 // new Leaflet
3178 if (Array.isArray(layers)) {
3179 // remove from array
3180 layers.splice(parseInt(arrayIdx, 10), 1);
3181 } else {
3182 // classic IITC, leaflet 0.7.7
3183 // delete from object
3184 delete layers[layerId];
3185 }
3186 window.layerChooser._update();
3187 removedLayers[name] = {
3188 layer: leafletLayer,
3189 enabled: enabled,
3190 isBase: isBase
3191 };
3192 window.updateDisplayedLayerGroup(name, enabled);
3193 }
3194 const removedLayers = {};
3195 let portalsLayerGroup;
3196
3197 function removeIngressLayers() {
3198 removeLayer('CartoDB Dark Matter');
3199 removeLayer('CartoDB Positron');
3200 removeLayer('Google Default Ingress Map');
3201
3202 removeLayer('Fields');
3203 removeLayer('Links');
3204 removeLayer('DEBUG Data Tiles');
3205 removeLayer('Artifacts');
3206 removeLayer('Ornaments');
3207 removeLayer('Beacons');
3208 removeLayer('Frackers');
3209
3210 removeLayer('Unclaimed/Placeholder Portals');
3211 for (let i = 1; i <= 8; i++) {
3212 removeLayer('Level ' + i + ' Portals');
3213 }
3214 //removeLayer('Resistance');
3215 //removeLayer('Enlightened');
3216 mergePortalLayers();
3217 }
3218
3219 /**
3220 * Put all the layers for Ingress portals under a single one
3221 */
3222 function mergePortalLayers() {
3223 portalsLayerGroup = new L.LayerGroup();
3224 window.addLayerGroup('Ingress Portals', portalsLayerGroup, true);
3225 portalsLayerGroup.addLayer(removedLayers['Unclaimed/Placeholder Portals'].layer);
3226 for (let i = 1; i <= 8; i++) {
3227 portalsLayerGroup.addLayer(removedLayers['Level ' + i + ' Portals'].layer);
3228 }
3229 //portalsLayerGroup.addLayer(removedLayers['Resistance'].layer);
3230 //portalsLayerGroup.addLayer(removedLayers['Enlightened'].layer);
3231 }
3232
3233 /**
3234 * Remove the single layer for all the portals
3235 */
3236 function revertPortalLayers() {
3237 if (!portalsLayerGroup) {
3238 return;
3239 }
3240 const name = 'Ingress Portals';
3241 const layerId = portalsLayerGroup._leaflet_id;
3242 const enabled = map._layers[layerId] != null;
3243
3244 const layers = window.layerChooser._layers;
3245 if (Array.isArray(layers)) {
3246 // remove from array
3247 const idx = layers.findIndex(o => o.layer._leaflet_id == layerId);
3248 layers.splice(idx, 1);
3249 } else {
3250 // classic IITC, leaflet 0.7.7
3251 // delete from object
3252 delete layers[layerId];
3253 }
3254 window.layerChooser._update();
3255 window.updateDisplayedLayerGroup(name, enabled);
3256
3257 if (typeof portalsLayerGroup.off != 'undefined')
3258 portalsLayerGroup.off();
3259 if (enabled) {
3260 map.removeLayer(portalsLayerGroup);
3261 }
3262 portalsLayerGroup = null;
3263 }
3264
3265 function restoreIngressLayers() {
3266 revertPortalLayers();
3267
3268 Object.keys(removedLayers).forEach(name => {
3269 const info = removedLayers[name];
3270 if (info.isBase)
3271 window.layerChooser.addBaseLayer(info.layer, name);
3272 else
3273 window.addLayerGroup(name, info.layer, info.enabled);
3274 });
3275 }
3276
3277 function zoomListener() {
3278 const zoom = map.getZoom();
3279 document.body.classList.toggle('smallpokestops', zoom < 16);
3280 }
3281
3282 const setup = function () {
3283 thisPlugin.isSmart = window.isSmartphone();
3284
3285 initSvgIcon();
3286
3287 loadSettings();
3288
3289 // Load data from localStorage
3290 thisPlugin.loadStorage();
3291
3292 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>
3293 <a class="pogoGym" accesskey="g" onclick="window.plugin.pogo.switchStarPortal('gyms');return false;" title="Mark this portal as a PokeGym [g]"><span></span></a>
3294 <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>
3295 `;
3296
3297 thisPlugin.setupCSS();
3298
3299 const sidebarPogo = document.createElement('div');
3300 sidebarPogo.id = 'sidebarPogo';
3301 sidebarPogo.style.display = 'none';
3302 if (thisPlugin.isSmart) {
3303 const status = document.getElementById('updatestatus');
3304 sidebarPogo.classList.add('mobile');
3305 status.insertBefore(sidebarPogo, status.firstElementChild);
3306
3307 const dStatus = document.createElement('div');
3308 dStatus.className = 'PogoStatus';
3309 status.insertBefore(dStatus, status.firstElementChild);
3310 } else {
3311 document.getElementById('sidebar').appendChild(sidebarPogo);
3312 }
3313
3314 sidebarPogo.appendChild(createCounter('New pokestops', 'pokestops', promptForNewPokestops));
3315 sidebarPogo.appendChild(createCounter('Review required', 'classification', promptToClassifyPokestops));
3316 sidebarPogo.appendChild(createCounter('Moved portals', 'moved', promptToMovePokestops));
3317 sidebarPogo.appendChild(createCounter('Missing portals', 'missing', promptToRemovePokestops));
3318 sidebarPogo.appendChild(createCounter('New Gyms', 'gyms', promptToClassifyGyms));
3319 sidebarPogo.appendChild(createCounter('Cells with extra Gyms', 'extraGyms', promptToVerifyGyms));
3320
3321 window.addHook('portalSelected', thisPlugin.onPortalSelected);
3322
3323 window.addHook('portalAdded', onPortalAdded);
3324 window.addHook('mapDataRefreshStart', function () {
3325 sidebarPogo.classList.add('refreshingData');
3326 });
3327 window.addHook('mapDataRefreshEnd', function () {
3328 sidebarPogo.classList.remove('refreshingData');
3329 refreshNewPortalsCounter();
3330 });
3331 map.on('moveend', function () {
3332 refreshNewPortalsCounter();
3333 });
3334 sidebarPogo.classList.add('refreshingData');
3335
3336 // Layer - pokemon go portals
3337 stopLayerGroup = L.layerGroup();
3338 window.addLayerGroup('PokeStops', stopLayerGroup, true);
3339 gymLayerGroup = L.layerGroup();
3340 window.addLayerGroup('Gyms', gymLayerGroup, true);
3341 regionLayer = L.layerGroup();
3342 window.addLayerGroup('S2 Grid', regionLayer, true);
3343
3344 // this layer will group all the nearby circles that are added or removed from it when the portals are added or removed
3345 nearbyLayerGroup = L.featureGroup();
3346 // this layer will group all the shaded cells and cell borders
3347 cellLayerGroup = L.featureGroup();
3348 // this layer will contain the s2 grid
3349 gridLayerGroup = L.layerGroup()
3350 // this layer will contain the gym centers for checking ex eligibility
3351 gymCenterLayerGroup = L.featureGroup();
3352
3353 thisPlugin.addAllMarkers();
3354
3355 const toolbox = document.getElementById('toolbox');
3356
3357 const buttonPoGo = document.createElement('a');
3358 buttonPoGo.textContent = 'PoGo Actions';
3359 buttonPoGo.title = 'Actions on Pokemon Go data';
3360 buttonPoGo.addEventListener('click', thisPlugin.pogoActionsDialog);
3361 toolbox.appendChild(buttonPoGo);
3362
3363 const buttonGrid = document.createElement('a');
3364 buttonGrid.textContent = 'PoGo Settings';
3365 buttonGrid.title = 'Settings for S2 & PokemonGo';
3366 buttonGrid.addEventListener('click', e => {
3367 if (thisPlugin.isSmart)
3368 window.show('map');
3369 showS2Dialog();
3370 });
3371 toolbox.appendChild(buttonGrid);
3372
3373 map.on('zoomend', zoomListener);
3374 zoomListener();
3375 map.on('moveend', updateMapGrid);
3376 updateMapGrid();
3377 map.on('overlayadd', function(event) {
3378 if (event && event.name === "S2 Grid") {
3379 updateMapGrid();
3380 }
3381 });
3382
3383 // add ids to the links that we want to be able to hide
3384 const links = document.querySelectorAll('#toolbox > a');
3385 links.forEach(a => {
3386 const text = a.textContent;
3387 if (text == 'Region scores') {
3388 a.id = 'scoresLink';
3389 }
3390 if (text == 'Artifacts') {
3391 a.id = 'artifactLink';
3392 }
3393 });
3394
3395 };
3396
3397 function createCounter(title, type, callback) {
3398 const div = document.createElement('div');
3399 div.style.display = 'none';
3400 const sTitle = document.createElement('span');
3401 sTitle.textContent = title;
3402 const counter = document.createElement('a');
3403 counter.id = 'PogoCounter-' + type;
3404 counter.addEventListener('click', function (e) {
3405 callback(counter.PogoData);
3406 return false;
3407 });
3408 div.appendChild(sTitle);
3409 div.appendChild(counter);
3410 return div;
3411 }
3412
3413 function updateCounter(type, data) {
3414 const counter = document.querySelector('#PogoCounter-' + type);
3415 counter.PogoData = data;
3416 counter.textContent = data.length;
3417 counter.parentNode.style.display = data.length > 0 ? '' : 'none';
3418
3419 // Adjust visibility of the pane to avoid the small gap due to padding
3420 const pane = counter.parentNode.parentNode;
3421 if (data.length > 0) {
3422 pane.style.display = '';
3423 return;
3424 }
3425 let node = pane.firstElementChild;
3426 while (node) {
3427 const rowData = node.lastElementChild.PogoData;
3428 if (rowData && rowData.length > 0) {
3429 pane.style.display = '';
3430 return;
3431 }
3432 node = node.nextElementSibling;
3433 }
3434 pane.style.display = 'none';
3435 }
3436
3437 // PLUGIN END //////////////////////////////////////////////////////////
3438
3439 setup.info = plugin_info; //add the script info data to the function as a property
3440 // if IITC has already booted, immediately run the 'setup' function
3441 if (window.iitcLoaded) {
3442 setup();
3443 } else {
3444 if (!window.bootPlugins) {
3445 window.bootPlugins = [];
3446 }
3447 window.bootPlugins.push(setup);
3448 }
3449}
3450
3451 const plugin_info = {};
3452 if (typeof GM_info !== 'undefined' && GM_info && GM_info.script) {
3453 plugin_info.script = {
3454 version: GM_info.script.version,
3455 name: GM_info.script.name,
3456 description: GM_info.script.description
3457 };
3458 }
3459
3460 // Greasemonkey. It will be quite hard to debug
3461 if (typeof unsafeWindow != 'undefined' || typeof GM_info == 'undefined' || GM_info.scriptHandler != 'Tampermonkey') {
3462 // inject code into site context
3463 const script = document.createElement('script');
3464 script.appendChild(document.createTextNode('(' + wrapperPlugin + ')(' + JSON.stringify(plugin_info) + ');'));
3465 (document.body || document.head || document.documentElement).appendChild(script);
3466 } else {
3467 // Tampermonkey, run code directly
3468 wrapperPlugin(plugin_info);
3469 }
3470})();