// ==UserScript== // @author azrael-42 // @id dronehelper@azrael-42 // @name Drone Helper // @category Misc // @version 0.5.3.2 // @updateURL https://raw.githubusercontent.com/IITC-CE/Community-plugins/master/dist/azrael-42/dronehelper.meta.js // @downloadURL https://raw.githubusercontent.com/IITC-CE/Community-plugins/master/dist/azrael-42/dronehelper.user.js // @homepageURL https://github.com/azrael-42/IITC-Drone-Helper/ // @description Display area drone can "see" from currently selected portal. Manually record if drone has visited portals // @match https://intel.ingress.com/* // @grant none // ==/UserScript== /*global $:false */ /*global portals:false */ /*global map:false */ /*global L:false */ "use strict"; function wrapper(plugin_info) { // this is based on https://github.com/jonatkins/s2-geometry-javascript // Renamed/Namespaced inside the plugin as I have added some extra functions, and left in window scope it may be overwritten /* Some explanatory notes from the original // S2 Geometry functions // 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; } else { return 2; } } else { if (temp[1] > temp[2]) { return 1; } else { return 2; } } }; static faceXYZToUV = function(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'}; break; } return [u,v]; } static XYZToFaceUV = function(xyz) { let face = dh_S2.largestAbsComponent(xyz); if (xyz[face] < 0) { face += 3; } let uv = dh_S2.faceXYZToUV (face,xyz); return [face, uv]; }; static FaceUVToXYZ = function(face,uv) { let u = uv[0]; let 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'}; } }; static STToUV = function(st) { let singleSTtoUV = function (st) { if (st >= 0.5) { return (1 / 3.0) * (4 * st * st - 1); } else { return (1 / 3.0) * (1 - (4 * (1 - st) * (1 - st))); } }; return [singleSTtoUV(st[0]), singleSTtoUV(st[1])]; }; static UVToST = function(uv) { let singleUVtoST = function (uv) { if (uv >= 0) { return 0.5 * Math.sqrt(1 + 3 * uv); } else { return 1 - 0.5 * Math.sqrt(1 - 3 * uv); } }; return [singleUVtoST(uv[0]), singleUVtoST(uv[1])]; }; static STToIJ = function(st,order) { let maxSize = (1 << order); let singleSTtoIJ = function (st) { let ij = Math.floor(st * maxSize); return Math.max(0, Math.min(maxSize - 1, ij)); }; return [singleSTtoIJ(st[0]), singleSTtoIJ(st[1])]; }; static IJToST = function(ij,order,offsets) { let maxSize = (1 << order); return [ (ij[0]+offsets[0])/maxSize, (ij[1]+offsets[1])/maxSize ]; } // hilbert space-filling curve // based on http://blog.notdot.net/2009/11/Damn-Cool-Algorithms-Spatial-indexing-with-Quadtrees-and-Hilbert-Curves // note: rather then calculating the final integer hilbert position, we just return the list of quads // this ensures no precision issues whth large orders (S3 cell IDs use up to 30), and is more // convenient for pulling out the individual bits as needed later static pointToHilbertQuadList = function(x,y,order) { let hilbertMap = { 'a': [[0, 'd'], [1, 'a'], [3, 'b'], [2, 'a']], 'b': [[2, 'b'], [1, 'b'], [3, 'a'], [0, 'c']], 'c': [[2, 'c'], [3, 'd'], [1, 'c'], [0, 'b']], 'd': [[0, 'a'], [3, 'c'], [1, 'd'], [2, 'd']] }; let currentSquare='a'; let positions = []; for (let i=order-1; i>=0; i--) { let mask = 1<=0 && ij[1]>=0 && ij[0]e;e++){var s=r.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null===o||void 0===o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;t>e;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(o){return null==o?"":i._compress(o,6,function(o){return e.charAt(o)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(n){return o(e,r.charAt(n))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(o,r,n){if(null==o)return"";var e,t,i,s={},p={},u="",c="",a="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;ie;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++),s[c]=f++,a=String(u)}if(""!==a){if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==r-1){d.push(n(m));break}v++}return d.join("")},decompress:function(o){return null==o?"":""==o?null:i._decompress(o.length,32768,function(r){return o.charCodeAt(r)})},_decompress:function(o,n,e){var t,i,s,p,u,c,a,l,f=[],h=4,d=4,m=3,v="",w=[],A={val:e(0),position:n,index:1};for(i=0;3>i;i+=1)f[i]=i;for(p=0,c=Math.pow(2,2),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(t=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 2:return""}for(f[3]=l,s=l,w.push(l);;){if(A.index>o)return"";for(p=0,c=Math.pow(2,m),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(l=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 2:return w.join("")}if(0==h&&(h=Math.pow(2,m),m++),f[l])v=f[l];else{if(l!==d)return null;v=s+s.charAt(0)}w.push(v),f[d++]=s+v.charAt(0),h--,s=v,0==h&&(h=Math.pow(2,m),m++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module&&(module.exports=LZString); window.plugin.droneHelper = function() {} const self = window.plugin.droneHelper; const dh_view = window.plugin.dh_view; const dh_visits = window.plugin.dh_visits; const dh_route = window.plugin.dh_route; const dh_coverage = window.plugin.dh_coverage; self.DEBUG = false; /**********************************************************************************************************************/ /** DIMENSIONS etc ****************************************************************************************************/ /**********************************************************************************************************************/ self.VISIBLE_RADIUS = 500; self.COVERAGE_S2_SIZE = 16; self.TRAVEL_WITH_KEY = 1250; self.plotReachable = false; self.cellStatus = {}; // expected to be 'visible', 'reachable', 'outside' self.portalsReachable = {}; // guids - true if used to extend, false if not been visible/reachable yet self.portalsToCheck = {}; self.currentLocation = null; self.cellColouring = { visible:{stroke:false, fillColor: '#00ffff', fillOpacity: 0.5, interactive: false}, reachable:{stroke:false, fillColor: '#00ffff', fillOpacity: 0.2, interactive: false}, outside:{stroke:false, fillColor: '#00ffff', fillOpacity: 0, interactive: false} }; /* syncType - 'merge' - requires timestamp, to be held as .t */ window.plugin.dh_sync = class { constructor(plugin, pluginName, fieldName, syncType, compress, callback) { this.haveChanges = false; this.enableSync = false; this.SYNC_DELAY = 5000; this.pluginName = pluginName; this.fieldName = fieldName; this.compress = compress; this.syncType = syncType; this.plugin = plugin; this.callback = callback; this.localStorageKey = pluginName + '[' + fieldName + ']'; if (syncType != null) window.addHook('iitcLoaded', this.registerSyncHandler.bind(this)); } //Call after IITC and all plugins loaded registerSyncHandler() { if(!window.plugin.sync) return; window.plugin.sync.registerMapForSync(this.pluginName, this.fieldName, this.syncCallback.bind(this), this.syncInitialised.bind(this)); } // e is null, included for backwards compatibility; either called with no parameters, or fullUpdate is true syncCallback(pluginName, fieldName, e, fullUpdate) { if (fieldName === this.fieldName) { if (this.syncType === 'merge') this.mergeSyncData(); else this.saveLocal(); if (this.callback !== null) this.callback(); } } mergeSyncData() { let newData = this.plugin[this.fieldName]; this.loadLocal(); let originalData = this.plugin[this.fieldName]; // if the portal id is in the sync data, and timestamp is not older than our current data, keep sync data for (const id in originalData) { if (newData[id] && newData[id].timestamp && !newData[id].t) { newData[id].t = newData[id].timestamp; delete newData[id].timestamp; } if (originalData[id].timestamp && !originalData[id].t) { originalData[id].t = originalData[id].timestamp; delete originalData[id].timestamp; } if (newData[id] && newData[id].t >= originalData[id].t) { originalData[id] = newData[id]; delete newData[id]; } else { this.haveChanges = true; } } for (const id in newData) { originalData[id] = newData[id]; } this.saveLocal(); } // called by sync as signal it is ready to sync, also after every time it checks files for updates syncInitialised(pluginName,fieldName) { if(fieldName === this.fieldName) { this.enableSync = true; if(this.haveChanges) { this.delaySync(); } } } delaySync() { this.haveChanges = true; if(this.syncType === null || !this.enableSync) return; clearTimeout(this.delaySync.timer); this.delaySync.timer = setTimeout(() => { this.delaySync.timer = null; this.syncNow(); }, this.SYNC_DELAY); } syncNow() { if(!this.enableSync) return; plugin.sync.updateMap(this.pluginName, this.fieldName, Object.keys(this.plugin[this.fieldName])); this.haveChanges = false; } saveLocal() { let value = JSON.stringify(this.plugin[this.fieldName]); localStorage[this.localStorageKey] = this.compress ? LZString.compress(value) : value; } loadLocal() { if(localStorage[this.localStorageKey] !== undefined) { //storageSettings.key = null; // if (this.compress) this.plugin[this.fieldName] = JSON.parse(LZString.decompress(localStorage[this.localStorageKey])); if (this.plugin[this.fieldName] === null) this.plugin[this.fieldName] = JSON.parse(localStorage[this.localStorageKey]) //if (this.callback !== null) this.callback(); } } save() { this.saveLocal(); if (this.syncType !== null) this.delaySync(); } load() { this.loadLocal(); } }/**********************************************************************************************************************/ /** UTILITY FUNCTIONS - used by more than one associate plug-in *******************************************************/ /**********************************************************************************************************************/ // record keys.... // gets data from: // Live Inventory (uses inventory information made available to C.O.R.E subscribers): https://github.com/EisFrei/IngressLiveInventory/ // Keys plug-in (uses key counts obtained from user input) - one of standard plug-ins, https://iitc.app/ window.plugin.dh_utility = { portalsWithKeys: {}, keysPluginTimeout: 400, liveInventoryTimeout: 400, addNewLayerToIITC: function(label, name) { let layer = new L.FeatureGroup(); window.addLayerGroup(label, layer, true) map.on('layeradd', (obj) => { if(obj.layer === layer) { delete window.plugin[name].disabled; } }); map.on('layerremove', (obj) => { if(obj.layer === layer) { window.plugin[name].disabled = true; } }); // ensure 'disabled' flag is initialised if (!map.hasLayer(layer)) { window.plugin[name].disabled = true; } return layer; }, uuidv4: function() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }, isPortalVisible: function(droneLocn, portalLocn) { const dist = this.haversineDistance(droneLocn, portalLocn, window.plugin.dh_distance.earthRadius) // any portal inside the defining circle will be visible if (dist < window.plugin.dh_view.visibilityParams.radius) return true; // using dimensions on https://s2geometry.io/resources/s2cell_statistics.html, it looks pretty safe to say no L16 cell will be as large as 200x200, so any portal beyond the 500m circle and the diagonal of a 200mx200m quad will not be visible if (dist > Math.sqrt(200*200*2)+window.plugin.dh_view.visibilityParams.radius) return false; let visibleCells = this.findCellsCoveringCircle(droneLocn, window.plugin.dh_view.visibilityParams.radius, 16, 'visible'); let portalCell = dh_S2.S2Cell.FromLatLng(portalLocn, 16); return this.cellInArray(portalCell, visibleCells); }, findCellsCoveringCircle: function(centre, radius, cellLevel, type) { let emptyCells = []; let includedCells = []; let cell = dh_S2.S2Cell.FromLatLng(centre, cellLevel); let queuedCells = [cell]; while (queuedCells.length > 0) { let nextCell = queuedCells.pop(); if (this.cellInArray(nextCell,emptyCells)) continue; if (this.cellInArray(nextCell,includedCells)) continue; nextCell.visible = this.cellContainsCircle(nextCell, centre, radius, type); if (nextCell.visible || type !== 'visible') includedCells.push(nextCell); else emptyCells.push(nextCell); if (nextCell.visible) { let neighbours = nextCell.getNeighbors(); queuedCells = queuedCells.concat(neighbours); } } return includedCells; }, cellInArray: function(cell, array) { for (let i = 0; i < array.length; i++) { if (cell.equals(array[i])) return true; } return false; }, cellContainsCircle: function (cell, centre, radius, type) { let corners = cell.getCornerLatLngs(); // if a corner is in the circle, we know some part of this cell is in the circle this.sortByDistance(centre,corners); if (this.pointInCircle(corners[0], centre, radius, cell)) return true; return this.lineSegmentInsideCircle(centre, radius, corners[0], corners[1], cell); }, sortByDistance: function(comparison, latlngArray) { latlngArray.sort((l1,l2) => { let d1 = (comparison.lat - l1.lat) * (comparison.lat - l1.lat) + (comparison.lng - l1.lng) * (comparison.lng - l1.lng); // this.haversineDistance(comparison,l1);//comparison.distanceTo(l1); let d2 = (comparison.lat - l2.lat) * (comparison.lat - l2.lat) + (comparison.lng - l2.lng) * (comparison.lng - l2.lng); // this.haversineDistance(comparison,l2);//comparison.distanceTo(l2); if (d1 < d2) return -1; if (d1 > d2) return 1; return 0; }) }, pointInCircle: function (point, centre, radius, cell) { let d = this.viewDistance(centre, point);//centre.distanceTo([point.lat, point.lng]) cell.distance = d; return (d < radius) }, lineSegmentInsideCircle: function(centre, radius, l1, l2, cell) { let c = map.project(centre); let p1 = map.project(l1); let p2 = map.project(l2); let p = L.LineUtil.closestPointOnSegment(c,p1,p2); let l = map.unproject(p); return this.pointInCircle(l, centre, radius, cell); }, // I don't use this method, but at some point want to compare results lineSegmentInsideCircle_quartered: function(centre, radius, l1, l2) { let half = L.latLng([(l1.lat + l2.lat)/2, (l1.lng + l2.lng)/2]); let q1 = L.latLng([(half.lat + l1.lat)/2, (half.lng + l1.lng)/2]); let q2 = L.latLng([(half.lat + l2.lat)/2, (half.lng + l2.lng)/2]); let x = this.pointInCircle(half, centre, radius); let y = this.pointInCircle(l1, centre, radius); let z = this.pointInCircle(l2, centre, radius); return this.pointInCircle(half, centre, radius) || this.pointInCircle(q1, centre, radius) || this.pointInCircle(q2, centre, radius); }, viewDistance: function(latlng1, latlng2) { return this.mercatorDistance(latlng1,latlng2); }, // allow changing of radius to match observations of drone journey viewDistance - these appear to be based on polar radius haversineDistance: function(latlng1, latlng2, R) { if (R === null || R === undefined || R === L.CRS.Earth.R) return map.distance(latlng1,latlng2); else return map.distance(latlng1,latlng2) * R / L.CRS.Earth.R; }, // viewDistance based on a point obtained using mercator projection, corrected for latitude using average latitude of both latlngs mercatorDistance: function(latlng1, latlng2) { let a = L.Projection.SphericalMercator.project(latlng1); let b = L.Projection.SphericalMercator.project(latlng2); let d = a.distanceTo(b); // cartesian viewDistance between 2 points // correction for latitude d = d*Math.cos((latlng1.lat+latlng2.lat)/2*Math.PI/180); // get rid of excessive decimal places // d = Math.round(d * 1000)/1000; // fails at 51.510065,-0.228333 d = Math.ceil(d * 1000)/1000; return d; }, updateKeyOwnership: function() { if (self.haveKeysPlugin) this.addKeysPluginInfo() if (self.haveLiveInventoryPlugin) console.log("liveInventory " + this.addLiveInventoryPluginInfo()) }, addKeysPluginInfo: function() { console.log('trying to change key info') let keysChanged = false; if (!plugin.keys.keys) { this.keysPluginTimeout += 100 setTimeout(this.addKeysPluginInfo, this.keysPluginTimeout) } else { this.keysPluginTimeout = 400 for (const guid in plugin.keys.keys) { if (plugin.keys.keys[guid] > 0) { if (this.portalsWithKeys[guid]) { if (!this.portalsWithKeys[guid].keysPlugin) { this.portalsWithKeys[guid].keysPlugin = true keysChanged = true; } } else { this.portalsWithKeys[guid] = {keysPlugin: true} keysChanged = true; } } } for (const guid in this.portalsWithKeys) { if (guid == '7aaa86c7f0304c7b8dd2208496084576.16') z = 0 if (this.portalsWithKeys[guid].keysPlugin && !plugin.keys.keys[guid]) { this.portalsWithKeys[guid].keysPlugin = false; keysChanged = true } } if (keysChanged) runHooks('dh_keysChanged', '') } }, addLiveInventoryPluginInfo: function() { let keysChanged = false; if (!plugin.LiveInventory.keyCount) { this.liveInventoryTimeout += 100 setTimeout(window.plugin.dh_utility.addLiveInventoryPluginInfo.bind(this), this.liveInventoryTimeout) } else { this.liveInventoryTimeout = 400 const keyCount = plugin.LiveInventory.keyCount; keyCount.forEach(portal => { if (this.setPortalHasKey(portal.portalCoupler.portalGuid, 'liveInventory')) keysChanged = true; }) if (keysChanged) runHooks('dh_keysChanged', '') } }, getPortalHasKey: function(guid, infoSource) { if (this.portalsWithKeys[guid]) return !!this.portalsWithKeys[guid][infoSource] else return false }, setPortalHasKey: function(guid, infoSource) { if (this.portalsWithKeys[guid]) { if (!this.portalsWithKeys[guid][infoSource]) { this.portalsWithKeys[guid][infoSource] = true return true; } } else { this.portalsWithKeys[guid] = {} this.portalsWithKeys[guid][infoSource] = true return true; } return false } } window.plugin.dh_view = { circles:{ outerKey: {radius:1250, color: '#ff0000'} }, cells: {size: 16, drawOptions: {stroke:false, fillColor: '#999999', fillOpacity: 0.4, interactive: false}}, //'#0EC1BB' visibilityParams: {radius: 500, cellSize: 16, type: 'cover', distance: 'mercatorDistance', description: 'Standard drone view, L16 cell, 500m circle'}, settings: {visibilityOption: 'standard'}, visibilityOptions: { standard: {radius: 500, description:'500m circle - standard visibility calculation, works for most devices'}, lowSpec: {radius:300, description:'300m circle - smaller view area, used for some old/low spec devices'} }, setup: function() { this.layer = window.plugin.dh_utility.addNewLayerToIITC('Drone View', 'dh_view') window.addHook('portalSelected', this.onPortalSelected.bind(this)); this.settingsStorageHandler = new window.plugin.dh_sync(this, 'dh_view', 'settings', null, false, null); this.settingsStorageHandler.load(); if (this.visibilityOptions[this.settings.visibilityOption] && this.visibilityOptions[this.settings.visibilityOption].radius) this.visibilityParams.radius = this.visibilityOptions[this.settings.visibilityOption].radius else this.visibilityOptions[this.settings.visibilityOption].radius= this.visibilityParams.radius; this.addViewOptionsToDialog(); window.runHooks('droneViewSettingsChanged'); }, addViewOptionsToDialog() { let html = '

Drone View Options - reachable portals

'; for (const type in this.visibilityOptions) { html += '' + this.visibilityOptions[type].description + '
'; } html += '
'; $('#dialog-dh-options').append(html); }, changeVisType(name, value) { if (name === 'visRadius') { this.visibilityParams.radius = this.visibilityOptions[value].radius; this.settings.visibilityOption = value; } this.settingsStorageHandler.save(); this.drawDroneView(); window.runHooks('droneViewSettingsChanged'); }, onPortalSelected: function (guid) { if (guid === undefined) return; let id = guid.selectedPortalGuid; let p = portals[id]; let coords = p.getLatLng(); this.drawDroneView(coords); }, drawDroneView: function(coords) { coords = coords || self.currentLocation || (portals[window.selectedPortal] && portals[window.selectedPortal].getLatLng()); // coords = coords || self.currentLocation || portals[window.selectedPortal]?.getLatLng(); if (!coords) { console.log('coords false'); return} this.layer.clearLayers(); // any circles required, e.g. key markers for (let circle in this.circles) { L.circle(coords, {radius: this.circles[circle].radius, fill: false, color: this.circles[circle].color, interactive: false}).addTo(this.layer); } // circle to be used for constructing cells controlling visibility L.circle(coords, {radius: this.visibilityParams.radius, fill: false, color: '#0EC1BB', interactive: false}).addTo(this.layer); let cells = window.plugin.dh_utility.findCellsCoveringCircle(coords, this.visibilityParams.radius, this.visibilityParams.cellSize, this.visibilityParams.type); cells.forEach(cell => { if (cell.visible) { let corners = cell.getCornerLatLngs(); L.polygon(corners, this.cells.drawOptions).addTo(this.layer); } }) let visible = this.findVisiblePortals(cells); let oneWay = this.findOneWayJumps(coords,visible); this.highlightPortals(visible,oneWay); window.plugin.dh_route.layer.bringToFront(); window.Render.prototype.bringPortalsToFront(); // See IITC code }, highlightPortals(visible,oneWay) { const scale = portalMarkerScale(); // portal level 0 1 2 3 4 5 6 7 8 const LEVEL_TO_WEIGHT = [2, 2, 2, 2, 2, 3, 3, 4, 4]; const LEVEL_TO_RADIUS = [7, 7, 7, 7, 8, 8, 9,10,11]; let styles = { oneway: {radius:1,fill:true,color:'#ffffff',weight:1,interactive:false,clickable:false}, twoway: {} } for (let guid in visible) { let portal = window.portals[guid]; if (oneWay[guid]) { const level = Math.floor(portal["options"]["level"]||0); const lvlWeight = LEVEL_TO_WEIGHT[level] * Math.sqrt(scale) + 1; const lvlRadius = LEVEL_TO_RADIUS[level] * scale + 3; this.layer.addLayer(L.circleMarker(portal._latlng, { radius: lvlRadius, fill: true, color: "red", weight: lvlWeight, interactive: false, clickable: false })); } else { const level = Math.floor(portal["options"]["level"]||0); const lvlWeight = LEVEL_TO_WEIGHT[level] * Math.sqrt(scale) + 1; const lvlRadius = LEVEL_TO_RADIUS[level] * scale + 3; this.layer.addLayer(L.circleMarker(portal._latlng, { radius: lvlRadius, fill: true, color: "limegreen", weight: lvlWeight, interactive: false, clickable: false })); } } }, findVisiblePortals(cells) { let reachablePortals = {}; for (guid in window.portals) { let testCell = new dh_S2.S2Cell.FromLatLng(window.portals[guid]._latlng, this.visibilityParams.cellSize); for (let cell of cells) { if (cell.equals(testCell) && cell.visible) { reachablePortals[guid] = true; } } } return reachablePortals; }, findOneWayJumps(coords,portals) { let startCell = new dh_S2.S2Cell.FromLatLng(coords, this.visibilityParams.cellSize); let oneWay = {...portals}; for (guid in portals) { if (window.plugin.dh_utility.viewDistance(coords,window.portals[guid]._latlng) < this.visibilityParams.radius) { delete oneWay[guid]; continue; } if (window.plugin.dh_utility.cellContainsCircle(startCell, window.portals[guid]._latlng, this.visibilityParams.radius, this.visibilityParams.type)) { delete oneWay[guid]; } } return oneWay; } }; window.plugin.dh_visits = { lastPortalUri: {uri:''}, droneVisited: {}, disabledMessage: null, contentHTML: null, isHighlightActive: false, setup: function() { this.visitStorageHandler = new window.plugin.dh_sync(this, 'dh_visits', 'droneVisited', 'merge', true, () => { if (window.selectedPortal) { this.updateCheckedAndHighlight(window.selectedPortal); } // and also update all highlights, if needed if (this.isHighlightActive) { resetHighlightedPortals(); } }); this.portalUriStorageHandler = new window.plugin.dh_sync(this, 'dh_visits', 'lastPortalUri', 'replace', false, () => { this.displayLastPortalLink(); }); window.addHook('portalDetailsUpdated', this.onPortalDetailsUpdated.bind(this)); window.addPortalHighlighter('Drone Visits', window.plugin.dh_visits.highlighter); window.addHook('portalSelected', this.onPortalSelected.bind(this)); this.visitStorageHandler.loadLocal(); this.portalUriStorageHandler.loadLocal(); this.contentHTML = '
' + '' + 'Total: ' + '
'; this.disabledMessage = '
Recording of Drone Visits Disabled
'; $('#dh-toolbox').append('Last Drone Visit '); this.displayLastPortalLink(); if (window.plugin.droneHelper.isSmart) { const status = document.getElementById('updatestatus'); const dStatus = document.createElement('div'); dStatus.className = 'DroneStatus'; status.insertBefore(dStatus, status.firstElementChild); } }, onPortalSelected() { if (window.plugin.droneHelper.isSmart) { document.querySelector('.DroneStatus').innerHTML = this.contentHTML; } }, updatePortalVisited(portalVisited, guid) { if(guid === undefined) guid = window.selectedPortal; if (this.droneVisited[guid] === undefined) this.droneVisited[guid] = {} if(portalVisited === this.droneVisited[guid].visited) return; this.droneVisited[guid].visited = portalVisited; this.droneVisited[guid].t = Date.now(); this.updateCheckedAndHighlight(guid); if (portalVisited) { this.lastPortalUri.uri = window.makePermalink(portals[guid]._latlng); this.displayLastPortalLink(); } this.sync(guid); }, sync(guid) { this.visitStorageHandler.save(); this.portalUriStorageHandler.save(); }, displayLastPortalLink() { $('#lastDroneVisit').attr('href', this.lastPortalUri.uri); }, onPortalDetailsUpdated() { if(typeof(Storage) === "undefined") { $('#dh-visitcount').html(this.disabledMessage); return; } let guid = window.selectedPortal, details = portalDetail.get(guid); $('#dh-visitcount').html(this.contentHTML); //$('#portaldetails > .imgpreview').after(this.contentHTML); this.updateCheckedAndHighlight(guid); }, updateCheckedAndHighlight(guid) { if (guid === window.selectedPortal) { let droneVisited = false; if (this.droneVisited[guid] !== undefined) { droneVisited = this.droneVisited[guid].visited || false; } $('.droneVisit').prop('checked', droneVisited); $('.droneTotal').text(this.numberVisited()); } if (this.isHighlightActive) { if (portals[guid]) { window.setMarkerStyle (portals[guid], guid === selectedPortal); } } }, numberVisited() { let count = 0; for (const [key,value] of Object.entries(this.droneVisited)) { count+= value.visited ? 1 : 0; } return count; }, highlighter: { styles: {visited: {fillColor: 'black', fillOpacity: 0.7}, unvisited: {fillColor: 'white', fillOpacity: 0.7}}, highlight: function(data) { let guid = data.portal.options.ent[0]; let droneVisit = window.plugin.dh_visits.droneVisited[guid] ? window.plugin.dh_visits.droneVisited[guid].visited : false; let style; if (droneVisit) { style = this.styles.visited; } else { style = this.styles.unvisited; } data.portal.setStyle(style); }, setSelected: function(active) { window.plugin.dh_visits.isHighlightActive = active; } } } window.plugin.dh_route = { currentRoute: {route:[]}, savedRoutes: {}, settings: { colourJumps: true, useLiveInventoryKeys: true, useKeysPluginKeys: true, keyboardShortcuts: { addPortal: 'd', showRoute: 'r' }, }, jumpColours: {tooLong: '#ff0000', needsKey: '#ff9900', usesOwnedKey: '#CDEF0D', normalJump: '#008306'}, setup() { this.currentRouteStorageHandler = new window.plugin.dh_sync(this, 'dh_route', 'currentRoute', 'replace', true, () => { $('#drone-jump-list').html(this.jumpListHtml()); this.drawRoute(); }); this.savedRoutesStorageHandler = new window.plugin.dh_sync(this, 'dh_route', 'savedRoutes', null, true, null); this.settingsStorageHandler = new window.plugin.dh_sync(this, 'dh_route', 'settings', null, false, null); this.layer = window.plugin.dh_utility.addNewLayerToIITC('Drone Route', 'dh_route'); window.addHook('portalDetailsUpdated', this.onPortalDetailsUpdated.bind(this)); window.addHook('dh_keysChanged', this.drawRoute.bind(this)); this.addKeyboardShortcuts(); this.currentRouteStorageHandler.loadLocal(); this.savedRoutesStorageHandler.loadLocal(); this.settingsStorageHandler.loadLocal(); this.drawRoute(); this.addRouteOptionsToDialog(); this.addRouteViewLink(); }, addRouteOptionsToDialog() { let html = '

Drone Route Options

' + '
' if (self.haveLiveInventoryPlugin) html += '
' if (self.haveKeysPlugin) html += '
' html += '
Keyboard shortcuts
' + '
' + '
' + '
'; $('#dialog-dh-options').append(html); }, addRouteViewLink() { $('#dh-toolbox').append('Show Routes'); }, updateKeyboardShortcut(name, value) { this.settings.keyboardShortcuts[name] = value; this.settingsStorageHandler.save(); }, toggleSettings(name) { this.settings[name] = !this.settings[name]; this.settingsStorageHandler.save(); if (name === 'colourJumps') this.currentRouteChanged(); if (name === 'useLiveInventoryKeys') this.currentRouteChanged(); if (name === 'useKeysPluginKeys') this.currentRouteChanged(); }, addKeyboardShortcuts() { //document.addEventListener,map.addEventListener,this.layer.addEventListener map.on('keyup', (e) => { if (e.originalEvent.key === this.settings.keyboardShortcuts.addPortal) { this.addToRoute(window.selectedPortal); } if (e.originalEvent.key === this.settings.keyboardShortcuts.showRoute) { this.showJumpList(); } }, false); }, onPortalDetailsUpdated() { if (this.disabled) return; let guid = window.selectedPortal; let html = 'Add to route'; $('#dh-portal-info .routeAdd').remove(); $('#dh-portal-info').append(html); }, addToRoute(guid) { if (this.disabled) return; if(guid === undefined) guid = window.selectedPortal; if(guid === undefined) return; let nextPortal = {guid:guid, _latlng: portals[guid]._latlng, label: portals[guid].options.data.title}; this.currentRoute.route.push(nextPortal); this.currentRouteChanged(); }, drawRoute() { this.layer.clearLayers(); if (this.currentRoute.route.length > 0) { L.marker(this.currentRoute.route[0]._latlng, { icon: L.divIcon.coloredSvg('#888'), draggable: false, title: 'Route start' }).addTo(this.layer); } if (this.currentRoute.route.length > 1) { L.marker(this.currentRoute.route[this.currentRoute.route.length - 1]._latlng, { icon: L.divIcon.coloredSvg('#888'), draggable: false, title: 'Route end' }).addTo(this.layer); } for(let i = 1; i < this.currentRoute.route.length; i++) { this.drawJump(this.currentRoute.route[i-1],this.currentRoute.route[i]); } }, drawJump(startPortal, endPortal) { let coords = [startPortal._latlng, endPortal._latlng]; let colour = "#888"; if (this.settings.colourJumps) { if (window.plugin.dh_utility.haversineDistance(startPortal._latlng, endPortal._latlng, window.plugin.dh_distance.earthRadius) > 1250) colour = this.jumpColours.tooLong; else if (window.plugin.dh_utility.isPortalVisible(startPortal._latlng, endPortal._latlng)) colour = this.jumpColours.normalJump; else if ((this.settings.useLiveInventoryKeys && window.plugin.dh_utility.getPortalHasKey(endPortal.guid, 'liveInventory')) || (this.settings.useKeysPluginKeys && window.plugin.dh_utility.getPortalHasKey(endPortal.guid, 'keysPlugin'))) { colour = this.jumpColours.usesOwnedKey; } else { colour = this.jumpColours.needsKey; } } L.polyline(coords, {color: colour, weight:3, opacity:1, dashArray:[20,6,15,6,10,6,5,6], clickable: false}).addTo(this.layer); }, showJumpList() { let html = '
'; html += ''; html +='
Portal Name
'; html += '
Total jumps: ' + Math.max(0,(this.currentRoute.route.length - 1)) + '
'; //html += '' html += '' html += '' html += '' html += '
'; dialog({ id:'drone-jump-box', html: html, title: 'Drone Jumps', position: {my: "left top", at: "left+10% top"} /*buttons: { 'Clear Route': function() { window.plugin.dh_route.clearRoute(); } }*/ }) $('#drone-jump-list').sortable({ axis:'y', update: this.changePortalOrder.bind(this) }); // add jump list after making dialog as soooo much faster!!! $('#drone-jump-list').html(this.jumpListHtml()); }, jumpListHtml() { let html = ''; if (this.currentRoute.route.length === 0) { html += 'No portals in route'; return html; } for (let i=0;i'+portalName+''; portalLink = ''+portalName+''; html +=''+portalLink+''; //html +='='; html +='X'; } return html; }, deletePortal(routeStep) { this.currentRoute.route.splice(routeStep,1); this.currentRouteChanged(); }, clearRoute() { let confirmClear = confirm('Current route will be deleted across all synced devices. Are you sure?'); if (confirmClear) { this.currentRoute.route = []; this.currentRouteChanged(); } }, clearSavedRoutes() { let confirmClear = confirm('This deletes all your saved routes. Are you sure?'); if (confirmClear) { this.savedRoutes = {}; this.routesChanged(); } }, saveRoute() { let routeName = prompt('Name for this route:', new Date().toLocaleString()); let routeId = window.plugin.dh_utility.uuidv4(); this.savedRoutes[routeId] = {name: routeName, route:this.currentRoute.route}; this.routesChanged(); }, loadRoute(id) { let confirmLoad = confirm('Current route will be replaced. Are you sure?'); if (confirmLoad) { this.currentRoute.route = this.savedRoutes[id].route; this.currentRouteChanged(); } }, manageRoutes() { let html = '
'; html += this.routeListHtml(); html += '
Route Name
'; html += '' html += '' html += '
'; dialog({ id:'drone-route-box', html: html, title: 'Drone Routes', /*buttons: { 'Clear Saved Routes': function() { window.plugin.dh_route.clearSavedRoutes(); } }*/ }) }, routeListHtml() { let html = ''; for (const id of Object.keys(this.savedRoutes)) { html +=''+this.savedRoutes[id].name+''; html +='Export JSON'; html +='X'; } return html; }, deleteRoute(id) { const confirmDelete = confirm('Delete route '+ this.savedRoutes[id].name +' - are you sure?'); if (confirmDelete) { delete this.savedRoutes[id]; this.routesChanged(); } }, saveJSON(id) { const routeName = this.savedRoutes[id].name + '.json'; const json = JSON.stringify({"id": id,name: this.savedRoutes[id].name, route: this.savedRoutes[id].route }); if (typeof window.saveFile != 'undefined') { window.saveFile(json, routeName, 'application/json'); } else { alert('cannot export route - browser compatibility issue'); } }, loadJSON() { if (typeof L.FileListLoader != 'undefined') { L.FileListLoader.loadFiles({accept: 'application/json'}) .on('load', e => { let route,guid; const data = JSON.parse(e.reader.result); if ('id' in data && 'name' in data && 'route' in data) { let valid = true; guid = data.id route = {name: data.name, route:[]}; for (const id of Object.keys(data.route)) { if ('guid' in data.route[id] && '_latlng' in data.route[id] && 'lat' in data.route[id]._latlng && 'lng' in data.route[id]._latlng) { route.route[id] = data.route[id]; } else valid = false; } if (!valid) return alert("Invalid route import"); } this.savedRoutes[window.plugin.dh_utility.uuidv4()] = route; this.routesChanged(); }); } }, // context for 'this' is provided by jQuery changePortalOrder(e, ui) { let newOrder = []; $('#drone-jump-list tr').each(function() { let id = $(this).attr('id').split('-')[2]; newOrder.push(window.plugin.dh_route.currentRoute.route[id]); }) window.plugin.dh_route.currentRoute.route = newOrder; window.plugin.dh_route.currentRouteChanged(); }, currentRouteChanged() { $('#drone-jump-list').html(this.jumpListHtml()); $('.jump-count').text('Total jumps: ' + Math.max(0,(this.currentRoute.route.length - 1))); this.drawRoute(); this.currentRouteStorageHandler.save(); }, routesChanged() { $('#drone-route-list').html(this.routeListHtml()); this.savedRoutesStorageHandler.save(); }, } window.plugin.dh_coverage = { default_cellColouring: { visible:{stroke:false, fillColor: '#00ffff', fillOpacity: 0.5, interactive: false}, reachable:{stroke:false, fillColor: '#ff0000', fillOpacity: 0.3, interactive: false}, visited:{stroke:false, fillColor: '#0ec18d', fillOpacity: 0.2, interactive: false}, outside:{stroke:false, fillColor: '#00ffff', fillOpacity: 0, interactive: false} }, settings: { extendByKeysPlugin: true, extendByLiveInventoryPlugin: true, assumeKeys: false, cellColouring: { visible:{stroke:false, fillColor: '#00ffff', fillOpacity: 0.5, interactive: false, labelText:'Cells where all portals are visible in drone view'}, reachable:{stroke:false, fillColor: '#ff0000', fillOpacity: 0.3, interactive: false, labelText:'Cells where all portals can be reached with multiple drone moves'}, visited:{stroke:false, fillColor: '#0ec18d', fillOpacity: 0.2, interactive: false, labelText:'Cells that can be reached and all portals have been visited'}, outside:{stroke:false, fillColor: '#00ffff', fillOpacity: 0, interactive: false} }, }, plotReachable: false, setup: function() { this.layer = window.plugin.dh_utility.addNewLayerToIITC('Drone Coverage', 'dh_coverage'); map.on('layeradd', (obj) => { if(obj.layer === this.layer) { $('.leaflet-control-droneHelper').show(); } }); map.on('layerremove', (obj) => { if(obj.layer === this.layer) { $('.leaflet-control-droneHelper').hide(); } }); window.addHook('mapDataRefreshEnd', this.mapDataRefreshEnd.bind(this)); window.addHook('pluginKeysUpdateKey', this.keyUpdate.bind(this)); window.addHook('pluginKeysRefreshAll', this.refreshAllKeys.bind(this)); this.settingsStorageHandler = new window.plugin.dh_sync(this, 'dh_coverage', 'settings', null, false, null); this.settingsStorageHandler.load(); this.addCoverageOptionsToDialog(); this.addLeafletControl(); if (!self.haveKeysPlugin) this.settings.extendByKeysPlugin = false; if (!self.haveLiveInventoryPlugin) this.settings.extendByLiveInventoryPlugin = false; this.assumeKeys = false; }, addCoverageOptionsToDialog() { let html = '

Drone Coverage Options

'; if (self.haveLiveInventoryPlugin) html += '
'; if (self.haveKeysPlugin) html += '
'; html += '
'; for (const cellType in this.settings.cellColouring) { if (cellType !== 'outside') html += '
'; } html += '
'; $('#dialog-dh-options').append(html); }, updateColour (name, colour) { this.settings.cellColouring[name.split('-')[1]].fillColor = colour; this.settingsStorageHandler.save(); this.drawReachable(); }, updateSettings(name, value) { this[name] = value; this.settingsStorageHandler.save(); }, toggleSettings(name) { this.settings[name] = !this.settings[name]; if (this.plotReachable) { if (name === 'extendByKeysPlugin' || name === 'extendByLiveInventoryPlugin' || name === 'assumeKeys') { if (!this.settings[name] && confirm('Restart coverage without keys (select OK) or leave any jumps relying on keys in the coverage?')) { this.startCoverage(this.currentLocation); } else { if (this.settings[name]) { this.updateReachable(portals); this.keyCheck(); this.drawReachable(); } } } } this.settingsStorageHandler.save(); }, addLeafletControl: function() { $('