// ==UserScript==
// @author DanielOnDiordna
// @name IITC plugin: Missions add-on
// @category Addon
// @version 1.1.1.20221027.234600
// @updateURL https://softspot.nl/ingress/plugins/iitc-plugin-missions-addon.meta.js
// @downloadURL https://softspot.nl/ingress/plugins/iitc-plugin-missions-addon.user.js
// @description [danielondiordna-1.1.1.20221027.234600] Add-on to add extra functionality for the missions plugin (up to version 0.3.0): 1: Optionally search for all missions within visible range, not just the top 25 (be aware: using this option will increase your server requests). 2: Sort the loaded missions by title (including roman numbers). 3: Show and remove stored missions. 4: Selected mission color changed to red. 5. Displayed missions show filled start portal. 6. Banner view for stored missions. 7. Transfer missions (export/import). 8. Missions routes layer can show a colorful path. 9. Redraw opened missions after IITC reloads.
// @id iitc-plugin-missions-addon@danielondiordna
// @namespace https://softspot.nl/ingress/
// @match https://intel.ingress.com/*
// @grant none
// ==/UserScript==
function wrapper(plugin_info) {
// ensure plugin framework is there, even if iitc is not yet loaded
if(typeof window.plugin !== 'function') window.plugin = function() {};
// use own namespace for plugin
window.plugin.missionsAddon = function() {};
var self = window.plugin.missionsAddon;
self.id = 'missionsAddon';
self.title = 'Missions add-on';
self.version = '1.1.1.20221027.234600';
self.author = 'DanielOnDiordna';
self.changelog = `
Changelog:
version 1.1.1.20221027.234600
- fixed an error with 'this' in the showMissionListDialog function
version 1.1.0.20221005.214400
- fixed a path redraw issue when opening an already opened mission again
- hardcode replace function showMissionListDialog because stock plugin Missions version 0.3.0 has too many (unneeded) changes
- this also the fixed the Bannergress plugin (which now needs the Add-on to work properly with stock plugin Missions version 0.3.0)
version 1.0.0.20221003.221800
- reversed the changelog order
- added changelog button on settings help screen
- fixed compatibility for stock Missions plugin version 0.3.0
- added auto list refresh on map move end for pre version 0.3.0
- added option to toggle auto refresh missions list when map is moved (default on from Missions plugin version 0.3.0)
- changed dialog ids to be compatible with stock Missions plugin version 0.2.2 and version 0.3.0
- changed setupShowMissionListDialog to be compatible with stock Missions plugin version 0.2.2 and version 0.3.0
- placed the eval functions inside try catch methods to prevent crashes and handle errors
- updated the help page to match the current list of settings
- added button to open all missions from banner view
- added zoom all missions from banner view
- added 'window' in front of global functions like L and dialog
- added curly brackets for readability
- open settings help in a separate window aligned to the right
- toggle colorful path option now redraws all opened missions instantly
version 0.2.5.20210724.002500
- prevent double plugin setup on hook iitcLoaded
version 0.2.5.20210527.203000
- fixed debuglayer retry bounds coloring
- added expert setting to choose the number of scan retry attempts (default 2)
version 0.2.4.20210526.002100
- changed debuglayer from LayerGroup to FeatureGroup to enable bringToBack
- changed mission debug layer colors (yellow: split area for more then 25 missions, purple: maximum 25 missions, green: less then 25 missions, orange: retry failed scan, red: failed scan)
- added scan dialog information about scans with 25 missions and failed scans
version 0.2.3.20210524.175700
- moved declaration of missiondebuglayer up, in case openTopMissions is called to soon
version 0.2.2.20210511.225000
- removed bug with settings menu create new mission button
version 0.2.1.20210501.230100
version 0.2.1.20210501.232800
version 0.2.1.20210501.233600
- added an option to keep missions always on top
- added an option to redraw opened missions after IITC reloads
- keep active mission on top
- on desktop open mission dialogs at the top left and stack them under eachother instead of at the screen center
version 0.2.0.20210501.111500
version 0.2.0.20210501.114200
- Missions routes layer can show a colorful path
version 0.1.10.20210430.001700
- added search method settings radio buttons and an option for default search
- fixed mission checkbox when clicking a mission title
version 0.1.9.20210426.234700
- improved bounds split for mission start portals
- hardcoded a minimum bounds size when splitting bounds areas
- changed some dialogs
version 0.1.8.20210422.000500
version 0.1.8.20210422.234900
- added advanced option to enable or disable a color for the active mission
- improved color setup for drawn missions (prepared for rainbow colors)
- debug layer fill when succesfull scan
- renamed Settings button to Add-on opt
- fixed endless loop on small scan areas when overlapping missions were found
- added hsl colors on debug layer to show results
- changed replace method of mission details to retain extra buttons
version 0.1.7.20210419.233500
- injected code for mission colors into functions highlightMissionLayers and drawMission
- added advanced option to use an alternative color for checked/completed missions
- added expert option to not clear mission results on rescan
version 0.1.6.20210418.231900
- fixed banneredit refresh and title
- improved missions transfer dialog
- added fix for loadmission stock function, to handle errors
version 0.1.5.20210421.190200
- minor fix for IITC CE where runHooks iitcLoaded is executed before addHook is defined in this plugin
version 0.1.5.20210416.210200
version 0.1.5.20210417.002900
- improved banner edit placeholder dialog
- added mobile side pane buttons for Stored missions and Add on Settings
- removed questionmark help buttons from settings, moved to separate help dialog, added help button on dialog titlebar
version 0.1.4.20210415.232500
- added banner editor title list to copy as placeholder
- added information for every setting in settings dialog
- added expert settings to enable a debug layer
- improved missions scanning, detect map moves
- improved dialogs if map moved
version 0.1.3.20210413.235700
version 0.1.3.20210414.070800
version 0.1.3.20210414.071600
version 0.1.3.20210415.000100
version 0.1.3.20210415.153800
version 0.1.3.20210415.161000
- fixed a bug on iOS IITC that stopped the plugin from working
- replaced named buttons by (unicode) symbols
- added a settings menu to toggle the advanced buttons
- added a settings to show classic text buttons
- added map load status in waiting dialog
- improved banner edit dialog renewing
- fixed a zoom level error with getMapZoomTileParameters
version 0.1.2.20210412.233200
- first raw version to export and import stored missions
version 0.1.1.20210412.204800
- added banner placeholders for missing missions
- added extra dialogs
- replace reload with rescan button
- improved missions finder, also find missions without start portals waypoints
version 0.1.0.20210412.001600
- improved dialogs
- added setting to always find more missions
- removed banner edit functionality
- stop loading when map is moved or zoomed
- do not reload when map is in same position
version 0.0.6.20210328.232800
version 0.0.6.20210329.231200
- minimized image download size to save on bandwidth
- display total bounds during mission reload
- added total banner missions time
- added total banner missions distance
version 0.0.5.20210327.231300
- improved mission finder system
- replaced X buttons by checkbox for missions in view
- added edit modus for banner view to remove missions
version 0.0.4.20210327.094100
version 0.0.4.20210327.102600
- improved mission title sorting mechanism, including sort by roman numbers
version 0.0.4.20210322.202300
- fix to replace http with https for image urls
version 0.0.4.20210321.211700
- added button to show earlier selected missions
- added button X to remove selected missions
- Selected mission color changed to red
- Displayed missions show filled start portal
- Banner view for selected missions
version 0.0.3.20210131.222200
- load all missions for all on screen mission start portals, not just the top 25
- store and show loaded missions until map reload or missions reload
- improved title sort for digits without trailing zeros
version 0.0.2.20210130.211900
- updated plugin wrapper and userscript header formatting to match IITC-CE coding
version 0.0.1.20191129.233100
- first release
`;
self.namespace = 'window.plugin.' + self.id + '.';
self.pluginname = 'plugin-' + self.id;
self.retrybounds = '';
self.retrycount = 0;
self.subboundstotal = 0;
self.subboundslist = [];
self.subboundscallback = undefined;
self.subboundserrorcallback = undefined;
self.subboundsloading = false;
self.missions = [];
self.missionsindex = {};
self.boundsloaded = '';
self.boundsloadedcomplete = false;
self.count25 = 0;
self.count25max = 0;
self.countretries = 0;
self.countfailed = 0;
self.countrequests = 0;
self.countmissions = 0;
self.localstoragesettings = self.pluginname + '-settings';
self.settings = {};
self.settings.showcreatenewbutton = true;
self.settings.showstoredbutton = false;
self.settings.showstoredicon = false;
self.settings.showbannerbutton = false;
self.settings.showtransferbutton = false;
self.settings.alwaysfindmore = false;
self.settings.activemissioncolor = true;
self.settings.colorcheckedmissions = false;
self.settings.showtextbuttons = true;
self.settings.enabledebuglayer = false;
self.settings.cleardebuglayeronscan = true;
self.settings.keepresultsonscan = false;
self.settings.colorfulpath = false;
self.settings.keepontop = false;
self.settings.redrawmissions = false;
self.settings.retrymax = 2;
self.settings.autorefreshonmoveend = false;
self.settings.searchmethoddefault = false;
self.settings.searchmethodstartportals = true;
self.localstorageplaceholdertitles = self.pluginname + '-placeholdertitles';
self.placeholdertitles = [];
self.localstoragedrawnmissions = self.pluginname + '-drawnmissions';
self.drawnmissions = [];
self.missiondebuglayer = undefined;
self.missiondebuglayertitle = "Missions DEBUG";
self.missiondebugobjects = {};
self.missionactiveguid = undefined;
self.restoresettings = function() {
if (typeof localStorage[self.localstoragesettings] != 'string' || localStorage[self.localstoragesettings] == '') return;
try {
var settings = JSON.parse(localStorage[self.localstoragesettings]);
if (typeof settings === 'object' && settings instanceof Object && !(settings instanceof Array)) { // expect an object
for (const i in self.settings) {
if (i in settings && typeof settings[i] === typeof self.settings[i]) { // only accept settings from default settings template of same type
if (typeof self.settings[i] === 'object' && self.settings[i] instanceof Object && !(self.settings[i] instanceof Array)) { // 1 sublevel is supported
for (const a in self.settings[i]) {
if (a in settings[i] && typeof settings[i][a] === typeof self.settings[i][a]) {
self.settings[i][a] = settings[i][a];
}
}
} else {
self.settings[i] = settings[i];
}
}
}
}
} catch(e) {
return false;
}
};
self.storesettings = function() {
localStorage[self.localstoragesettings] = JSON.stringify(self.settings);
};
self.restorePlaceholders = function() {
if (typeof localStorage[self.localstorageplaceholdertitles] != 'string' || localStorage[self.localstorageplaceholdertitles] == '') return;
try {
let placeholdertitles = JSON.parse(localStorage[self.localstorageplaceholdertitles]);
if (typeof placeholdertitles === 'object' && placeholdertitles instanceof Object && placeholdertitles instanceof Array) { // expect an array
for (let cnt = 0; cnt < placeholdertitles.length; cnt++) {
if (typeof placeholdertitles[cnt] == 'string' && placeholdertitles[cnt] != '' && self.placeholdertitles.indexOf(placeholdertitles[cnt]) < 0) {
self.placeholdertitles.push(placeholdertitles[cnt]);
}
}
}
} catch(e) {
return false;
}
};
self.storePlaceholders = function() {
localStorage[self.localstorageplaceholdertitles] = JSON.stringify(self.placeholdertitles);
};
self.restoredrawnmissions = function() {
if (typeof localStorage[self.localstoragedrawnmissions] != 'string' || localStorage[self.localstoragedrawnmissions] == '') return;
try {
let drawnmissions = JSON.parse(localStorage[self.localstoragedrawnmissions]);
if (typeof drawnmissions === 'object' && drawnmissions instanceof Object && drawnmissions instanceof Array) { // expect an array
for (let cnt = 0; cnt < drawnmissions.length; cnt++) {
if (typeof drawnmissions[cnt] == 'string' && drawnmissions[cnt] != '' && self.drawnmissions.indexOf(drawnmissions[cnt]) < 0) {
self.drawnmissions.push(drawnmissions[cnt]);
}
}
}
} catch(e) {
return false;
}
};
self.storedrawnmissions = function() {
localStorage[self.localstoragedrawnmissions] = JSON.stringify(self.drawnmissions);
};
self.clearmissions = function() {
if (!self.settings.keepresultsonscan) {
self.missions = [];
self.missionsindex = {};
}
self.boundsloaded = '';
self.boundsloadedcomplete = false;
};
self.appendmissions = function(missions) {
if (!missions || missions.length == 0) return 0;
let newcount = 0;
for (let cnt = 0; cnt < missions.length; cnt++) {
let missionguid = missions[cnt].guid;
if (self.missionsindex[missionguid] >= 0) {
self.missions[self.missionsindex[missionguid]] = missions[cnt];
} else {
newcount++;
self.missions.push(missions[cnt]);
self.missionsindex[missionguid] = self.missions.length - 1;
}
}
return newcount;
};
self.sortMissionPortalsByLng = function(missionportals) { // missionportals = [{guid:string,lat:float,lng:float,title:string}]
// sort the lng positions, this creates a list from west to east:
missionportals.sort(function(a, b) {
var x = a.lng;
var y = b.lng;
return x < y ? -1 : x > y ? 1 : 0;
});
return missionportals;
};
self.sortMissionPortalsByLat = function(missionportals) { // missionportals = [{guid:string,lat:float,lng:float,title:string}]
// sort the lat positions, this creates a list from north to south:
missionportals.sort(function(a, b) {
var x = a.lat;
var y = b.lat;
return x < y ? -1 : x > y ? 1 : 0;
});
return missionportals;
};
self.getMissionPortalsInBounds = function(bounds) {
if (!bounds || !(bounds instanceof window.L.LatLngBounds)) bounds = window.map.getBounds();
let missionportals = [];
for (let guid in window.portals) {
if (window.portals[guid].options.data.mission || window.portals[guid].options.data.mission50plus) {
let latlng = window.portals[guid].getLatLng();
if (bounds.contains(latlng)) {
missionportals.push({
guid: guid,
lat: latlng.lat,
lng: latlng.lng,
title: window.portals[guid].options.data.title
});
}
}
}
return missionportals;
};
self.zoomlevelHasPortals = function() {
return window.getMapZoomTileParameters(window.getDataZoomForMapZoom(window.map.getZoom())).hasPortals;
};
self.splitboundslist = function(bounds,maxmissionportals,horizontalsplit,splitlatlng) {
let subboundslist = [];
if (self.settings.searchmethodstartportals && self.zoomlevelHasPortals()) {
let missionportals = self.getMissionPortalsInBounds(bounds);
if (horizontalsplit) {
missionportals = self.sortMissionPortalsByLng(missionportals);
} else { // vertical split
missionportals = self.sortMissionPortalsByLat(missionportals);
}
// split bounds in blocks of mission start portals
for (let cnt = 0; cnt < missionportals.length;) {
let north = bounds.getNorth();
let south = bounds.getSouth();
let west = bounds.getWest();
let east = bounds.getEast();
// find next portal
let nextcnt = Math.min(cnt + maxmissionportals - 1, missionportals.length - 1);
if (splitlatlng != undefined) {
for (nextcnt = cnt; nextcnt < missionportals.length; nextcnt++) { // find portal with splitlatlng
if (horizontalsplit && missionportals[nextcnt].lat == splitlatlng || !horizontalsplit && missionportals[nextcnt].lng == splitlatlng) {
break;
}
}
}
// adjust bounds, between portals
if (horizontalsplit) {
let prevlng = cnt - 1;
while (prevlng > 0 && (missionportals[cnt].lng - missionportals[prevlng].lng) == 0) {
prevlng--;
}
let nextlng = nextcnt + 1;
while (nextlng < missionportals.length - 1 && (missionportals[nextlng].lng - missionportals[nextcnt].lng) == 0) {
nextcnt--;
}
if (cnt > 0) west = missionportals[cnt].lng - (missionportals[cnt].lng - missionportals[prevlng].lng) / 2;
if (nextcnt < missionportals.length - 1) east = missionportals[nextcnt].lng + (missionportals[nextlng].lng - missionportals[nextcnt].lng) / 2;
} else { // vertical split
let prevlat = cnt - 1;
while (prevlat > 0 && (missionportals[cnt].lat - missionportals[prevlat].lat) == 0) {
prevlat--;
}
let nextlat = nextcnt + 1;
while (nextlat < missionportals.length - 1 && (missionportals[nextlat].lat - missionportals[nextcnt].lat) == 0) {
nextcnt--;
}
if (cnt > 0) south = missionportals[cnt].lat - (missionportals[cnt].lat - missionportals[prevlat].lat) / 2;
if (nextcnt < missionportals.length - 1) north = missionportals[nextcnt].lat + (missionportals[nextlat].lat - missionportals[nextcnt].lat) / 2;
}
let subbounds = new window.L.LatLngBounds([south,west],[north,east]);
let subportals = self.getMissionPortalsInBounds(subbounds);
let splitneeded = false;
if (horizontalsplit) {
subportals = self.sortMissionPortalsByLng(subportals);
for (let cnt = 1; cnt < subportals.length - 1; cnt++) {
if (subportals[cnt - 1].lng == subportals[cnt].lng) {
splitneeded = true;
subboundslist = subboundslist.concat(self.splitboundslist(subbounds,1,false,subportals[cnt].lng)); // split vertical!
break;
}
}
} else {
subportals = self.sortMissionPortalsByLat(subportals);
for (let cnt = 1; cnt < subportals.length - 1; cnt++) {
if (subportals[cnt - 1].lat == subportals[cnt].lat) {
splitneeded = true;
subboundslist = subboundslist.concat(self.splitboundslist(subbounds,1,true,subportals[cnt].lat)); // split horizontal!
break;
}
}
}
if (!splitneeded) {
let missionportalcount = subportals.length;
subboundslist.push({bounds:subbounds,missionportalcount:missionportalcount,missionportals:subportals});
}
cnt += maxmissionportals;
if (splitlatlng != undefined) {
cnt = nextcnt + 1;
}
}
} else {
let south = bounds.getNorth();
let north = bounds.getSouth();
let west = bounds.getWest();
let east = bounds.getEast();
let width = east - west;
let height = south - north;
if (width * height > 4.2558632880323265e-9) { // minimum split size
if (width > height) { // split horizontal
subboundslist.push({bounds:new window.L.LatLngBounds([south,west],[north,west + width / 2])});
subboundslist.push({bounds:new window.L.LatLngBounds([south,west + width / 2],[north,east])});
} else { // split vertical
subboundslist.push({bounds:new window.L.LatLngBounds([north + height / 2,west],[north,east])});
subboundslist.push({bounds:new window.L.LatLngBounds([south,west],[north + height / 2,east])});
}
} else {
self.count25max++;
self.updateProgressDialog();
self.setdebuglayercolor(bounds,{color: '#c000c0', fill: true, fillColor: '#c000c0', fillOpacity: 0.5}); // MAX: purple fill
console.log('splitboundslist minimum bounds reached',bounds,width * height);
}
}
self.subboundstotal += subboundslist.length;
return subboundslist;
};
self.decodeWaypoint = function(data) { // litteral copy from Missions plugin
var result = {
hidden: data[0],
guid: data[1],
title: data[2],
typeNum: data[3],
type: [null, 'Portal', 'Field Trip'][data[3]],
objectiveNum: data[4],
objective: [null, 'Hack this Portal', 'Capture or Upgrade Portal', 'Create Link from Portal', 'Create Field from Portal', 'Install a Mod on this Portal', 'Take a Photo', 'View this Field Trip Waypoint', 'Enter the Passphrase'][data[4]],
};
if (result.typeNum === 1 && data[5]) {
result.portal = window.decodeArray.portalSummary(data[5]);
// Portal waypoints have the same guid as the respective portal.
result.portal.guid = result.guid;
}
return result;
};
self.decodeMission = function(data) { // copy from Missions plugin, with fix for empty result
if (!data) return null; // fix for empty result
return {
guid: data[0],
title: data[1],
description: data[2],
authorNickname: data[3],
authorTeam: data[4],
// Notice: this format is weird(100%: 1.000.000)
ratingE6: data[5],
medianCompletionTimeMs: data[6],
numUniqueCompletedPlayers: data[7],
typeNum: data[8],
type: [null, 'Sequential', 'Non Sequential', 'Hidden'][data[8]],
waypoints: data[9].map(self.decodeWaypoint),
image: data[10]
};
};
self.decodeMissionSummary = function(data) { // copy from Missions plugin, with fix for http images, and resize to default 50x50
return {
guid: data[0],
title: data[1],
image: data[2].replace("http:","https:").replace(/=[swh]\d+(|\-[cp])$/,'') + '=s50-c', // fix invalid urls, download max 50x50 image,
ratingE6: data[3],
medianCompletionTimeMs: data[4]
};
};
self.loadmapmissions = function() {
self.updateProgressDialog();
if (!self.subboundsloading || self.subboundslist.length == 0) {
self.subboundsloading = false;
self.subboundslist = [];
console.log('Total requests: ' + self.countrequests + ' (split at 25 missions: ' + self.count25 + ', stopped at maximum 25 missions: ' + self.count25max + ', retried scans: ' + self.countretries + ', failed scans: ' + self.countfailed + ') Missions found: ' + self.countmissions);
if (typeof self.subboundscallback == "function") {
let missions = self.getMissions();
self.subboundscallback(missions);
self.subboundscallback = undefined;
} else {
console.log('no subboundscallback function');
}
return;
}
let subbounds = self.subboundslist.shift();
if (self.subboundslist.length == 0) self.boundsloadedcomplete = true;
let bounds = subbounds.bounds;
self.drawdebuglayer(bounds);
self.countrequests++;
window.postAjax('getTopMissionsInBounds', { // returns all (top 25) missions with mission portals starting in - or has mission portals passing through - this region
northE6: ((bounds.getNorth() * 1E6) | 0),
southE6: ((bounds.getSouth() * 1E6) | 0),
westE6: ((bounds.getWest() * 1E6) | 0),
eastE6: ((bounds.getEast() * 1E6) | 0)
}, function(data) {
var missions = data.result.map(self.decodeMissionSummary);
//console.log('loadmapmissions - debug',data,missions);
if (!missions) {
console.log('loadmapmissions - warning: no valid mission data found',data);
} else {
self.setdebuglayercolor(bounds,{color: '#ffff00', fill: false}); // SPLIT: yellow border (reset color after possible retry attempt)
let newmissionscount = self.appendmissions(missions);
if (missions.length == 25) { // there are 25 missions found, this could be exactly the total number of missions, or it is capped of at 25
self.count25++;
self.updateProgressDialog();
if (self.settings.searchmethodstartportals && self.zoomlevelHasPortals()) {
let missionportalcount = subbounds.missionportalcount;
if (missionportalcount <= 1) { // stop!
self.count25max++;
self.updateProgressDialog();
self.setdebuglayercolor(bounds,{color: '#c000c0', fill: true, fillColor: '#c000c0', fillOpacity: 0.5}); // MAX: purple fill
console.log('loadmapmissions - maximum of 25 missions found on 1 portal',bounds);
} else { // split bounds and continue
self.setdebuglayercolor(bounds,{color: '#ffff00', fill: false}); // SPLIT: yellow border
//console.log('loadmapmissions - maximum of 25 found on ' + missionportalcount + ' portals - requeuing into 2 smaller areas to find all missions',bounds);
let maxmissionportals = Math.min(15, Math.ceil(missionportalcount / 2)); // min:25 higher chance of getting max missions, min:20 better, min:15 even better, min:10 less good
[].unshift.apply(self.subboundslist, self.splitboundslist(bounds,maxmissionportals,true));
}
} else {
// split bounds in 2 and continue:
self.setdebuglayercolor(bounds,{color: '#ffff00', fill: false}); // SPLIT: yellow border
// console.log('loadmapmissions - maximum of 25 found - requeuing into 2 smaller areas to find all missions',bounds);
[].unshift.apply(self.subboundslist, self.splitboundslist(bounds));
}
} else {
self.countmissions += missions.length;
// less then 25 missions found, continue
self.setdebuglayercolor(bounds,{color: '#008000', fill: true, fillColor: '#008000'}); // OKAY: green fill
}
}
setTimeout(self.loadmapmissions,10);
}, function(error) {
subbounds.retrycount = (subbounds.retrycount || 0) + 1;
if (subbounds.retrycount <= self.settings.retrymax) {
self.countretries++;
self.setdebuglayercolor(bounds,{color: '#ff690f', fill: true, fillColor: '#ff690f'}); // RETRY: orange fill
console.log('loadmapmissions - error loading map missions, retry (' + subbounds.retrycount + '/' + self.settings.retrymax + ')',bounds,arguments);
self.subboundslist.unshift(subbounds); // retry same bounds
setTimeout(self.loadmapmissions,50);
} else {
self.countfailed++;
self.updateProgressDialog();
self.setdebuglayercolor(bounds,{color: '#FF0000', fill: true, fillColor: '#FF0000', fillOpacity: 0.8}); // FAIL: red fill
console.warn('loadmapmissions - permanently failed loading map missions for bounds:',bounds,arguments);
setTimeout(self.loadmapmissions,10);
}
});
};
self.updateProgressDialog = function() {
$('#boundsLength').text(self.subboundslist.length + ' (total: ' + self.subboundstotal + ')');
$('#count25').text(self.count25);
$('#count25max').text(self.count25max);
$('#countfailed').text(self.countfailed);
$('#missionsFound').text(self.missions.length);
};
self.showprogressDialog = function() {
let container = document.createElement('div');
container.appendChild(document.createTextNode('Areas to go: '));
let boundsLength = container.appendChild(document.createElement('span'));
boundsLength.id = 'boundsLength';
boundsLength.textContent = self.subboundslist.length + ' (total: ' + self.subboundstotal + ')';
container.appendChild(document.createElement('br'));
container.appendChild(document.createTextNode('Areas with 25 missions that will be split up: '));
let count25 = container.appendChild(document.createElement('span'));
count25.id = 'count25';
count25.textContent = self.count25;
container.appendChild(document.createElement('br'));
container.appendChild(document.createTextNode('Areas with max 25 missions: '));
let count25max = container.appendChild(document.createElement('span'));
count25max.id = 'count25max';
count25max.textContent = self.count25max;
container.appendChild(document.createElement('br'));
container.appendChild(document.createTextNode('Permanently failed scans: '));
let countfailed = container.appendChild(document.createElement('span'));
countfailed.id = 'countfailed';
countfailed.textContent = self.countfailed;
container.appendChild(document.createElement('br'));
container.appendChild(document.createTextNode('Missions found: '));
let missionsFound = container.appendChild(document.createElement('span'));
missionsFound.id = 'missionsFound';
missionsFound.textContent = self.missions.length;
container.appendChild(document.createElement('br'));
container.appendChild(document.createElement('br'));
container.appendChild(document.createTextNode('Please wait...'));
container.appendChild(document.createElement('br'));
let stopbutton = container.appendChild(document.createElement('button'));
stopbutton.textContent = 'Stop';
stopbutton.addEventListener('click', function(e) {
e.preventDefault();
stopbutton.disabled = true;
stopbutton.textContent = 'Stopped';
self.stoploading();
}, false);
let alwayscheckboxarea = container.appendChild(document.createElement('label'));
let alwayscheckbox = alwayscheckboxarea.appendChild(document.createElement('input'));
alwayscheckbox.type = 'checkbox';
alwayscheckbox.checked = self.settings.alwaysfindmore;
alwayscheckboxarea.appendChild(document.createTextNode('Always automatically try to find more'));
alwayscheckbox.addEventListener('change', function(e) {
e.preventDefault();
self.settings.alwaysfindmore = this.checked;
self.storesettings();
},false);
let buttons = {};
buttons[(self.settings.showtextbuttons?'Close':'✖')] = function() { $(this).dialog('close'); };
window.dialog({
id: "missionsList",
title: 'Missions in view',
height: 'auto',
html: container,
width: '400px',
closeCallback: function() {
if (self.subboundslist.length != 0) {
self.stoploading();
self.subboundscallback = undefined;
}
}
}).dialog('option', 'buttons', buttons);
};
self.stoploading = function() {
self.subboundsloading = false;
//self.subboundslist = [];
};
self.removehooks = function() {
// remove hooks
if (window._hooks && window._hooks.mapDataRefreshEnd && window._hooks.mapDataRefreshEnd.length > 0) {
for (let cnt = 0; cnt < window._hooks.mapDataRefreshEnd.length; cnt++) {
if (window._hooks.mapDataRefreshEnd[cnt].toString().match(/loadAllMissionsInBounds/)) {
window.removeHook('mapDataRefreshEnd',window._hooks.mapDataRefreshEnd[cnt]);
break;
}
}
}
if (window._hooks && window._hooks.requestFinished && window._hooks.requestFinished.length > 0) {
for (let cnt = 0; cnt < window._hooks.requestFinished.length; cnt++) {
if (window._hooks.requestFinished[cnt].toString().match(/mapDataRequest/)) {
window.removeHook('requestFinished',window._hooks.requestFinished[cnt]);
break;
}
}
}
};
self.loadAllMissionsInBounds = function(bounds, callback, errorcallback) {
if (self.subboundsloading || self.subboundslist.length > 0) { // already running
self.showprogressDialog();
return;
}
self.removehooks();
let status = window.mapDataRequest.getStatus();
if (self.settings.searchmethodstartportals && self.zoomlevelHasPortals() && status.short && status.short != 'done') {
let container = document.createElement('div');
container.className = self.id + 'mainmenu';
container.textContent = 'Waiting until map loading status is "done"...';
let statusarea = container.appendChild(document.createElement('div'));
let missionstartportalsarea = container.appendChild(document.createElement('div'));
let buttons = {};
buttons[(self.settings.showtextbuttons?'In view':'🔎')] = function() { window.plugin.missions.showMissionListDialog(self.getMissions()); };
buttons[(self.settings.showtextbuttons?'Close':'✖')] = function() { $(this).dialog('close'); };
window.dialog({
id: "missionsList",
title: 'Missions in view - Wait',
height: 'auto',
html: container,
width: '400px',
closeCallback: function() {
if (window.mapDataRequest.status.short != 'done') {
self.removehooks();
}
}
}).dialog('option', 'buttons', buttons);
// add hook to update map loading status
window.addHook('requestFinished',function() {
if (!statusarea) return;
let status = window.mapDataRequest.getStatus();
statusarea.textContent = 'map: ' + status.short + (status.progress !== undefined && status.progress !== -1 ? ' ' + Math.floor(status.progress*100)+'%':'');
missionstartportalsarea.textContent = 'mission start portals found: ' + self.getMissionPortalsInBounds(bounds).length;
});
// add hook to wait for map loading done
window.addHook('mapDataRefreshEnd',function() { setTimeout(function() { self.loadAllMissionsInBounds(bounds, callback, errorcallback); },0); }); // timeout needed to wait for status to be updated to done (setStatus is executed after the hook is fired)
return;
}
if (window.map.getBounds().toBBoxString() != bounds.toBBoxString()) { // map was moved during this dialog!
console.log('map was moved during this dialog!');
bounds = window.map.getBounds();
}
let maxmissionportals = 15; // max 25 gives a high chance of getting max 25 missions; After some testing: 20 better, 15 even better, 10 less good
self.subboundslist = self.splitboundslist(bounds,maxmissionportals,true);
self.subboundstotal = self.subboundslist.length;
self.subboundscallback = callback;
self.subboundserrorcallback = errorcallback;
self.showprogressDialog();
self.subboundsloading = true;
setTimeout(self.loadmapmissions,0);
};
self.confirmFindmore = function(missions,bounds,callback,errorcallback) {
if (self.settings.alwaysfindmore) {
self.loadAllMissionsInBounds(bounds,callback,errorcallback);
return;
}
let container = document.createElement('div');
container.className = self.id + 'mainmenu';
container.appendChild(document.createTextNode('Default maximum of 25 missions found.'));
container.appendChild(document.createElement('br'));
container.appendChild(document.createTextNode('There are probably more in view.'));
container.appendChild(document.createElement('br'));
container.appendChild(document.createElement('br'));
container.appendChild(document.createTextNode('Try to find more?'));
container.appendChild(document.createElement('br'));
container.appendChild(document.createTextNode('(be aware that this function will increase your server requests)'));
container.appendChild(document.createElement('br'));
if (!self.zoomlevelHasPortals()) {
container.appendChild(document.createTextNode('Zoom in to show all portals to find more missions.'));
container.appendChild(document.createElement('br'));
}
let buttonsarea = container.appendChild(document.createElement('div'));
let nobutton = buttonsarea.appendChild(document.createElement('button'));
nobutton.textContent = 'No';
nobutton.addEventListener('click', function(e) {
e.preventDefault();
window.plugin.missions.showMissionListDialog(self.getMissions());
}, false);
let okaybutton = buttonsarea.appendChild(document.createElement('button'));
okaybutton.textContent = 'Okay';
okaybutton.addEventListener('click', function(e) {
e.preventDefault();
if (window.map.getBounds().toBBoxString() != bounds.toBBoxString()) { // map was moved during this dialog!
let restartarea = document.createElement('div');
let restartbutton = restartarea.appendChild(document.createElement('button'));
restartbutton.textContent = 'Restart';
restartarea.appendChild(document.createTextNode(' Map was moved during this dialog!'));
restartarea.appendChild(document.createElement('br'));
restartarea.appendChild(document.createTextNode('Scan needs to restart.'));
restartbutton.addEventListener('click', function(e) {
e.preventDefault();
window.plugin.missions.openTopMissions(window.map.getBounds());
}, false);
buttonsarea.replaceWith(restartarea);
} else {
self.loadAllMissionsInBounds(bounds,callback,errorcallback);
}
}, false);
let alwayscheckboxarea = buttonsarea.appendChild(document.createElement('label'));
let alwayscheckbox = alwayscheckboxarea.appendChild(document.createElement('input'));
alwayscheckbox.type = 'checkbox';
alwayscheckbox.checked = self.settings.alwaysfindmore;
alwayscheckboxarea.appendChild(document.createTextNode('Always try to automatically find more'));
alwayscheckbox.addEventListener('change', function(e) {
e.preventDefault();
self.settings.alwaysfindmore = this.checked;
self.storesettings();
},false);
let buttons = self.getdialogbuttons();
window.dialog({
id: "missionsList",
title: 'Missions in view - Scan',
height: 'auto',
html: container,
width: '400px'
}).dialog('option', 'buttons', buttons);
};
self.createsortablemissiontitle = function(missiontitle) {
function deromanize(str) {
str = str.toUpperCase();
let validator = /^M*(?:D?C{0,3}|C[MD])(?:L?X{0,3}|X[CL])(?:V?I{0,3}|I[XV])$/;
if (!(str && validator.test(str))) {
return str;
}
let token = /[MDLV]|C[MD]?|X[CL]?|I[XV]?/g;
let key = {M:1000,CM:900,D:500,CD:400,C:100,XC:90,L:50,XL:40,X:10,IX:9,V:5,IV:4,I:1};
let num = 0, m;
while (m = token.exec(str)) {
num += key[m[0]];
}
return num;
}
let sortabletitle = missiontitle.toLowerCase();
sortabletitle = sortabletitle.replace(/([^a-z])([MDCLXVI]+)([^a-z])/gi,function(full,one,two,three,pos) {
return one + deromanize(two) + three;
}).replace(/([^a-z])([MDCLXVI]+)$/gi,function(full,one,two,pos) {
return one + deromanize(two);
}).replace(/^([MDCLXVI]+)([^a-z])/gi,function(full,one,two,pos) {
return deromanize(one) + two;
});
sortabletitle = sortabletitle.replace(/\d+/g, function(num) {
return "0000".substr(num.length) + num;
});
sortabletitle = sortabletitle.replace(/^([0-9\s\-\/\\\.\[\]\(\)#<>:!,;\{\}'"]+)([^0-9\s\-\/\\\.\[\]\(\)#<>:!,;\{\}'"]+)/,'$2$1');
return sortabletitle;
};
self.sortmissions = function(missions) {
// sort the list by mission title (case insensitive, smart sort numbers, sort roman numbers):
let sortable = [];
missions.forEach(function(mission) {
sortable.push({sortabletitle:self.createsortablemissiontitle(mission.title), mission:mission});
}, this);
sortable.sort(function(a, b) {
var x = a.sortabletitle;
var y = b.sortabletitle;
return x < y ? -1 : x > y ? 1 : 0;
});
let sortedmissions = [];
for (let cnt = 0; cnt < sortable.length; cnt++) {
sortedmissions.push(sortable[cnt].mission);
}
return sortedmissions;
};
self.bannerEdit = function(oldcontainer) {
let missions = self.getMissions(true);
let bannermissions = [];
for (let cnt = 0; cnt < missions.length; cnt++) {
bannermissions.push(missions[cnt]);
bannermissions[cnt].index = cnt;
}
for (let cnt = 0; cnt < self.placeholdertitles.length; cnt++) {
bannermissions.push({
placeholdertitle: true,
title: self.placeholdertitles[cnt],
image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAAAAAA7VNdtAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQflBAwOJxIW6dfOAAAC4klEQVRIx43VW08bRxQH8P85M3vDC8TGCBbiNk1pkjZJJVLlIlUqqdRP0Y+ZfoE+9AYoiNAqihBNGqKEVhBfkMFe786cPnCz2RWeeZmHOT/9NdI5M/RP7WerBI6Le41veKP1HVlyFf2FBxus1w+fwtFwP3myvssUrHUcDfeTx6t7EQtcDfeTx6sfQstwNScZoQXD0ZwLMNzMhQDDyQyJEzLWDItTMsaMiDNypRkV5+TMGBonLghEgrXOUyqYy2KIACgzBTFCpMQUxWhK0ZSIUVK4T5m4RIARQ70SUSBAsNb53rMEgLJGiQADRAQCAAJAgmCtdU9AIDN1Z/3f0NJJCYhO92dkrGghghgNZEpE930SIqMwYChYJUbEFwMlBh40ZZ9+1vnzVus/qi1tGNydl532E70TdQ+Wt8JHE83N6cbW1H0+eDl3m19Wr+9tE5OtdqJGJRK7mNR7X84/fx2H4U6rFnJNP+z8Ed2nGRNE29c/9/V2d/bDfD1nUD4XtAg2nnzxCdc3m7ubCL6opl99a6tmq7txLTBE2dutOancirMbWZcZoj7+9tHL+rMzC7OVtJarKnd/fR/8vYnUj9OqGAwENJ2rg1+aQPtQqx/zxeP3Ormhk3dvouj1cn1psrk0O5g83L/5Kvx6Zumv9HbtuLE4/8K7We/W3yx2j+gnW8ER4nhg29lE5SBO0j0d62PKB1OHg1p9vxVd43bstY8mpnXT6wf2iJ6RAcMahiJrtcnJT6vBPhEZDmZ2ERgj2ohS1oi2LGCGKAUoX2sIaeEgTBdWfCiCpjT5wcvZ88nzWcjzSZNiMCACiIgAEIB6yaPVt1oAAf3eWSEDwcmhQCDFHjt9d09/j7BsTrlMnHdi6WzzVaLc8JWi1PDVoszwGFFieJwoGh4rCobHi8uGHcQlwy5i1LCTGDHsJoYNO4ohw67iwjD3F9zEueGeY8aZWWFaXV53FQAofVij7Pm7yFkAyB/8D26O+q45FJqpAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTA0LTEyVDE0OjM4OjQzKzAwOjAwkPtYggAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wNC0xMlQxNDozODo0MyswMDowMOGm4D4AAAAASUVORK5CYII='
});
}
bannermissions = self.sortmissions(bannermissions);
let container = document.createElement('div');
container.textContent = 'Mission and placeholder titles:';
let titlesarea = container.appendChild(document.createElement('textarea'));
titlesarea.readOnly = true;
titlesarea.textContent = bannermissions.map(function(mission) { return mission.title + (mission.placeholdertitle ? ' [p]':''); }).join('\n');
titlesarea.style.width = '96%';
titlesarea.style.height = '75px';
titlesarea.style.resize = 'vertical';
titlesarea.style.display = 'block';
let addplaceholderbutton = container.appendChild(document.createElement('button'));
addplaceholderbutton.textContent = 'Add placeholder title';
addplaceholderbutton.title = 'You can add a placeholder title to fill up the banner for a missing mission (not found/stored yet).\nPlaceholder titles are automatically removed when you download and store the mission later on.';
addplaceholderbutton.id = self.id + 'placeholderbutton';
addplaceholderbutton.lasttitle = '';
let oldplaceholderbutton = document.getElementById(self.id + 'placeholderbutton');
if (oldplaceholderbutton != null) addplaceholderbutton.lasttitle = oldplaceholderbutton.lasttitle;
addplaceholderbutton.addEventListener('click', function(e) {
e.preventDefault();
let suggestion = (addplaceholderbutton.lasttitle? addplaceholderbutton.lasttitle : (bannermissions && bannermissions.length > 0?bannermissions[0].title:'') );
if (titlesarea.selectionStart !== undefined && titlesarea.selectionStart != titlesarea.selectionEnd) {
suggestion = titlesarea.value.substring(titlesarea.selectionStart, titlesarea.selectionEnd).replace(/\n.*$/s,'');
}
let placeholdertitle = prompt('Enter a (missing) mission title to use as placeholder:',suggestion);
if (typeof placeholdertitle == 'string') {
placeholdertitle = placeholdertitle.trim();
if (placeholdertitle != '' && self.placeholdertitles.map(function(title) { return title.toUpperCase(); }).indexOf(placeholdertitle.toUpperCase()) >= 0) {
addplaceholderbutton.lasttitle = placeholdertitle;
placeholdertitle = '';
alert('Placeholder title already exists');
}
for (let cnt = 0; cnt < missions.length && placeholdertitle != ""; cnt++) {
if (missions[cnt].title.toUpperCase() == placeholdertitle.toUpperCase()) {
addplaceholderbutton.lasttitle = placeholdertitle;
placeholdertitle = '';
alert('Placeholder title already exists as a stored mission');
}
}
if (placeholdertitle != '') {
addplaceholderbutton.lasttitle = placeholdertitle;
self.placeholdertitles.push(placeholdertitle);
self.storePlaceholders();
self.bannerEdit(container);
}
}
}, false);
container.appendChild(document.createElement('br'));
let table = container.appendChild(document.createElement('table'));
table.className = self.id + '-banner-summary';
bannermissions = self.sortmissions(bannermissions).reverse(); // reverse sort, switched a and b
for (let rowcnt = 0; rowcnt < bannermissions.length; rowcnt += 6) {
var row = table.appendChild(document.createElement('tr'));
for (let cellcnt = 0; cellcnt < 6 && rowcnt + cellcnt < bannermissions.length; cellcnt++) {
let mission = bannermissions[rowcnt + cellcnt];
let cell = row.appendChild(document.createElement('td'));
cell.style.position = 'relative';
let img = cell.appendChild(document.createElement('img'));
if (mission.placeholdertitle) {
img.src = mission.image;
} else {
img.src = mission.image.replace('http:','https:').replace(/=[swh]\d+(|\-[cp])$/,'') + '=s50-c'; // fix for http urls, download max 50x50 image
}
img.title = (mission.placeholdertitle?'Placeholder: ':'') + mission.title;
let txt = cell.appendChild(document.createElement('span'));
txt.className = self.id + '-banner-edit-buttons';
let deleteButton = txt.appendChild(document.createElement('span'));
deleteButton.textContent = 'X';
deleteButton.title = img.title;
deleteButton.addEventListener('click', function() {
if (mission.placeholdertitle) {
self.placeholdertitles.splice(self.placeholdertitles.indexOf(mission.title),1);
self.storePlaceholders();
} else {
self.deleteCachedListItem(mission.guid);
}
self.bannerEdit(container);
}, false);
}
}
if (typeof oldcontainer == 'object' && oldcontainer instanceof HTMLElement) {
oldcontainer.replaceWith(container);
document.getElementById('dialog-missionsList').parentElement.getElementsByClassName('ui-dialog-title')[0].textContent = 'Stored Missions Banner Edit - ' + missions.length;
return;
}
let buttons = self.getdialogbuttons();
window.dialog({
title: 'Stored Missions Banner Edit - ' + missions.length,
html: container,
id: "missionsList",
minWidth: 360,
height: 'auto'
}).dialog('option', 'buttons', buttons);
};
self.bannerDisplay = function(missions) {
if (!missions) missions = self.getMissions(true);
function msToTime(s) {
// Pad to 2 or 3 digits, default is 2
function pad(n, z) {
z = z || 2;
return ('00' + n).slice(-z);
}
var ms = s % 1000;
s = (s - ms) / 1000;
var secs = s % 60;
s = (s - secs) / 60;
var mins = s % 60;
var hrs = (s - mins) / 60;
return hrs + ':' + pad(mins) + ':' + pad(secs) + '.' + pad(ms, 3);
}
function distance(len) {
if (len > 0) {
if (len > 1000) {
len = Math.round(len / 100) / 10 + 'km';
} else {
len = Math.round(len * 10) / 10 + 'm';
}
} else {
len = 'unknown';
}
return len;
}
let container = document.createElement('div');
let table = container.appendChild(document.createElement('table'));
table.className = self.id + '-banner-summary';
let medianCompletionTimeMs_total = 0;
let waypoints_length = 0;
let bannermissions = [];
let missiontitles = [];
for (let cnt = 0; cnt < missions.length; cnt++) {
let mission = missions[cnt];
bannermissions.push(mission);
missiontitles.push(mission.title);
if (mission.medianCompletionTimeMs) medianCompletionTimeMs_total += mission.medianCompletionTimeMs;
if (mission.waypoints) {
var len = mission.waypoints.filter(function(waypoint) {
return !!waypoint.portal;
}).map(function(waypoint) {
return window.L.latLng(waypoint.portal.latE6/1E6, waypoint.portal.lngE6/1E6);
}).map(function(latlng1, i, latlngs) {
if(i == 0) return 0;
var latlng2 = latlngs[i - 1];
return latlng1.distanceTo(latlng2);
}).reduce(function(a, b) {
return a + b;
}, 0);
waypoints_length += len;
missions[cnt].waypoints_length = len;
}
}
for (let cnt = self.placeholdertitles.length - 1; cnt >= 0; cnt--) {
if (missiontitles.indexOf(self.placeholdertitles[cnt]) >= 0) {
self.placeholdertitles.splice(cnt,1); // remove placeholdertitles if mission with same name is stored
self.storePlaceholders();
} else {
bannermissions.push({
placeholdertitle: true,
title: self.placeholdertitles[cnt],
image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAAAAAA7VNdtAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQflBAwOJxIW6dfOAAAC4klEQVRIx43VW08bRxQH8P85M3vDC8TGCBbiNk1pkjZJJVLlIlUqqdRP0Y+ZfoE+9AYoiNAqihBNGqKEVhBfkMFe786cPnCz2RWeeZmHOT/9NdI5M/RP7WerBI6Le41veKP1HVlyFf2FBxus1w+fwtFwP3myvssUrHUcDfeTx6t7EQtcDfeTx6sfQstwNScZoQXD0ZwLMNzMhQDDyQyJEzLWDItTMsaMiDNypRkV5+TMGBonLghEgrXOUyqYy2KIACgzBTFCpMQUxWhK0ZSIUVK4T5m4RIARQ70SUSBAsNb53rMEgLJGiQADRAQCAAJAgmCtdU9AIDN1Z/3f0NJJCYhO92dkrGghghgNZEpE930SIqMwYChYJUbEFwMlBh40ZZ9+1vnzVus/qi1tGNydl532E70TdQ+Wt8JHE83N6cbW1H0+eDl3m19Wr+9tE5OtdqJGJRK7mNR7X84/fx2H4U6rFnJNP+z8Ed2nGRNE29c/9/V2d/bDfD1nUD4XtAg2nnzxCdc3m7ubCL6opl99a6tmq7txLTBE2dutOancirMbWZcZoj7+9tHL+rMzC7OVtJarKnd/fR/8vYnUj9OqGAwENJ2rg1+aQPtQqx/zxeP3Ormhk3dvouj1cn1psrk0O5g83L/5Kvx6Zumv9HbtuLE4/8K7We/W3yx2j+gnW8ER4nhg29lE5SBO0j0d62PKB1OHg1p9vxVd43bstY8mpnXT6wf2iJ6RAcMahiJrtcnJT6vBPhEZDmZ2ERgj2ohS1oi2LGCGKAUoX2sIaeEgTBdWfCiCpjT5wcvZ88nzWcjzSZNiMCACiIgAEIB6yaPVt1oAAf3eWSEDwcmhQCDFHjt9d09/j7BsTrlMnHdi6WzzVaLc8JWi1PDVoszwGFFieJwoGh4rCobHi8uGHcQlwy5i1LCTGDHsJoYNO4ohw67iwjD3F9zEueGeY8aZWWFaXV53FQAofVij7Pm7yFkAyB/8D26O+q45FJqpAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTA0LTEyVDE0OjM4OjQzKzAwOjAwkPtYggAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wNC0xMlQxNDozODo0MyswMDowMOGm4D4AAAAASUVORK5CYII='
});
}
}
bannermissions = self.sortmissions(bannermissions).reverse(); // reverse sort, switched a and b
for (let rowcnt = 0; rowcnt < bannermissions.length; rowcnt += 6) {
var row = table.appendChild(document.createElement('tr'));
for (let cellcnt = 0; cellcnt < 6 && rowcnt + cellcnt < bannermissions.length; cellcnt++) {
let mission = bannermissions[rowcnt + cellcnt];
let cell = row.appendChild(document.createElement('td'));
cell.style.position = 'relative';
let img = cell.appendChild(document.createElement('img'));
if (mission.placeholdertitle) {
img.src = mission.image;
} else {
img.src = mission.image.replace('http:','https:').replace(/=[swh]\d+(|\-[cp])$/,'') + '=s50-c'; // fix for http urls, download max 50x50 image
}
img.title = (mission.placeholdertitle?'Placeholder: ':'') + mission.title;
if (mission.medianCompletionTimeMs) img.title += '\n' + msToTime(mission.medianCompletionTimeMs);
if (mission.waypoints_length) img.title += '\n' + distance(mission.waypoints_length);
img.style.cursor = 'pointer';
img.addEventListener('click', function() {
if (!mission.placeholdertitle) {
window.plugin.missions.openMission(mission.guid);
}
}, false);
}
}
let totaltime = container.appendChild(document.createElement('span'));
totaltime.className = 'plugin-mission-info time help';
totaltime.textContent = 'Total time: ' + msToTime(medianCompletionTimeMs_total);
let timeimg = totaltime.insertBefore(document.createElement('img'), totaltime.firstChild);
timeimg.src = 'https://commondatastorage.googleapis.com/ingress.com/img/tm_icons/time.png';
container.appendChild(document.createElement('br'));
let totallength = container.appendChild(document.createElement('span'));
totallength.className = 'plugin-mission-info length help';
totallength.textContent = 'Total distance: ' + distance(waypoints_length);
let distanceimg = totallength.insertBefore(document.createElement('img'), totallength.firstChild);
distanceimg.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAQMAAABsABwUAAAABlBMVEUAAACy+/gnk9HpAAAAAXRSTlMAQObYZgAAABVJREFUCNdjYEADB9Dg//8QjA7RAAB2VBF9TkATUAAAAABJRU5ErkJggg==';
container.appendChild(document.createElement('br'));
let drawmissions = container.appendChild(document.createElement('button'));
drawmissions.textContent = "Draw all missions";
drawmissions.addEventListener('click', function(e) {
e.preventDefault();
for (let cnt = bannermissions.length - 1; cnt >= 0; cnt--) { // reverse
if (!bannermissions[cnt].placeholdertitle) {
window.plugin.missions.openMission(bannermissions[cnt].guid);
}
}
},false);
let zoommissions = container.appendChild(document.createElement('button'));
zoommissions.textContent = "Zoom to view all missions";
zoommissions.addEventListener('click', function(e) {
e.preventDefault();
let allmissionwaypoints = { waypoints: [] };
for (let cnt = 0; cnt < bannermissions.length; cnt++) {
allmissionwaypoints.waypoints = allmissionwaypoints.waypoints.concat(bannermissions[cnt].waypoints);
}
window.plugin.missions.zoomToMission(allmissionwaypoints);
},false);
let author = container.appendChild(document.createElement('div'));
author.className = self.id + 'author';
author.textContent = self.title + ' version ' + self.version + ' by ' + self.author;
let buttons = self.getdialogbuttons('Banner view');
window.dialog({
title: 'Stored Missions Banner View - ' + missions.length,
html: container,
id: "missionsList",
minWidth: 360,
height: 'auto'
}).dialog('option', 'buttons', buttons);
};
self.loadMissionsList = function(missionguids,total,importstatusarea,exportbutton,failonerror) {
let loadguid = '';
while ((typeof loadguid != 'string' || loadguid == '') && missionguids.length > 0) {
loadguid = missionguids.shift();
//if (typeof loadguid == 'string' && loadguid != '' && window.plugin.missions.cacheByMissionGuid[loadguid]) loadguid = '';
}
if (typeof loadguid == 'string' && loadguid != '') {
importstatusarea.textContent = 'Importing: ' + (total - missionguids.length) + '/' + total;
setTimeout(function() { window.plugin.missions.loadMission(loadguid,function(mission) { let missions = self.getMissions(true); exportbutton.textContent = 'Export stored missions: ' + missions.length; self.loadMissionsList(missionguids,total,importstatusarea,exportbutton,false); },function(error) { importstatusarea.textContent += ' Failed'; if (failonerror === false) missionguids.unshift(loadguid); self.loadMissionsList(missionguids,total,importstatusarea,exportbutton,true); }); },20);
} else {
importstatusarea.textContent = 'Import ready: ' + total;
}
};
self.missionsTransfer = function() {
let missions = self.getMissions(true);
let container = document.createElement('div');
container.className = self.id + 'mainmenu';
container.style.textAlign = 'center';
let exportbutton = container.appendChild(document.createElement('button'));
exportbutton.textContent = 'Export stored missions: ' + missions.length;
exportbutton.style.minWidth = '190px';
exportbutton.style.margin = '5px';
exportbutton.addEventListener('click', function(e) {
e.preventDefault();
let missionguids = [];
for (let cnt = 0; cnt < missions.length; cnt++) {
missionguids.push(missions[cnt].guid);
}
prompt('Export missions:',JSON.stringify(missionguids));
}, false);
container.appendChild(document.createElement('br'));
let importbutton = container.appendChild(document.createElement('button'));
importbutton.textContent = 'Import missions';
importbutton.style.minWidth = '190px';
importbutton.style.margin = '5px';
let importstatusarea = container.appendChild(document.createElement('div'));
importbutton.addEventListener('click', function(e) {
e.preventDefault();
let missionguids = prompt('Import missions:');
if (missionguids == null) {
importstatusarea.textContent = '';
} else {
importstatusarea.textContent = 'Checking data...';
try {
missionguids = JSON.parse(missionguids);
} catch(e) {
importstatusarea.textContent = 'Invalid data';
}
if (typeof missionguids === 'object' && missionguids instanceof Object && missionguids instanceof Array) { // expect an array
self.loadMissionsList(missionguids,missionguids.length,importstatusarea,exportbutton,false);
} else {
importstatusarea.textContent = 'No valid data';
}
}
}, false);
let clearbutton = container.appendChild(document.createElement('button'));
clearbutton.textContent = 'Clear stored';
clearbutton.style.minWidth = '190px';
clearbutton.style.margin = '5px';
clearbutton.addEventListener('click', function(e) {
e.preventDefault();
self.clearStored(function() { self.missionsTransfer(); });
}, false);
container.appendChild(document.createElement('br'));
let buttons = self.getdialogbuttons('Transfer');
window.dialog({
title: 'Stored Missions Transfer',
html: container,
id: "missionsList",
width: 'auto',
height: 'auto'
}).dialog('option', 'buttons', buttons);
};
self.showconfirmDialog = function(bounds) {
let container = document.createElement('div');
container.appendChild(document.createTextNode('Missions for the current map view were recently scanned.'));
container.appendChild(document.createElement('br'));
container.appendChild(document.createTextNode('Missions found: ' + self.missions.length));
if (!self.boundsloadedcomplete) {
container.appendChild(document.createElement('br'));
container.appendChild(document.createTextNode('Last scan was incomplete, there could be more missions.'));
}
if (!self.zoomlevelHasPortals()) {
container.appendChild(document.createElement('br'));
container.appendChild(document.createTextNode('Zoom in to show all portals to find more missions.'));
}
container.appendChild(document.createElement('br'));
let cancelbutton = container.appendChild(document.createElement('button'));
cancelbutton.textContent = 'Use previous scan';
cancelbutton.addEventListener('click', function(e) {
e.preventDefault();
cancelbutton.disabled = true;
window.plugin.missions.showMissionListDialog(self.getMissions());
}, false);
let rescanbutton = container.appendChild(document.createElement('button'));
rescanbutton.textContent = (self.settings.showtextbuttons?'Rescan anyway':'🔎↻');
rescanbutton.addEventListener('click', function(e) {
e.preventDefault();
rescanbutton.disabled = true;
self.clearmissions();
window.plugin.missions.openTopMissions(bounds);
}, false);
let buttons = self.getdialogbuttons('Confirm rescan');
window.dialog({
title: 'Missions in view',
html: container,
id: "missionsList",
minWidth: 360,
height: 'auto'
}).dialog('option', 'buttons', buttons);
};
self.setupOpenTopMissions = function() {
// replace stock missions function:
window.plugin.missions.openTopMissions = function(bounds) {
if (self.subboundsloading || self.subboundslist.length > 0) { // already running
self.showprogressDialog();
return;
}
bounds = bounds || window.map.getBounds();
if (self.boundsloaded == bounds.toBBoxString()) {
// map did not move, ask to show same missions or reload
self.showconfirmDialog(bounds);
return;
}
let buttons = self.getdialogbuttons('Scan');
window.dialog({
title: 'Missions in view',
html: 'Loading top missions for current view
Please wait...',
id: "missionsList",
minWidth: 360,
height: 'auto'
}).dialog('option', 'buttons', buttons);
if (self.settings.cleardebuglayeronscan) {
self.cleardebuglayer();
}
self.count25 = 0;
self.count25max = 0;
self.countretries = 0;
self.countfailed = 0;
self.countrequests = 1;
self.countmissions = 0;
self.drawdebuglayer(bounds);
window.plugin.missions.loadMissionsInBounds(
bounds,
function(missions) {
// first run
self.clearmissions();
self.appendmissions(missions);
self.boundsloaded = bounds.toBBoxString();
if (missions.length < 25) {
self.setdebuglayercolor(bounds,{color: '#008000', fill: true, fillColor: '#008000'}); // OKAY: green fill
} else {
self.count25++;
self.updateProgressDialog();
if (!self.settings.searchmethoddefault) {
self.setdebuglayercolor(bounds,{color: '#ffff00', fill: false}); // SPLIT: yellow border
self.confirmFindmore(missions,bounds,window.plugin.missions.showMissionListDialog);
return;
}
self.setdebuglayercolor(bounds,{color: '#c000c0', fill: true, fillColor: '#c000c0', fillOpacity: 0.5}); // MAX: purple fill
}
self.countmissions += missions.length;
console.log('Total requests: ' + self.countrequests + ' (result 25 missions: ' + self.count25 + ') Missions found: ' + self.countmissions);
self.boundsloadedcomplete = true;
window.plugin.missions.showMissionListDialog(self.getMissions());
},
function() { // on error
if (self.retrybounds != bounds.toBBoxString()) {
self.retrybounds = bounds.toBBoxString();
self.retrycount = 0;
}
self.retrycount++;
if (self.retrycount <= self.settings.retrymax) {
self.setdebuglayercolor(bounds,{color: '#ff690f', fill: true, fillColor: '#ff690f'}); // RETRY: orange fill
window.plugin.missions.openTopMissions(bounds);
} else {
self.setdebuglayercolor(bounds,{color: '#FF0000', fill: true, fillColor: '#FF0000', fillOpacity: 0.8}); // FAIL: red fill
alert('Failed to load missions in view');
}
}
);
}
};
self.getdialogbuttons = function(currentdialog) {
let buttons = {};
if (self.settings.showtransferbutton) buttons[(self.settings.showtextbuttons?'Transfer':'🔀')] = function() { self.missionsTransfer(); };
if (self.settings.showbannerbutton) {
if (currentdialog == 'Banner view') {
buttons[(self.settings.showtextbuttons?'Banner edit':'⚬⚬⚬🧰')] = function() { self.bannerEdit(); };
} else {
buttons[(self.settings.showtextbuttons?'Banner view':'⚬⚬⚬')] = function() { self.bannerDisplay(); };
}
}
if (self.settings.showstoredbutton) {
if (currentdialog == 'Stored') {
buttons[(self.settings.showtextbuttons?'Clear stored':'☑⛔')] = function() { self.clearStored(function() { window.plugin.missions.showMissionListDialog([],"Missions stored"); }); };
} else {
buttons[(self.settings.showtextbuttons?'Stored':'☑')] = function() { window.plugin.missions.showMissionListDialog(self.getMissions(true),"Missions stored"); };
}
}
if (currentdialog == 'In view') {
buttons[(self.settings.showtextbuttons?'Rescan':'🔎↻')] = function() { window.plugin.missions.openTopMissions(); };
} else {
buttons[(self.settings.showtextbuttons?'In view':'🔎')] = function() { window.plugin.missions.showMissionListDialog(self.getMissions()); };
}
if (self.settings.showcreatenewbutton) buttons[(self.settings.showtextbuttons?'Create new mission':'➕')] = function() { open('//missions.ingress.com','_blank').focus(); };
buttons[(self.settings.showtextbuttons?'Add-on opt':'☰')] = function() { self.settingsdialog(); };
buttons[(self.settings.showtextbuttons?'Close':'✖')] = function() { $(this).dialog('close'); };
return buttons;
};
self.showhelpdialog = function() {
let container = document.createElement('div');
container.innerHTML = `Basic settings:
- Show traditonal text buttons (instead of icons) (default on)
Dialog buttons at the bottom are traditonally text buttons.
If there are a lot of buttons it can become unclear.
Disable this option to show unicode icons for your buttons.
Be aware that unicode icons differ between phones and desktops.
- Show button "Create new mission" ➕ (default on)
The missions plugin shows a button to open the mission creator website.
You can disable this button if you never intend to create a new mission.
- Auto refresh missions list when map is moved
- Default mission search (top 25 missions)
The standard missions plugin stops after 1 search and returns a maximum of 25 missions inside the visible area.
- Keep splitting in even areas if 25 missions found
This add on can keep searching smaller areas until less then 25 missions are found. All found missions from the smaller areas are combined in one list. It continues until the area has a minimum size.
- At portal zoomlevel split area by mission startportals
At portal zoomlevel it can be more efficient (less areas of maximum 25 missions) to split the area up by using small groups of mission startportals. It continues until a single portal has 25 or less missions. At higher zoom levels it will automatically use the even area split method.