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