diff --git a/plugins/missions.user.js b/plugins/missions.user.js
index 97b94a4b..aa76a29a 100644
--- a/plugins/missions.user.js
+++ b/plugins/missions.user.js
@@ -2,11 +2,11 @@
// @id iitc-plugin-missions@jonatkins
// @name IITC plugin: Missions
// @category Info
-// @version 0.0.1.@@DATETIMEVERSION@@
+// @version 0.0.2.@@DATETIMEVERSION@@
// @namespace https://github.com/jonatkins/ingress-intel-total-conversion
// @updateURL @@UPDATEURL@@
// @downloadURL @@DOWNLOADURL@@
-// @description [@@BUILDNAME@@-@@BUILDDATE@@] WORK IN PROGRESS: view missions. Currently, only adds a mission start portal highlighter
+// @description [@@BUILDNAME@@-@@BUILDDATE@@] View missions. Marking progress on waypoints/missions basis. Showing mission paths on the map.
// @include https://www.ingress.com/intel*
// @include http://www.ingress.com/intel*
// @match https://www.ingress.com/intel*
@@ -19,33 +19,653 @@
// PLUGIN START ////////////////////////////////////////////////////////
-// use own namespace for plugin
-window.plugin.missions = function() {};
+var decodeWaypoint = function(data) {
+ 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) {
+ result.portal = window.decodeArray.portalSummary(data[5]);
+ // Portal waypoints have the same guid as the respective portal.
+ result.portal.guid = result.guid;
+ }
+ return result;
+};
+var decodeMission = function(data) {
+ 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(decodeWaypoint),
+ image: data[10]
+ };
+};
+var decodeMissionSummary = function(data) {
+ return {
+ guid: data[0],
+ title: data[1],
+ image: data[2],
+ ratingE6: data[3],
+ medianCompletionTimeMs: data[4]
+ };
+};
+var timeToRemaining = function(t) {
+ var data = parseInt(t / 86400) + 'd ' + (new Date(t % 86400 * 1000)).toUTCString().replace(/.*(\d{2}):(\d{2}):(\d{2}).*/, "$1h $2m $3s");
+ data = data.replace('0d', '');
+ data = data.replace('00h', '');
+ data = data.replace('00m', '');
+ return data.trim();
+};
+window.plugin.missions = {
+ // 3 days.
+ missionCacheTime: 3 * 24 * 3600 * 1E3,
+ // 3 weeks.
+ portalMissionsCacheTime: 21 * 24 * 3600 * 1E3,
+ onPortalSelected: function(event) {
+ /*if(event.selectedPortalGuid === event.unselectedPortalGuid) {
+ return;
+ }*/
+ if (window.selectedPortal === null) {
+ return;
+ }
+ var portal = window.portals[window.selectedPortal];
+ if (!portal || (!portal.options.data.mission && !portal.options.data.mission50plus)) {
+ return;
+ }
+ // After select.
+ setTimeout(function() {
+ // #resodetails
+ $('.linkdetails').append('Open mission(s)');
+ }, 0);
+ },
-window.plugin.missions.highlight = function(data) {
- var opacity = 0.7;
- var color = undefined;
+ openTopMissions: function(bounds) {
+ bounds = bounds || window.map.getBounds();
+ this.loadMissionsInBounds(bounds, this.showMissionListDialog.bind(this));
+ },
- if (data.portal.options.data.mission50plus) {
- color='red';
- } else if (data.portal.options.data.mission) {
- color='darkorange';
- }
+ openPortalMissions: function() {
+ var me = this,
+ portal = window.portals[window.selectedPortal];
+ if (!portal) {
+ return;
+ }
+ this.loadPortalMissions(window.selectedPortal, function(missions) {
+ if (!missions.length) {
+ return;
+ }
- if (color) {
- data.portal.setStyle({fillColor: color, fillOpacity: opacity});
- }
-}
+ if (missions.length === 1) {
+ me.loadMission(missions[0].guid, me.showMissionDialog.bind(me));
+ } else {
+ me.showMissionListDialog(missions);
+ }
+ });
+ },
-window.plugin.missions.setup = function() {
+ openMission: function(guid) {
+ this.loadMission(guid, this.showMissionDialog.bind(this));
+ },
- window.addPortalHighlighter('Mission start point', window.plugin.missions.highlight);
+ showMissionDialog: function(mission) {
+ var me = this;
+ var markers = this.highlightMissionPortals(mission);
+ dialog({
+ // id: 'mission-' + mission.guid,
+ title: mission.title,
+ height: 'auto',
+ html: this.renderMission(mission),
+ width: '450px',
+ closeCallback: function() {
+ me.unhighlightMissionPortals(markers);
+ },
+ collapseCallback: this.collapsFix,
+ expandCallback: this.collapsFix
+ });
+ },
+ showMissionListDialog: function(missions) {
+ dialog({
+ html: this.renderMissionList(missions),
+ height: 'auto',
+ width: '400px',
+ collapseCallback: this.collapsFix,
+ expandCallback: this.collapsFix
+ });
+ },
+
+ collapsFix: function() {
+ if (this && this.parentNode) {
+ this.parentNode.style.height = 'auto';
+ }
+ },
+
+ loadMissionsInBounds: function(bounds, callback, errorcallback) {
+ var me = this;
+ window.postAjax('getTopMissionsInBounds', {
+ northE6: ((bounds.getNorth() * 1000000) | 0),
+ southE6: ((bounds.getSouth() * 1000000) | 0),
+ westE6: ((bounds.getWest() * 1000000) | 0),
+ eastE6: ((bounds.getEast() * 1000000) | 0)
+ }, function(data) {
+ var missions = data.result.map(decodeMissionSummary);
+ if (!missions) {
+ if (errorcallback) {
+ errorcallback('Invalid data');
+ }
+ return;
+ }
+ callback(missions);
+ }, function(error) {
+ console.log('Error loading missions in bounds', arguments);
+ if (errorcallback) {
+ errorcallback(error);
+ }
+ });
+ },
+
+ loadPortalMissions: function(guid, callback, errorcallback) {
+ var me = this;
+ // Mission summary rarely goes stale.
+ if (me.cacheByPortalGuid[guid] && this.cacheByPortalGuid[guid].time > (Date.now() - this.portalMissionsCacheTime)) {
+ callback(me.cacheByPortalGuid[guid].data);
+ return;
+ }
+ window.postAjax('getTopMissionsForPortal', {
+ guid: window.selectedPortal
+ }, function(data) {
+ var missions = data.result.map(decodeMissionSummary);
+ if (!missions) {
+ if (errorcallback) {
+ errorcallback('Invalid data');
+ }
+ return;
+ }
+
+ window.runHooks('portalMissionsLoaded', { missions: missions, portalguid: guid });
+
+ me.cacheByPortalGuid[guid] = {
+ time: Date.now(),
+ data: missions
+ };
+ me.saveData();
+ callback(missions);
+ }, function(error) {
+ console.log('Error loading portal missions', arguments);
+ if (errorcallback) {
+ errorcallback(error);
+ }
+ // awww
+ });
+ },
+ loadMission: function(guid, callback, errorcallback) {
+ var me = this;
+ // TODO: we need to refresh data often enough, portal data can quickly go stale
+ if (this.cacheByMissionGuid[guid] && this.cacheByMissionGuid[guid].time > (Date.now() - this.missionCacheTime)) {
+ callback(this.getMissionCache(guid, true));
+ return;
+ }
+ window.postAjax('getMissionDetails', {
+ guid: guid
+ }, function(data) {
+ var mission = decodeMission(data.result);
+ if (!mission) {
+ if (errorcallback) {
+ errorcallback('Invalid data');
+ }
+ return;
+ }
+
+ window.runHooks('missionLoaded', { mission: mission });
+
+ me.cacheByMissionGuid[guid] = {
+ time: Date.now(),
+ data: mission
+ };
+ me.saveData();
+
+ callback(mission);
+ }, function() {
+ console.log('Error loading mission data', guid, arguments);
+ if (errorcallback) {
+ errorcallback(error);
+ }
+ // awww
+ });
+ },
+
+ renderMissionList: function(missions) {
+ return missions.map(this.renderMissionSummary, this).join(' ');
+ },
+
+ renderMissionSummary: function(mission) {
+ var cachedMission = this.getMissionCache(mission.guid);
+
+ var html = '';
+ var checked = this.settings.checkedMissions[mission.guid];
+
+ html += '
';
+ html += '

';
+ html += '
';
+ if (cachedMission) {
+ html += '
' + cachedMission.authorNickname + '';
+ html += '
';
+ }
+ html += '

' +
+ timeToRemaining((mission.medianCompletionTimeMs / 1000) | 0);
+ html += '

' +
+ (((mission.ratingE6 / 100) | 0) / 100) + '%';
+
+ if (cachedMission) {
+ html += '

' +
+ cachedMission.numUniqueCompletedPlayers;
+ html += '

' +
+ cachedMission.waypoints.length;
+ }
+ html += '
';
+ html += '
';
+ return html;
+ },
+
+ renderMission: function(mission) {
+ var me = this;
+ var checked = this.settings.checkedMissions[mission.guid];
+ //commondatastorage.googleapis.com/ingress.com/img/map_icons/linkmodeicon.png
+ var html = '';
+ html += '
';
+ html += '✓';
+ html += '
';
+ html += '';
+ html += '
' + mission.title + '
';
+ html += '
' + mission.authorNickname + '';
+ html += '
';
+ html += '
';
+ html += '

' +
+ timeToRemaining((mission.medianCompletionTimeMs / 1000) | 0);
+ html += '

' +
+ (((mission.ratingE6 / 100) | 0) / 100) + '%';
+ html += '

' +
+ mission.numUniqueCompletedPlayers;
+ html += '

' +
+ mission.waypoints.length;
+ html += '
';
+ html += '
';
+ html += mission.description;
+ html += '
';
+ html += '
';
+ html += mission.waypoints.map(function(waypoint, index) {
+ return me.renderMissionWaypoint(waypoint, index, mission);
+ }).join(' ');
+ return html;
+ },
+
+ renderMissionWaypoint: function(waypoint, index, mission) {
+ var html = '';
+ html += '';
+ html += '
';
+ if (waypoint.portal) {
+ var color = 'white';
+ if (waypoint.portal.team === 'R') { // Yay
+ color = '#00c2ff';
+ } else if (waypoint.portal.team === 'E') { // Booo
+ color = '#28f428';
+ }
+ var realHealth = (waypoint.portal.resCount / 8) * (waypoint.portal.health / 100);
+ html += this.renderPortalCircle(color, waypoint.portal.level, realHealth);
+ /*
+ var radius = ((realHealth * 360) | 0);
+ html += '
';
+ html += '
';
+ html += '
'; // 360 + 45
+ html += '
' + waypoint.portal.level + '
';
+ html += '
';
+ */
+ }
+ if (waypoint.portal) {
+ html += '
';
+ }
+ if (waypoint.title) {
+ html += waypoint.title;
+ } else {
+ html += 'Unknown';
+ }
+ if (waypoint.portal) {
+ html += '';
+ }
+ html += '
';
+ /*
+ checkbox_grey
+ checkbox_orange
+ checkbox_cyan
+ */
+ var img = 'cyan';
+ if (index === 0) {
+ img = 'orange';
+ } else if (!waypoint.objective) {
+ img = 'grey';
+ }
+ // ✓
+ var mwpid = mission.guid + '-' + waypoint.guid;
+ var checked = this.settings.checkedWaypoints[mwpid];
+
+ html += '';
+ html += '✓';
+
+ html += '
';
+ html += '';
+ html += (waypoint.objective ? waypoint.objective : '?');
+ html += '';
+ return html;
+ },
+ renderPortalCircle: function(portalColor, portalLevel, portalHealth) {
+ var s = 20,
+ bg = '#999',
+ c = portalColor,
+ i = 14,
+ ic = '#555',
+ s2 = ((s / 2) | 0),
+ si = (((s - i) / 2) | 0),
+ d = (portalHealth * 180) | 0,
+ num = portalLevel;
+ var html = '';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
' + num;
+ html += '
';
+ html += '
';
+ return html;
+ },
+
+ toggleWaypoint: function(mid, wpid, dontsave) {
+ var mwpid = mid + '-' + wpid;
+ var el = document.getElementsByClassName('wp-' + mwpid);
+ if (!this.settings.checkedWaypoints[mwpid]) {
+ this.settings.checkedWaypoints[mwpid] = true;
+ window.runHooks('waypointFinished', { mission: this.getMissionCache(mid), waypointguid: wpid });
+ $(el).show();
+ } else {
+ delete this.settings.checkedWaypoints[mwpid];
+ $(el).hide();
+ }
+ if (!dontsave) {
+ this.saveData();
+ }
+ },
+
+ toggleMission: function(mid) {
+ var mission = this.getMissionCache(mid);
+ if (!mission) {
+ return;
+ }
+ var el = document.getElementsByClassName('m-' + mid);
+ var sumel = document.getElementsByClassName('mc-' + mid);
+ if (!this.settings.checkedMissions[mid]) {
+ this.settings.checkedMissions[mid] = true;
+ mission.waypoints.forEach(function(waypoint) {
+ if (!this.settings.checkedWaypoints[mid + '-' + waypoint.guid]) {
+ this.toggleWaypoint(mid, waypoint.guid, true);
+ }
+ }, this);
+ $(el).show();
+ $(sumel).css('background-color', 'rgba(255, 187, 0, 0.3)');
+ window.runHooks('missionFinished', { mission: mission });
+ } else {
+ delete this.settings.checkedMissions[mid];
+ mission.waypoints.forEach(function(waypoint) {
+ if (this.settings.checkedWaypoints[mid + '-' + waypoint.guid]) {
+ this.toggleWaypoint(mid, waypoint.guid, true);
+ }
+ }, this);
+ $(el).hide();
+ $(sumel).css('background-color', '');
+ }
+ this.saveData();
+ },
+
+ getMissionCache: function(guid, updatePortals) {
+ if (this.cacheByMissionGuid[guid]) {
+ var cache = this.cacheByMissionGuid[guid];
+ // Update portal data from map if older then 2 minutes.
+ if (updatePortals && cache.time < (Date.now() - (2 * 60 * 1000))) {
+ cache.data.waypoints.map(function(waypoint) {
+ if (!waypoint.portal) {
+ return;
+ }
+ var wp = window.portals[waypoint.portal.guid];
+ if (!wp) {
+ return;
+ }
+ $.extend(waypoint.portal, wp.options.data);
+ });
+ }
+ return cache.data;
+ }
+ return null;
+ },
+
+ getPortalCache: function(guid) {
+ if (this.cacheByPortalGuid[guid]) {
+ return this.cacheByPortalGuid[guid].data;
+ }
+ return null;
+ },
+
+ saveData: function() {
+ this.checkCacheSize();
+ localStorage['plugins-missions-portalcache'] = JSON.stringify(this.cacheByPortalGuid);
+ localStorage['plugins-missions-missioncache'] = JSON.stringify(this.cacheByMissionGuid);
+ localStorage['plugins-missions-settings'] = JSON.stringify(this.settings);
+ },
+
+ loadData: function() {
+ this.cacheByPortalGuid = JSON.parse(localStorage['plugins-missions-portalcache'] || '{}');
+ this.cacheByMissionGuid = JSON.parse(localStorage['plugins-missions-missioncache'] || '{}');
+ this.settings = JSON.parse(localStorage['plugins-missions-settings'] || '{}');
+ },
+
+ checkCacheSize: function() {
+ if (JSON.stringify(this.cacheByPortalGuid).length > 1e6) { // 1 MB not MiB ;)
+ this.cleanupPortalCache();
+ }
+ if (JSON.stringify(this.cacheByMissionGuid).length > 2e6) { // 2 MB not MiB ;)
+ this.cleanupMissionCache();
+ }
+ },
+
+ // Cleanup oldest half of the data.
+ cleanupPortalCache: function() {
+ var me = this;
+ var cache = Object.keys(this.cacheByPortalGuid);
+ cache.sort(function(a, b) {
+ return me.cacheByPortalGuid[a].time - me.cacheByPortalGuid[b].time;
+ });
+ var toDelete = (cache.length / 2) | 0;
+ cache.splice(0, toDelete + 1).forEach(function(el) {
+ delete me.cacheByPortalGuid[el];
+ });
+ },
+
+ // Cleanup oldest half of the data.
+ cleanupMissionCache: function() {
+ var me = this;
+ var cache = Object.keys(this.cacheByMissionGuid);
+ cache.sort(function(a, b) {
+ return me.cacheByMissionGuid[a].time - me.cacheByMissionGuid[b].time;
+ });
+ var toDelete = (cache.length / 2) | 0;
+ cache.splice(0, toDelete + 1).forEach(function(el) {
+ delete me.cacheByMissionGuid[el];
+ });
+ },
+
+ highlightMissionPortals: function(mission) {
+ var markers = [];
+ var prevPortal = null;
+ mission.waypoints.forEach(function(waypoint) {
+ if (!waypoint.portal) {
+ return;
+ }
+ var portal = window.portals[waypoint.portal.guid];
+ if (!portal) { // not in view?
+ return;
+ }
+ var marker = L.circleMarker(
+ L.latLng(portal.options.data.latE6 / 1E6, portal.options.data.lngE6 / 1E6), {
+ radius: portal.options.radius + Math.ceil(portal.options.radius / 2),
+ weight: 3,
+ opacity: 1,
+ color: '#222',
+ fill: false,
+ dashArray: null,
+ clickable: false
+ }
+ );
+ this.missionLayer.addLayer(marker);
+ markers.push(marker);
+ if (prevPortal) {
+ var line = L.geodesicPolyline([
+ L.latLng(prevPortal.options.data.latE6 / 1E6, prevPortal.options.data.lngE6 / 1E6),
+ L.latLng(portal.options.data.latE6 / 1E6, portal.options.data.lngE6 / 1E6)
+ ], {
+ color: '#222',
+
+ opacity: 1,
+ weight: 2,
+ clickable: false
+ });
+ this.missionLayer.addLayer(line);
+ markers.push(line);
+ }
+ prevPortal = portal;
+ }, this);
+ return markers;
+ },
+
+ unhighlightMissionPortals: function(markers) {
+ markers.forEach(function(marker) {
+ this.missionLayer.removeLayer(marker);
+ }, this);
+ },
+
+ onPortalChanged: function(type, guid, oldval) {
+ var portal;
+ if (type === 'add' || type === 'update') {
+ // Compatibility
+ portal = window.portals[guid] || oldval;
+ if (!portal.options.data.mission && !portal.options.data.mission50plus) {
+ return;
+ }
+ if (this.markedStarterPortals[guid]) {
+ return;
+ }
+
+ this.markedStarterPortals[guid] = L.circleMarker(
+ L.latLng(portal.options.data.latE6 / 1E6, portal.options.data.lngE6 / 1E6), {
+ radius: portal.options.radius + Math.ceil(portal.options.radius / 2),
+ weight: 3,
+ opacity: 1,
+ color: '#555',
+ fill: false,
+ dashArray: null,
+ clickable: false
+ }
+ );
+ this.missionStartLayer.addLayer(this.markedStarterPortals[guid]);
+ } else if (type === 'delete') {
+ portal = oldval;
+ if (!this.markedStarterPortals[guid]) {
+ return;
+ }
+
+ this.missionStartLayer.removeLayer(this.markedStarterPortals[guid]);
+ delete this.markedStarterPortals[guid];
+ }
+ },
+
+ setup: function() {
+ this.cacheByPortalGuid = {};
+ this.cacheByMissionGuid = {};
+
+ this.markedStarterPortals = {};
+ this.markedMissionPortals = {};
+
+ this.loadData();
+
+ if (!this.settings.checkedWaypoints) {
+ this.settings.checkedWaypoints = {};
+ }
+ if (!this.settings.checkedMissions) {
+ this.settings.checkedMissions = {};
+ }
+
+ $('#toolbox').append('Open mission(s) in window');
+
+ // window.addPortalHighlighter('Mission start point', this.highlight.bind(this));
+ window.addHook('portalSelected', this.onPortalSelected.bind(this));
+
+ /*
+ I know iitc has portalAdded event but it is missing portalDeleted. So we have to resort to Object.observe
+ */
+ var me = this;
+ if (Object.observe) { // Chrome
+ Object.observe(window.portals, function(changes) {
+ changes.forEach(function(change) {
+ me.onPortalChanged(change.type, change.name, change.oldValue);
+ });
+ });
+ } else { // Firefox why no Object.observer ? :<
+ window.addHook('portalAdded', function(data) {
+ me.onPortalChanged('add', data.portal.options.guid, data.portal);
+ });
+ // TODO: bug iitc dev for portalRemoved event
+ var oldDeletePortal = window.Render.prototype.deletePortalEntity;
+ window.Render.prototype.deletePortalEntity = function(guid) {
+ if (guid in window.portals) {
+ me.onPortalChanged('delete', guid, window.portals[guid]);
+ }
+ oldDeletePortal.apply(this, arguments);
+ };
+ }
+
+ this.missionStartLayer = new L.LayerGroup();
+ this.missionLayer = new L.LayerGroup();
+
+ window.addLayerGroup('Mission start portals', this.missionStartLayer, false);
+ window.addLayerGroup('Mission portals', this.missionLayer, true);
+
+ window.pluginCreateHook('missionLoaded');
+ window.pluginCreateHook('portalMissionsLoaded');
+ window.pluginCreateHook('missionFinished');
+ window.pluginCreateHook('waypointFinished');
+ }
};
-var setup = window.plugin.missions.setup;
+var setup = window.plugin.missions.setup.bind(window.plugin.missions);
// PLUGIN END //////////////////////////////////////////////////////////
-@@PLUGINEND@@
+@@PLUGINEND@@
\ No newline at end of file