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

{{level}} level of grid to display:

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

Colors

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

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

` const html = selectRow .replace('{{title}}', '1st Grid') .replace( '{{width}}', " Width: " ) .replace(/{{id}}/g, 'grid0') + selectRow .replace('{{title}}', '2nd Grid') .replace( '{{width}}', " Width: " ) .replace(/{{id}}/g, 'grid1') + selectRow .replace('{{title}}', 'Cells with extra gyms') .replace(/{{id}}/g, 'cellsExtraGyms') .replace('{{width}}', '') + selectRow .replace('{{title}}', 'Cells with missing gyms') .replace(/{{id}}/g, 'cellsMissingGyms') .replace('{{width}}', '') + selectRow .replace( '{{title}}', `Cell ${poiCellLevel} with a gym or stop` ) .replace(/{{id}}/g, 'cell17Filled') .replace('{{width}}', '') + selectRow .replace('{{title}}', `Cell ${gymCellLevel} with 3 gyms`) .replace(/{{id}}/g, 'cell14Filled') .replace('{{width}}', '') + selectRow .replace('{{title}}', '20m submit radius border') .replace(/{{id}}/g, 'nearbyCircleBorder') .replace('{{width}}', '') + selectRow .replace('{{title}}', '20m submit radius fill') .replace(/{{id}}/g, 'nearbyCircleFill') .replace('{{width}}', '') + selectRow .replace('{{title}}', '1 more stop to get a gym') .replace(/{{id}}/g, 'missingStops1') .replace('{{width}}', '') + selectRow .replace('{{title}}', '2 more stops to get a gym') .replace(/{{id}}/g, 'missingStops2') .replace('{{width}}', '') + selectRow .replace('{{title}}', '3 more stops to get a gym') .replace(/{{id}}/g, 'missingStops3') .replace('{{width}}', '') + selectRow .replace('{{title}}', 'PokeStop inside') .replace(/{{id}}/g, 'photoStopInner') .replace('{{width}}', '') + selectRow .replace('{{title}}', 'PokeStop inside (no photo)') .replace(/{{id}}/g, 'stopInner') .replace('{{width}}', '') + selectRow .replace('{{title}}', 'PokeStop outside') .replace(/{{id}}/g, 'stopOuter') .replace('{{width}}', '') + selectRow .replace('{{title}}', 'Gym inside') .replace(/{{id}}/g, 'gymInner') .replace('{{width}}', '') + selectRow .replace('{{title}}', 'EX-Gym inside') .replace(/{{id}}/g, 'exGymInner') .replace('{{width}}', '') + selectRow .replace('{{title}}', 'Gym outside') .replace(/{{id}}/g, 'gymOuter') .replace('{{width}}', '') + selectRow .replace('{{title}}', 'Not in PoGO inside') .replace(/{{id}}/g, 'notpogoInner') .replace('{{width}}', '') + selectRow .replace('{{title}}', 'Not in PoGO outside') .replace(/{{id}}/g, 'notpogoOuter') .replace('{{width}}', '') + 'Reset all colors' const container = dialog({ id: 's2Colors', width: 'auto', html, title: 'PoGo Grid Colors' }) const div = container[0] const updatedSetting = function (id) { saveSettings() if (id === 'nearbyCircleBorder' || id === 'nearbyCircleFill') { redrawNearbyCircles() } else { updateMapGrid() thisPlugin.addAllMarkers() } } const configureItems = function (key, item, id) { if (!id) { id = item } const entry = settings[key][item] const select = div.querySelector('#' + id + 'Opacity') select.value = entry.opacity select.addEventListener('change', function (event) { settings[key][item].opacity = select.value updatedSetting(id) }) const input = div.querySelector('#' + id + 'Color') input.value = entry.color input.addEventListener('change', function (event) { settings[key][item].color = input.value updatedSetting(id) }) if (entry.width != null) { const widthInput = div.querySelector('#' + id + 'Width') widthInput.value = entry.width widthInput.addEventListener('change', function (event) { settings[key][item].width = widthInput.value updatedSetting(id) }) } } configureItems('grids', 0, 'grid0') configureItems('grids', 1, 'grid1') configureItems('colors', 'cellsExtraGyms') configureItems('colors', 'cellsMissingGyms') configureItems('colors', 'cell17Filled') configureItems('colors', 'cell14Filled') configureItems('colors', 'nearbyCircleBorder') configureItems('colors', 'nearbyCircleFill') configureItems('colors', 'missingStops1') configureItems('colors', 'missingStops2') configureItems('colors', 'missingStops3') configureItems('colors', 'photoStopInner') configureItems('colors', 'stopInner') configureItems('colors', 'stopOuter') configureItems('colors', 'gymInner') configureItems('colors', 'exGymInner') configureItems('colors', 'gymOuter') configureItems('colors', 'notpogoInner') configureItems('colors', 'notpogoOuter') const resetColorsLink = div.querySelector('#resetColorsLink') resetColorsLink.addEventListener('click', function () { container.dialog('close') resetColors() updatedSetting('nearbyCircleBorder') updatedSetting() editColors() }) } /** * Refresh the S2 grid over the map */ function updateMapGrid() { // preconditions if (!map.hasLayer(regionLayer)) { return } const zoom = map.getZoom() // first draw nearby circles at the bottom if (zoom > 16) { if (!regionLayer.hasLayer(nearbyLayerGroup)) { regionLayer.addLayer(nearbyLayerGroup) } nearbyLayerGroup.bringToBack() } else if (regionLayer.hasLayer(nearbyLayerGroup)) { regionLayer.removeLayer(nearbyLayerGroup) } // shade level 14 and level 17 cells let cellsCloseToThreshold if (settings.highlightGymCandidateCells && zoom > 14) { cellsCloseToThreshold = updateCandidateCells(zoom) if (!regionLayer.hasLayer(cellLayerGroup)) { regionLayer.addLayer(cellLayerGroup) } cellLayerGroup.bringToBack() } else if (regionLayer.hasLayer(cellLayerGroup)) { regionLayer.removeLayer(cellLayerGroup) } // then draw the cell grid if (zoom > 4) { drawCellGrid(zoom) // update cell grid with cells close to a threshold for a gym if (cellsCloseToThreshold) { // draw missing cells in reverse order for ( let missingStops = 3; missingStops >= 1; missingStops-- ) { const color = settings.colors['missingStops' + missingStops].color const opacity = settings.colors['missingStops' + missingStops] .opacity cellsCloseToThreshold[missingStops].forEach((cell) => gridLayerGroup.addLayer( drawCell(cell, color, 3, opacity) ) ) } } if (!regionLayer.hasLayer(gridLayerGroup)) { regionLayer.addLayer(gridLayerGroup) } } else if (regionLayer.hasLayer(gridLayerGroup)) { regionLayer.removeLayer(gridLayerGroup) } // update gym centers if (settings.highlightGymCenter && zoom > 16) { updateGymCenters() if (!regionLayer.hasLayer(gymCenterLayerGroup)) { regionLayer.addLayer(gymCenterLayerGroup) } } else if (regionLayer.hasLayer(gymCenterLayerGroup)) { regionLayer.removeLayer(gymCenterLayerGroup) } } function getLatLngPoint(data) { const result = { lat: typeof data.lat === 'function' ? data.lat() : data.lat, lng: typeof data.lng === 'function' ? data.lng() : data.lng } return result } /** * Highlight cells that are missing a few stops to get another gym. Also fills level 17 cells with a stop/gym. * based on data from https://www.reddit.com/r/TheSilphRoad/comments/7ppb3z/gyms_pok%C3%A9stops_and_s2_cells_followup_research/ * Cut offs: 2, 6, 20 */ function updateCandidateCells(zoom) { cellLayerGroup.clearLayers() // All cells with items const allCells = groupByCell(gymCellLevel) const bounds = map.getBounds() const seenCells = {} const cellsCloseToThreshold = { 1: [], 2: [], 3: [] } const drawCellAndNeighbors = function (cell) { const cellStr = cell.toString() if (!seenCells[cellStr]) { // cell not visited - flag it as visited now seenCells[cellStr] = true if (isCellOnScreen(bounds, cell)) { // on screen - draw it const cellData = allCells[cellStr] if (cellData) { // check for errors const missingGyms = computeMissingGyms(cellData) if ( missingGyms > 0 && !ignoredCellsMissingGyms[cellStr] ) { cellLayerGroup.addLayer( fillCell( cell, settings.colors.cellsMissingGyms.color, settings.colors.cellsMissingGyms.opacity ) ) } else if ( missingGyms < 0 && !ignoredCellsExtraGyms[cellStr] ) { cellLayerGroup.addLayer( fillCell( cell, settings.colors.cellsExtraGyms.color, settings.colors.cellsExtraGyms.opacity ) ) if (!cellsExtraGyms[cellStr]) { cellsExtraGyms[cellStr] = true updateCounter( 'extraGyms', Object.keys(cellsExtraGyms) ) } } // shade filled level 17 cells if (zoom > 15) { const subCells = {} const coverLevel17Cell = function (point) { const cell = S2.S2Cell.FromLatLng( point, poiCellLevel ) const cellId = cell.toString() if (subCells[cellId]) { return } subCells[cellId] = true cellLayerGroup.addLayer( fillCell( cell, settings.colors.cell17Filled.color, settings.colors.cell17Filled.opacity ) ) } cellData.gyms.forEach(coverLevel17Cell) cellData.stops.forEach(coverLevel17Cell) } // number of stops to next gym const missingStops = computeMissingStops(cellData) switch (missingStops) { case 0: if (missingGyms <= 0) { cellLayerGroup.addLayer( fillCell( cell, settings.colors.cell14Filled .color, settings.colors.cell14Filled .opacity ) ) } break case 1: case 2: case 3: cellsCloseToThreshold[missingStops].push( cell ) break default: break } cellLayerGroup.addLayer( writeInCell(cell, missingStops) ) } // and recurse to our neighbors const neighbors = cell.getNeighbors() for (let i = 0; i < neighbors.length; i++) { drawCellAndNeighbors(neighbors[i]) } } } } const cell = S2.S2Cell.FromLatLng( getLatLngPoint(map.getCenter()), gymCellLevel ) drawCellAndNeighbors(cell) return cellsCloseToThreshold } function drawCellGrid(zoom) { // clear, to redraw gridLayerGroup.clearLayers() countLayer.clearLayers() const bounds = map.getBounds() const seenCells = {} const drawCellAndNeighbors = function ( cell, color, width, opacity ) { const cellStr = cell.toString() if (!seenCells[cellStr]) { // cell not visited - flag it as visited now seenCells[cellStr] = true if (isCellOnScreen(bounds, cell)) { // on screen - draw it gridLayerGroup.addLayer( drawCell(cell, color, width, opacity) ) // show number of PoI in the cell const cellGroup = cellsPortals[cellStr] if (cellGroup) { countLayer.addLayer( writeInCell( cell, Object.keys(cellGroup.portals).length ) ) } // and recurse to our neighbors const neighbors = cell.getNeighbors() for (let i = 0; i < neighbors.length; i++) { drawCellAndNeighbors( neighbors[i], color, width, opacity ) } } } } for (let i = settings.grids.length - 1; i >= 0; --i) { const grid = settings.grids[i] const gridLevel = grid.level if (gridLevel >= 6 && gridLevel < zoom + 2) { const cell = S2.S2Cell.FromLatLng( getLatLngPoint(map.getCenter()), gridLevel ) drawCellAndNeighbors( cell, grid.color, grid.width, grid.opacity ) } } } /** * Draw a cross to the center of level 20 cells that have a Gym to check better EX locations */ function updateGymCenters() { // clear gymCenterLayerGroup.clearLayers() const visibleGyms = filterItemsByMapBounds(gyms) const level = 20 Object.keys(visibleGyms).forEach((id) => { const gym = gyms[id] const cell = S2.S2Cell.FromLatLng(gym, level) const corners = cell.getCornerLatLngs() // center point const center = cell.getLatLng() const style = { fill: false, color: 'red', opacity: 0.8, weight: 1, clickable: false, interactive: false } const line1 = L.polyline([corners[0], corners[2]], style) gymCenterLayerGroup.addLayer(line1) const line2 = L.polyline([corners[1], corners[3]], style) gymCenterLayerGroup.addLayer(line2) const circle = L.circle(center, 1, style) gymCenterLayerGroup.addLayer(circle) }) } // Computes how many new stops must be added to the L14 Cell to get a new Gym function computeMissingStops(cellData) { const gyms = cellData.gyms.length // exclude from the count those pokestops that have been marked as missing photos const validStops = cellData.stops.filter( (p) => typeof p.photos === 'undefined' || p.photos > 0 ) const sum = gyms + validStops.length if (sum < 2 && gyms === 0) { return 2 - sum } if (sum < 6 && gyms < 2) { return 6 - sum } if (sum < 20 && gyms < 3) { return 20 - sum } // No options to more gyms ATM. return 0 } // Checks if the L14 cell has enough Gyms and Stops and one of the stops should be marked as a Gym // If the result is negative then it has extra gyms function computeMissingGyms(cellData) { const totalGyms = cellData.gyms.length // exclude from the count those pokestops that have been marked as missing photos const validStops = cellData.stops.filter( (p) => typeof p.photos === 'undefined' || p.photos > 0 ) const sum = totalGyms + validStops.length if (sum < 2) { return 0 - totalGyms } if (sum < 6) { return 1 - totalGyms } if (sum < 20) { return 2 - totalGyms } return 3 - totalGyms } function drawCell(cell, color, weight, opacity) { // corner points const corners = cell.getCornerLatLngs() // the level 6 cells have noticible errors with non-geodesic lines - and the larger level 4 cells are worse // NOTE: we only draw two of the edges. as we draw all cells on screen, the other two edges will either be drawn // from the other cell, or be off screen so we don't care const region = L.geodesicPolyline( [corners[0], corners[1], corners[2], corners[3], corners[0]], { fill: false, color, opacity, weight, clickable: false, interactive: false } ) return region } function fillCell(cell, color, opacity) { // corner points const corners = cell.getCornerLatLngs() const region = L.polygon(corners, { color, fillOpacity: opacity, weight: 0, clickable: false, interactive: false }) return region } /** * Writes a text in the center of a cell */ function writeInCell(cell, text) { // center point const center = cell.getLatLng() const marker = L.marker(center, { icon: L.divIcon({ className: 'pogo-text', iconAnchor: [25, 5], iconSize: [50, 10], html: text }) }) // fixme, maybe add some click handler marker.on('click', function () { displayCellSummary(cell) }) return marker } /** Show a summary with the pokestops and gyms of a L14 Cell */ function displayCellSummary(cell) { const cellStr = cell.toString() const allCells = groupByCell(gymCellLevel) const cellData = allCells[cellStr] if (!cellData) { return } function updateScore(portal, wrapper) { const photos = typeof portal.photos === 'undefined' ? 1 : portal.photos const votes = photos === 0 || typeof portal.votes === 'undefined' ? 0 : portal.votes const score = photos + votes wrapper.querySelector('.Pogo_Score').textContent = score } function dumpGroup(data, title, useHeader) { const div = document.createElement('div') const header = document.createElement('h3') if (useHeader) { header.className = 'header' header.innerHTML = '' + title + ' (' + data.length + ')PhotosVotesScore' } else { header.textContent = title + ' (' + data.length + ')' } div.appendChild(header) data.sort(sortByName).forEach((portal) => { const wrapper = document.createElement('div') wrapper.setAttribute('data-guid', portal.guid) wrapper.className = 'PortalSummary' const img = getPortalImage(portal) let scoreData = '' if (title === 'Pokestops' || title === 'Portals') { const photos = typeof portal.photos === 'undefined' ? 1 : portal.photos const votes = typeof portal.votes === 'undefined' ? 0 : portal.votes scoreData = '' + '' + '' + (photos + votes) + '' } wrapper.innerHTML = '' + img + '' + '' + getPortalName(portal) + '' + scoreData if (scoreData !== '') { wrapper .querySelector('.Pogo_Photos') .addEventListener('input', function () { const update = portal.photos !== this.valueAsNumber && (portal.photos === 0 || this.valueAsNumber === 0) portal.photos = this.valueAsNumber updateScore(portal, wrapper) // any item getting photos edited should be a pokestop updateItemInObjectStoreStore( S2.db, POKESTOPS, portal ) if (update) { refreshPokestopMissingPhotoStatus(portal) updateMapGrid() } }) wrapper .querySelector('.Pogo_Votes') .addEventListener('input', function () { portal.votes = this.valueAsNumber updateScore(portal, wrapper) // any item getting votes edited should be a pokestop updateItemInObjectStoreStore( S2.db, POKESTOPS, portal ) }) } div.appendChild(wrapper) }) return div } const div = document.createElement('div') div.appendChild(dumpGroup(cellData.gyms, 'Gyms')) div.appendChild(dumpGroup(cellData.stops, 'Pokestops', true)) // div.appendChild(dumpGroup(cellData.notClassified, 'Other portals')); They don't matter, they have been removed from Pokemon // div.appendChild(dumpGroup(cellData.portals, 'Portals', true)); FIXME: portals from Ingress that are hidden in Pokemon div.className = 'PogoListing' const width = Math.min(screen.availWidth, 420) const container = dialog({ id: 'PokemonList', html: div, width: width + 'px', title: 'List of Pokestops and Gyms' }) configureHoverMarker(container) } // *************************** // IITC code // *************************** // ensure plugin framework is there, even if iitc is not yet loaded if (typeof window.plugin !== 'function') { window.plugin = function () {} } // PLUGIN START // use own namespace for plugin window.plugin.pogo = function () {} const thisPlugin = window.plugin.pogo const KEY_STORAGE = 'plugin-pogo' const KEY_SETTINGS = 'plugin-pogo-settings' const GYMS = 'gyms' const POKESTOPS = 'pokestops' const NOTPOGO = 'notpogo' const EXTRA_GYMS = 'ignoredCellsExtraGyms' const MISSING_GYMS = 'ignoredCellsMissingGyms' const MANUAL_WAYPOINTS = 'waypoints' /** * Create a new object where the extra properties of each pokestop/gym have been removed. Store only the minimum. */ function cleanUpExtraData(group) { const newGroup = {} Object.keys(group).forEach((id) => { newGroup[id] = cleanUpSingleItem(group[id]) }) return newGroup } function cleanUpSingleItem(data) { const newData = { guid: data.guid, lat: data.lat, lng: data.lng, name: data.name } // TODO: What prevents just setting these in the new object? if (data.isEx) { newData.isEx = data.isEx } if (data.medal) { newData.medal = data.medal } if (typeof data.photos !== 'undefined') { newData.photos = data.photos } if (data.votes) { newData.votes = data.votes } return newData } /** * Add a JSON object to an object store with a defined keyPath * @param {*} db containing the object store * @param {*} objectStoreName to add item to * @param {*} item to add */ function addItemToObjectStore(db, objectStoreName, item) { addItemsToObjectStore(db, objectStoreName, [item]) } /** * Add a JSON object to an object store with a defined keyPath * @param {*} db containing the object store * @param {*} objectStoreName to add item to * @param {*} items to add */ function addItemsToObjectStore( db, objectStoreName, items, itemSuccessCallback ) { const tx = db.transaction(objectStoreName, 'readwrite') tx.onabort = (e) => { console.log('txaborted!') console.log(e) } tx.onerror = (e) => { console.log('txerror!') console.log(e) } const os = tx.objectStore(objectStoreName) let successCount = 0 let lastAdd if (items.length === 0) { itemSuccessCallback(objectStoreName + ': nothing to import!') } items.forEach((item) => { lastAdd = os.add(item) lastAdd.onsuccess = (e) => { successCount++ if (itemSuccessCallback && successCount % 2500 === 0) { itemSuccessCallback( objectStoreName + ': ' + successCount ) } } }) if (lastAdd) { lastAdd.onsuccess = (e) => { if (itemSuccessCallback) { itemSuccessCallback(objectStoreName + ': completed!') } } } if (tx.commit) { tx.commit() } return tx } /** * Set a flag in an object store * @param {*} db containing the object store * @param {*} objectStoreName to set a flag in * @param {*} key to flag */ function setKeyInObjectStore(db, objectStoreName, key) { db.transaction(objectStoreName, 'readwrite') .objectStore(objectStoreName) .add(key, key) } /** * Update a JSON object in an object store with a defined keyPath * @param {*} db containing the object store * @param {*} objectStoreName to update item in * @param {*} item to update */ function updateItemInObjectStoreStore(db, objectStoreName, item) { db.transaction(objectStoreName, 'readwrite') .objectStore(objectStoreName) .put(item) } /** * Delete an object from an object store * @param {*} db containing the object store * @param {*} objectStoreName to remove the item from * @param {*} key to remove */ function deleteFromObjectStore(db, objectStoreName, key) { db.transaction(objectStoreName, 'readwrite') .objectStore(objectStoreName) .delete(key) } /** * Create an object store and load it from the provided data using the loader function * @param {*} db to create the object store in * @param {*} objectStoreName to create if it does not exist * @param {*} keyPath to apply to the object store, or null to require explicit keys * @param {*} data to provide to the loader * @param {*} loader to update the object store */ function createAndLoadObjectStore( db, objectStoreName, keyPath, data, loader ) { if (!db.objectStoreNames.contains(objectStoreName)) { const objectStore = db.createObjectStore(objectStoreName, { keyPath }) objectStore.transaction.addEventListener( 'complete', (event) => { const tx = db.transaction(objectStoreName, 'readwrite') const store = tx.objectStore(objectStoreName) loader.apply(this, [store, data]) if (tx.commit) { tx.commit() } } ) } } /** * Obtain all entries from an object store and load it using the loader function * @param {*} db containing the object store * @param {*} objectStoreName to retrieve the data from * @param {*} loader to process the retrieved data */ function loadAllFromObjectStore(db, objectStoreName, loader) { // prettier is not happy with just setting this function const onSuccess = (event) => { const result = event.target.result if (result) { loader.apply(this, [result]) } thisPlugin.resetAllMarkers() } db .transaction(objectStoreName) .objectStore(objectStoreName) .getAll().onsuccess = onSuccess } function loadPogoS2Data(db) { loadAllFromObjectStore(db, GYMS, (all) => all.forEach((gym) => { gyms[gym.guid] = gym }) ) loadAllFromObjectStore(db, POKESTOPS, (all) => all.forEach((pokestop) => { pokestops[pokestop.guid] = pokestop }) ) loadAllFromObjectStore(db, NOTPOGO, (all) => all.forEach((nothing) => { notpogo[nothing.guid] = nothing }) ) loadAllFromObjectStore(db, EXTRA_GYMS, (all) => all.forEach((ignoredCell) => { ignoredCellsExtraGyms[ignoredCell] = true }) ) loadAllFromObjectStore(db, MISSING_GYMS, (all) => all.forEach((ignoredCell) => { ignoredCellsMissingGyms[ignoredCell] = true }) ) loadAllFromObjectStore(db, MANUAL_WAYPOINTS, (all) => all.forEach((manualWaypoint) => { waypoints[manualWaypoint.guid] = manualWaypoint }) ) } /** * Delete all entries from an object store * @param {*} db containing the object store * @param {*} objectStoreName to remove all data from */ function deleteAllFromObjectStore(db, objectStoreName) { db.transaction(objectStoreName, 'readwrite') .objectStore(objectStoreName) .clear() } thisPlugin.createEmptyStorage = function () { gyms = {} pokestops = {} notpogo = {} ignoredCellsExtraGyms = {} ignoredCellsMissingGyms = {} waypoints = {} deleteAllFromObjectStore(S2.db, GYMS) deleteAllFromObjectStore(S2.db, POKESTOPS) deleteAllFromObjectStore(S2.db, NOTPOGO) deleteAllFromObjectStore(S2.db, EXTRA_GYMS) deleteAllFromObjectStore(S2.db, MISSING_GYMS) deleteAllFromObjectStore(S2.db, MANUAL_WAYPOINTS) allPortals = {} newPortals = {} movedPortals = [] missingPortals = {} } /*************************************************************************/ thisPlugin.findByGuid = function (guid) { if (gyms[guid]) { return { type: GYMS, store: gyms } } if (pokestops[guid]) { return { type: POKESTOPS, store: pokestops } } if (notpogo[guid]) { return { type: NOTPOGO, store: notpogo } } return null } // Append a 'star' flag in sidebar. thisPlugin.onPortalSelectedPending = false thisPlugin.onPortalSelected = function () { $('.pogoStop').remove() $('.pogoGym').remove() $('.notPogo').remove() const portalDetails = document.getElementById('portaldetails') portalDetails.classList.remove('isGym') if (window.selectedPortal == null) { return } if (!thisPlugin.onPortalSelectedPending) { thisPlugin.onPortalSelectedPending = true setTimeout(function () { // the sidebar is constructed after firing the hook thisPlugin.onPortalSelectedPending = false $('.pogoStop').remove() $('.pogoGym').remove() $('.notPogo').remove() // Show PoGo icons in the mobile status-bar if (thisPlugin.isSmart) { document.querySelector('.PogoStatus').innerHTML = thisPlugin.htmlStar $('.PogoStatus > a').attr('title', '') } let modHtml = "
Pokemon Go: " + thisPlugin.htmlStar + '
' + "
" + "
' + "" + '
' if (window.selectedPortal.includes('s2-pogo')) { modHtml += "Delete Waypoint" } $(portalDetails).append(modHtml) document .getElementById('PogoGymMedal') .addEventListener('change', (ev) => { const guid = window.selectedPortal const icon = document.getElementById( 'gym' + guid.replace('.', '') ) // remove styling of gym marker if (icon) { icon.classList.remove( gyms[guid].medal + 'Medal' ) } gyms[guid].medal = ev.target.value updateItemInObjectStoreStore( S2.db, GYMS, gyms[guid] ) // update gym marker if (icon) { icon.classList.add(gyms[guid].medal + 'Medal') } }) document .getElementById('PogoGymEx') .addEventListener('change', (ev) => { const guid = window.selectedPortal const icon = document.getElementById( 'gym' + guid.replace('.', '') ) gyms[guid].isEx = ev.target.checked updateItemInObjectStoreStore( S2.db, GYMS, gyms[guid] ) // update gym marker if (icon) { icon.classList[ gyms[guid].isEx ? 'add' : 'remove' ]('exGym') } }) thisPlugin.updateStarPortal() }, 0) } } // Update the status of the star (when a portal is selected from the map/pogo-list) thisPlugin.updateStarPortal = function () { $('.pogoStop').removeClass('favorite') $('.pogoGym').removeClass('favorite') $('.notPogo').removeClass('favorite') document.getElementById('portaldetails').classList.remove('isGym') const guid = window.selectedPortal // If current portal is into pogo: select pogo portal from portals list and select the star const pogoData = thisPlugin.findByGuid(guid) if (pogoData) { if (pogoData.type === POKESTOPS) { $('.pogoStop').addClass('favorite') } if (pogoData.type === GYMS) { $('.pogoGym').addClass('favorite') document .getElementById('portaldetails') .classList.add('isGym') const gym = gyms[guid] if (gym.medal) { document.getElementById('PogoGymMedal').value = gym.medal } document.getElementById('PogoGymEx').checked = gym.isEx } if (pogoData.type === NOTPOGO) { $('.notPogo').addClass('favorite') } } } function removePogoObject(type, guid) { if (type === POKESTOPS) { delete pokestops[guid] deleteFromObjectStore(S2.db, POKESTOPS, guid) const starInLayer = stopLayers[guid] stopLayerGroup.removeLayer(starInLayer) delete stopLayers[guid] } if (type === GYMS) { delete gyms[guid] deleteFromObjectStore(S2.db, GYMS, guid) const gymInLayer = gymLayers[guid] gymLayerGroup.removeLayer(gymInLayer) delete gymLayers[guid] } if (type === NOTPOGO) { delete notpogo[guid] deleteFromObjectStore(S2.db, NOTPOGO, guid) const notpogoInLayer = notpogoLayers[guid] notpogoLayerGroup.removeLayer(notpogoInLayer) delete notpogoLayers[guid] } } // Switch the status of the star thisPlugin.switchStarPortal = function (type) { const guid = window.selectedPortal // It has been manually classified, remove from the detection if (newPortals[guid]) { delete newPortals[guid] } // If portal is saved in pogo: Remove this pogo const pogoData = thisPlugin.findByGuid(guid) if (pogoData) { const existingType = pogoData.type removePogoObject(existingType, guid) thisPlugin.updateStarPortal() // Get portal name and coordinates const p = window.portals[guid] const ll = p.getLatLng() if (existingType !== type) { thisPlugin.addPortalpogo( guid, ll.lat, ll.lng, p.options.data.title, type ) } // we've changed one item from pogo, if the cell was marked as ignored, reset it. if (type === GYMS || existingType === GYMS) { updateExtraGymsCells(ll.lat, ll.lng) } } else { // If portal isn't saved in pogo: Add this pogo // Get portal name and coordinates const portal = window.portals[guid] const latlng = portal.getLatLng() thisPlugin.addPortalpogo( guid, latlng.lat, latlng.lng, portal.options.data.title, type ) } if (settings.highlightGymCandidateCells) { updateMapGrid() } } thisPlugin.deletePortalpogo = function (guid) { if (gyms[guid]) { delete gyms[guid] deleteFromObjectStore(S2.db, GYMS, guid) } if (pokestops[guid]) { delete pokestops[guid] deleteFromObjectStore(S2.db, POKESTOPS, guid) } if (notpogo[guid]) { delete notpogo[guid] deleteFromObjectStore(S2.db, NOTPOGO, guid) } } // Add portal thisPlugin.addPortalpogo = function (guid, lat, lng, name, type) { // Add pogo in the idb const obj = { guid, lat, lng, name } // prevent that it would trigger the missing portal detection if it's in our data if (window.portals[guid]) { obj.exists = true } if (type === GYMS) { updateExtraGymsCells(lat, lng) gyms[guid] = obj addItemToObjectStore(S2.db, GYMS, cleanUpSingleItem(obj)) } if (type === POKESTOPS) { updateExtraGymsCells(lat, lng) pokestops[guid] = obj addItemToObjectStore(S2.db, POKESTOPS, cleanUpSingleItem(obj)) } if (type === NOTPOGO) { notpogo[guid] = obj addItemToObjectStore(S2.db, NOTPOGO, cleanUpSingleItem(obj)) } thisPlugin.updateStarPortal() thisPlugin.addStar(guid, lat, lng, name, type) } /** * An item has been changed in a cell, check if the cell should no longer be ignored */ function updateExtraGymsCells(lat, lng) { if ( Object.keys(ignoredCellsExtraGyms).length === 0 && Object.keys(ignoredCellsMissingGyms).length === 0 ) { return false } const cell = S2.S2Cell.FromLatLng( new L.LatLng(lat, lng), gymCellLevel ) const cellId = cell.toString() if (ignoredCellsExtraGyms[cellId]) { delete ignoredCellsExtraGyms[cellId] deleteFromObjectStore(S2.db, EXTRA_GYMS, cellId) return true } if (ignoredCellsMissingGyms[cellId]) { delete ignoredCellsMissingGyms[cellId] deleteFromObjectStore(S2.db, MISSING_GYMS, cellId) return true } return false } /* OPTIONS */ // Manual import, export and reset data thisPlugin.pogoActionsDialog = function () { const content = `
Save... Reset PoGo portals Import Gyms & Pokestops Export Gyms & Pokestops
Gyms:${Object.keys(gyms).length}
Pokestops:${Object.keys(pokestops).length}
Manual waypoints:${Object.keys(waypoints).length}
` const container = dialog({ html: content, title: 'S2 & Pokemon Actions' }) const div = container[0] div.querySelector('#save-dialog').addEventListener('click', (e) => saveDialog() ) } function saveDialog() { const content = `

Select the data to save from the info on screen

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

' + message + '

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