Waze Edit Count Monitor

Displays your daily edit count in the WME toolbar. Warns if you might be throttled.

当前为 2019-06-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Waze Edit Count Monitor
  3. // @namespace https://greasyfork.org/en/users/45389-mapomatic
  4. // @version 2019.06.22.001
  5. // @description Displays your daily edit count in the WME toolbar. Warns if you might be throttled.
  6. // @author MapOMatic
  7. // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
  8. // @require https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js
  9. // @license GNU GPLv3
  10. // @contributionURL https://github.com/WazeDev/Thank-The-Authors
  11. // @grant GM_xmlhttpRequest
  12. // @connect www.waze.com
  13.  
  14. // ==/UserScript==
  15.  
  16. /* global W */
  17. /* global toastr */
  18. /* global $ */
  19. /* global GM_xmlhttpRequest */
  20.  
  21. // This function is injected into the page to allow it to run in the page's context.
  22. function wecmInjected() {
  23. const TOASTR_URL = 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js';
  24. const TOASTR_SETTINGS = {
  25. remindAtEditCount: 100,
  26. warnAtEditCount: 150,
  27. wasReminded: false,
  28. wasWarned: false
  29. };
  30. const TOOLTIP_TEXT = 'Your daily edit count from your profile. Click to open your profile.';
  31.  
  32. let _$outputElem = null;
  33. let _$outputElemContainer = null;
  34. let _lastEditCount = null;
  35. let _userName = null;
  36. let _savesWithoutIncrease = 0;
  37. let _lastURCount = null;
  38. let _lastMPCount = null;
  39.  
  40. function log(message) {
  41. console.log('Edit Count Monitor:', message);
  42. }
  43.  
  44. function checkEditCount() {
  45. window.postMessage(JSON.stringify(['wecmGetCounts', _userName]), '*');
  46. TOASTR_SETTINGS.wasReminded = false;
  47. TOASTR_SETTINGS.wasWarned = false;
  48. toastr.remove();
  49. }
  50.  
  51. function getChangedObjectCount() {
  52. let count = 0;
  53. const changed = W.model._getModifiedObjects();
  54. Object.keys(changed).forEach(key => {
  55. const obj = changed[key];
  56. count += obj.Insert.length + obj.Update.length + obj.Delete.length;
  57. });
  58. return count;
  59. }
  60.  
  61. function updateEditCount(editCount, urCount, mpCount, noIncrement) {
  62. // Add the counter div if it doesn't exist.
  63. if ($('#wecm-count').length === 0) {
  64. _$outputElemContainer = $('<div>', { class: 'toolbar-button', style: 'font-weight: bold; font-size: 16px; border-radius: 10px;' });
  65. const $innerDiv = $('<div>', { class: 'item-container', style: 'padding-left: 10px; padding-right: 10px; cursor: default;' });
  66. _$outputElem = $('<a>', {
  67. id: 'wecm-count',
  68. href: `https://www.waze.com/user/editor/${_userName.toLowerCase()}`,
  69. target: '_blank',
  70. style: 'text-decoration:none',
  71. 'data-original-title': TOOLTIP_TEXT
  72. });
  73. $innerDiv.append(_$outputElem);
  74. _$outputElemContainer.append($innerDiv);
  75. $('#edit-buttons').children().first().append(_$outputElemContainer);
  76. _$outputElem.tooltip({
  77. placement: 'auto top',
  78. delay: { show: 100, hide: 100 },
  79. html: true,
  80. template: '<div class="tooltip" role="tooltip" style="opacity:0.95"><div class="tooltip-arrow"></div>'
  81. + '<div class="my-tooltip-header" style="display:block;"><b></b></div>'
  82. + '<div class="my-tooltip-body tooltip-inner" style="display:block; font-weight:600; !important"></div></div>'
  83. });
  84. }
  85.  
  86. // log('edit count = ' + editCount + ', UR count = ' + urCount.count);
  87. if (_lastEditCount !== editCount || _lastURCount.count !== urCount.count || _lastMPCount.count !== mpCount.count) {
  88. _savesWithoutIncrease = 0;
  89. } else if (!noIncrement) {
  90. _savesWithoutIncrease++;
  91. }
  92.  
  93. let textColor;
  94. let bgColor;
  95. let tooltipTextColor;
  96. switch (_savesWithoutIncrease) {
  97. case 0:
  98. case 1:
  99. textColor = '#354148';
  100. bgColor = 'white';
  101. tooltipTextColor = 'white';
  102. break;
  103. case 2:
  104. textColor = '#354148';
  105. bgColor = 'yellow';
  106. tooltipTextColor = 'black';
  107. break;
  108. default:
  109. textColor = 'white';
  110. bgColor = 'red';
  111. tooltipTextColor = 'white';
  112. }
  113. _$outputElemContainer.css('background-color', bgColor);
  114. _$outputElem.css('color', textColor).html(editCount);
  115. const urCountText = `<div style="margin-top:8px;padding:3px;">UR's&nbsp;Closed:&nbsp;${urCount.count}&nbsp;&nbsp;(since&nbsp;${
  116. (new Date(urCount.since)).toLocaleDateString()})</div>`;
  117. const mpCountText = `<div style="margin-top:0px;padding:0px 3px;">MP's&nbsp;Closed:&nbsp;${mpCount.count}&nbsp;&nbsp;(since&nbsp;${(
  118. new Date(mpCount.since)).toLocaleDateString()})</div>`;
  119. const warningText = (_savesWithoutIncrease > 0) ? `<div style="border-radius:8px;padding:3px;margin-top:8px;margin-bottom:5px;color:${
  120. tooltipTextColor};background-color:${bgColor};">${_savesWithoutIncrease} consecutive saves without an increase. (Are you throttled?)</div>` : '';
  121. _$outputElem.attr('data-original-title', TOOLTIP_TEXT + urCountText + mpCountText + warningText);
  122. _lastEditCount = editCount;
  123. _lastURCount = urCount;
  124. _lastMPCount = mpCount;
  125. }
  126.  
  127. function receiveMessage(event) {
  128. let msg;
  129. try {
  130. msg = JSON.parse(event.data);
  131. } catch (err) {
  132. // Do nothing
  133. }
  134.  
  135. if (msg && msg[0] === 'wecmUpdateUi') {
  136. const editCount = msg[1][0];
  137. const urCount = msg[1][1];
  138. const mpCount = msg[1][2];
  139. updateEditCount(editCount, urCount, mpCount);
  140. }
  141. }
  142.  
  143. function checkChangedObjectCount() {
  144. const objectEditCount = getChangedObjectCount();
  145. if (objectEditCount >= TOASTR_SETTINGS.warnAtEditCount && !TOASTR_SETTINGS.wasWarned) {
  146. toastr.remove();
  147. toastr.warning(`You have edited at least ${TOASTR_SETTINGS.warnAtEditCount} objects. You should consider saving soon. If you get an error while saving, you may need to undo some actions and try again.`, 'Reminder from Edit Count Monitor:');
  148. TOASTR_SETTINGS.wasWarned = true;
  149. // TOASTR_SETTINGS.wasReminded = true;
  150. } else if (objectEditCount >= TOASTR_SETTINGS.remindAtEditCount && !TOASTR_SETTINGS.wasReminded) {
  151. toastr.remove();
  152. toastr.info(`You have edited at least ${TOASTR_SETTINGS.remindAtEditCount} objects. You should consider saving soon.`, 'Reminder from Edit Count Monitor:');
  153. TOASTR_SETTINGS.wasReminded = true;
  154. } else if (objectEditCount < TOASTR_SETTINGS.remindAtEditCount) {
  155. TOASTR_SETTINGS.wasWarned = false;
  156. TOASTR_SETTINGS.wasReminded = false;
  157. toastr.remove();
  158. }
  159. }
  160.  
  161. function errorHandler(callback) {
  162. try {
  163. callback();
  164. } catch (ex) {
  165. console.error('Edit Count Monitor:', ex);
  166. }
  167. }
  168.  
  169. async function init() {
  170. _userName = W.loginManager.user.userName;
  171. // Listen for events from sandboxed code.
  172. window.addEventListener('message', receiveMessage);
  173. // Listen for Save events.
  174.  
  175. $('head').append(
  176. $('<link/>', {
  177. rel: 'stylesheet',
  178. type: 'text/css',
  179. href: 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css'
  180. }),
  181. $('<style type="text/css">#toast-container {position: absolute;} #toast-container > div {opacity: 0.95;} .toast-top-center {top: 30px;}</style>')
  182. );
  183. await $.getScript(TOASTR_URL);
  184. toastr.options = {
  185. target: '#map',
  186. timeOut: 9999999999,
  187. positionClass: 'toast-top-right',
  188. closeOnHover: false,
  189. closeDuration: 0,
  190. showDuration: 0,
  191. closeButton: true
  192. // preventDuplicates: true
  193. };
  194. W.model.actionManager.events.register('afterclearactions', null, () => errorHandler(checkEditCount));
  195. W.model.actionManager.events.register('afteraction', null, () => errorHandler(checkChangedObjectCount));
  196. W.model.actionManager.events.register('afterundoaction', null, () => errorHandler(checkChangedObjectCount));
  197.  
  198. // Update the edit count first time.
  199. checkEditCount();
  200. log('Initialized.');
  201. }
  202.  
  203. function bootstrap() {
  204. if (W && W.loginManager && W.loginManager.events && W.loginManager.events.register && W.map && W.loginManager.user) {
  205. log('Initializing...');
  206. init();
  207. } else {
  208. log('Bootstrap failed. Trying again...');
  209. setTimeout(bootstrap, 1000);
  210. }
  211. }
  212.  
  213. bootstrap();
  214. }
  215.  
  216.  
  217. // Code that is NOT injected into the page.
  218. // Note that jQuery may or may not be available, so don't rely on it in this part of the script.
  219.  
  220. function getEditorProfileFromSource(source) {
  221. const match = source.match(/gon.data=({.*?});gon.env=/i);
  222. return JSON.parse(match[1]);
  223. }
  224.  
  225. function getEditCountFromProfile(profile) {
  226. const editingActivity = profile.editingActivity;
  227. return editingActivity[editingActivity.length - 1];
  228. }
  229.  
  230. function getEditCountByTypeFromProfile(profile, type) {
  231. const edits = profile.editsByType.find(editsEntry => editsEntry.key === type);
  232. return edits ? edits.value : -1;
  233. }
  234.  
  235. // Handle messages from the page.
  236. function receivePageMessage(event) {
  237. let msg;
  238. try {
  239. msg = JSON.parse(event.data);
  240. } catch (err) {
  241. // Ignore errors
  242. }
  243.  
  244. if (msg && msg[0] === 'wecmGetCounts') {
  245. const userName = msg[1];
  246. GM_xmlhttpRequest({
  247. method: 'GET',
  248. url: `https://www.waze.com/user/editor/${userName}`,
  249. onload: res => {
  250. const profile = getEditorProfileFromSource(res.responseText);
  251. window.postMessage(JSON.stringify(['wecmUpdateUi', [
  252. getEditCountFromProfile(profile),
  253. getEditCountByTypeFromProfile(profile, 'mapUpdateRequest'),
  254. getEditCountByTypeFromProfile(profile, 'machineMapProblem')
  255. ]]), '*');
  256. }
  257. });
  258. }
  259. }
  260.  
  261. const wecmInjectedScript = document.createElement('script');
  262. wecmInjectedScript.textContent = `${wecmInjected.toString()} \nwecmInjected();`;
  263. wecmInjectedScript.setAttribute('type', 'application/javascript');
  264. document.body.appendChild(wecmInjectedScript);
  265.  
  266. // Listen for events coming from the page script.
  267. window.addEventListener('message', receivePageMessage);