Waze Edit & UR Count Monitor

Displays your daily edit count in the WME toolbar. Warns if you might be throttled. Based on MapOMatic Edit Count

  1. // ==UserScript==
  2. // @name Waze Edit & UR Count Monitor
  3. // @version 2.5
  4. // @description Displays your daily edit count in the WME toolbar. Warns if you might be throttled. Based on MapOMatic Edit Count
  5. // @author Crotalo
  6. // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
  7. // @require https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js
  8. // @require https://greatest.deepsurf.us/scripts/24851-wazewrap/code/WazeWrap.js
  9.  
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM_addElement
  12. // @connect www.waze.com
  13.  
  14. // @namespace https://greatest.deepsurf.us/en/users/45389-mapomatic
  15. // ==/UserScript==
  16.  
  17. /* global W */
  18. /* global toastr */
  19. /* global WazeWrap */
  20.  
  21.  
  22. (function main() {
  23. 'use strict';
  24.  
  25. // This function is injected into the page to allow it to run in the page's context.
  26. function wecmInjected() {
  27. const TOASTR_URL = 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js';
  28. const TOASTR_SETTINGS = {
  29. remindAtEditCount: 100,
  30. warnAtEditCount: 150,
  31. wasReminded: false,
  32. wasWarned: false
  33. };
  34. const TOOLTIP_TEXT = 'Ediciones Diarias y URs Históricas. Click para abrir Perfil';
  35.  
  36. let _$outputElem = null;
  37. let _$outputElemContainer = null;
  38. let _lastEditCount = null;
  39. let _userName = null;
  40. let _savesWithoutIncrease = 0;
  41. let _lastURCount = null;
  42. let _lastMPCount = null;
  43.  
  44. function log(message) {
  45. console.log('Edit Count Monitor:', message);
  46. }
  47.  
  48. function checkEditCount() {
  49. window.postMessage(JSON.stringify(['wecmGetCounts', _userName]), '*');
  50. TOASTR_SETTINGS.wasReminded = false;
  51. TOASTR_SETTINGS.wasWarned = false;
  52. toastr.remove();
  53. }
  54.  
  55. function updateEditCount(editCount, urCount, purCount, mpCount, noIncrement) {
  56. // Add the counter div if it doesn't exist.
  57. if ($('#wecm-count').length === 0) {
  58. _$outputElemContainer = $('<div>', { class: 'toolbar-button', style: 'font-weight: bold; font-size: 16px; border-radius: 10px; margin-left: 4px;' });
  59. const $innerDiv = $('<div>', { class: 'item-container', style: 'padding-left: 10px; padding-right: 10px; cursor: default;' });
  60. _$outputElem = $('<a>', {
  61. id: 'wecm-count',
  62. href: `https://www.waze.com/user/editor/${_userName.toLowerCase()}`,
  63. target: '_blank',
  64. style: 'text-decoration:none',
  65. 'data-original-title': TOOLTIP_TEXT
  66. });
  67. $innerDiv.append(_$outputElem);
  68. _$outputElemContainer.append($innerDiv);
  69. if ($('#toolbar > div > div.secondary-toolbar > div.secondary-toolbar-actions > div.secondary-toolbar-actions-edit').length) {
  70. // Production WME, as of 4/25/2023
  71. $('#toolbar > div > div.secondary-toolbar > div.secondary-toolbar-actions > div.secondary-toolbar-actions-edit').after(_$outputElemContainer);
  72. } else {
  73. // Beta WME, as of 4/25/2023
  74. $('#toolbar > div > div.secondary-toolbar > div:nth-child(1)').after(_$outputElemContainer);
  75. }
  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; !important; min-width: fit-content"></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. if (_savesWithoutIncrease < 5) {
  97. textColor = '#354148';
  98. bgColor = 'white';
  99. tooltipTextColor = 'black';
  100. } else if (_savesWithoutIncrease < 10) {
  101. textColor = '#354148';
  102. bgColor = 'yellow';
  103. tooltipTextColor = 'black';
  104. } else {
  105. textColor = 'white';
  106. bgColor = 'red';
  107. tooltipTextColor = 'white';
  108. }
  109. _$outputElemContainer.css('background-color', bgColor);
  110. _$outputElem.css('color', textColor).html('Edits:&nbsp;' + editCount + ',URs:&nbsp;'+ urCount.count);
  111. const urCountText = `<div style="margin-top:8px;padding:3px;">URs&nbsp;Closed:&nbsp;${urCount.count.toLocaleString()}&nbsp;&nbsp;(since&nbsp;${
  112. (new Date(urCount.since)).toLocaleDateString()})</div>`;
  113. const purCountText = `<div style="margin-top:0px;padding:0px 3px;">PURs&nbsp;Closed:&nbsp;${
  114. purCount.count.toLocaleString()}&nbsp;&nbsp;(since&nbsp;${(
  115. new Date(purCount.since)).toLocaleDateString()})</div>`;
  116. const mpCountText = `<div style="margin-top:0px;padding:0px 3px;">MPs&nbsp;Closed:&nbsp;${mpCount.count.toLocaleString()}&nbsp;&nbsp;(since&nbsp;${(
  117. new Date(mpCount.since)).toLocaleDateString()})</div>`;
  118. let warningText = '';
  119. if (_savesWithoutIncrease) {
  120. warningText = `<div style="border-radius:8px;padding:3px;margin-top:8px;margin-bottom:5px;color:${
  121. tooltipTextColor};background-color:${bgColor};">${_savesWithoutIncrease} ${
  122. (_savesWithoutIncrease > 1) ? 'consecutive saves' : 'save'} without an increase. ${
  123. (_savesWithoutIncrease >= 5) ? '(Are you throttled?)' : ''}</div>`;
  124. }
  125. _$outputElem.attr('data-original-title', TOOLTIP_TEXT + urCountText + purCountText + mpCountText + warningText);
  126. _lastEditCount = editCount;
  127. _lastURCount = urCount;
  128. _lastMPCount = mpCount;
  129. }
  130.  
  131. function receiveMessage(event) {
  132. let msg;
  133. try {
  134. msg = JSON.parse(event.data);
  135. } catch (err) {
  136. // Do nothing
  137. }
  138.  
  139. if (msg && msg[0] === 'wecmUpdateUi') {
  140. const editCount = msg[1][0];
  141. const urCount = msg[1][1];
  142. const purCount = msg[1][2];
  143. const mpCount = msg[1][3];
  144. updateEditCount(editCount, urCount, purCount, mpCount);
  145. }
  146. }
  147.  
  148. function errorHandler(callback) {
  149. try {
  150. callback();
  151. } catch (ex) {
  152. console.error('Edit Count Monitor:', ex);
  153. }
  154. }
  155.  
  156. async function init() {
  157. _userName = W.loginManager.user.getUsername();
  158. // Listen for events from sandboxed code.
  159. window.addEventListener('message', receiveMessage);
  160. // Listen for Save events.
  161.  
  162. $('head').append(
  163. $('<link/>', {
  164. rel: 'stylesheet',
  165. type: 'text/css',
  166. href: 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css'
  167. }),
  168. $('<style type="text/css">#toast-container {position: absolute;} #toast-container > div {opacity: 0.95;} .toast-top-center {top: 30px;}</style>')
  169. );
  170. await $.getScript(TOASTR_URL);
  171. toastr.options = {
  172. target: '#map',
  173. timeOut: 9999999999,
  174. positionClass: 'toast-top-right',
  175. closeOnHover: false,
  176. closeDuration: 0,
  177. showDuration: 0,
  178. closeButton: true
  179. // preventDuplicates: true
  180. };
  181. W.model.actionManager.events.register('afterclearactions', null, () => errorHandler(checkEditCount));
  182.  
  183. // Update the edit count first time.
  184. checkEditCount();
  185. log('Initialized.');
  186. }
  187.  
  188. function bootstrap() {
  189. if (W && W.loginManager && W.loginManager.events && W.loginManager.events.register && W.map && W.loginManager.user) {
  190. log('Initializing...');
  191. init();
  192. } else {
  193. log('Bootstrap failed. Trying again...');
  194. setTimeout(bootstrap, 1000);
  195. }
  196. }
  197.  
  198. bootstrap();
  199. }
  200.  
  201. // Code that is NOT injected into the page.
  202. // Note that jQuery may or may not be available, so don't rely on it in this part of the script.
  203.  
  204. function getEditCountFromProfile(profile) {
  205. const { editingActivity } = profile;
  206. return editingActivity[editingActivity.length - 1];
  207. }
  208.  
  209. function getEditCountByTypeFromProfile(profile, type) {
  210. const edits = profile.editsByType.find(editsEntry => editsEntry.key === type);
  211. return edits ? edits.value : -1;
  212. }
  213.  
  214. // Handle messages from the page.
  215. function receivePageMessage(event) {
  216. let msg;
  217. try {
  218. msg = JSON.parse(event.data);
  219. } catch (err) {
  220. // Ignore errors
  221. }
  222.  
  223. if (msg && msg[0] === 'wecmGetCounts') {
  224. const userName = msg[1];
  225. GM_xmlhttpRequest({
  226. method: 'GET',
  227. url: `https://www.waze.com/Descartes/app/UserProfile/Profile?username=${userName}`,
  228. onload: res => {
  229. const profile = JSON.parse(res.responseText);
  230. window.postMessage(JSON.stringify(['wecmUpdateUi', [
  231. getEditCountFromProfile(profile),
  232. getEditCountByTypeFromProfile(profile, 'mapUpdateRequest'),
  233. getEditCountByTypeFromProfile(profile, 'venueUpdateRequest'),
  234. getEditCountByTypeFromProfile(profile, 'machineMapProblem')
  235. ]]), '*');
  236. }
  237. });
  238. }
  239. }
  240.  
  241. function waitForWazeWrap() {
  242. return new Promise(resolve => {
  243. function loopCheck(tries = 0) {
  244. if (WazeWrap.Ready) {
  245. resolve();
  246. } else if (tries < 1000) {
  247. setTimeout(loopCheck, 200, ++tries);
  248. }
  249. }
  250. loopCheck();
  251. });
  252. }
  253.  
  254. async function loadScriptUpdateMonitor() {
  255. let updateMonitor;
  256. await waitForWazeWrap();
  257. try {
  258. updateMonitor = new WazeWrap.Alerts.ScriptUpdateMonitor(SCRIPT_NAME, SCRIPT_VERSION, DOWNLOAD_URL, GM_xmlhttpRequest);
  259. updateMonitor.start();
  260. } catch (ex) {
  261. // Report, but don't stop if ScriptUpdateMonitor fails.
  262. console.error(`${SCRIPT_NAME}:`, ex);
  263. }
  264. }
  265.  
  266. function injectScript() {
  267. GM_addElement('script', {
  268. textContent: `${wecmInjected.toString()} \nwecmInjected();`
  269. });
  270.  
  271. // Listen for events coming from the page script.
  272. window.addEventListener('message', receivePageMessage);
  273. }
  274.  
  275. function mainInit() {
  276. injectScript();
  277. loadScriptUpdateMonitor();
  278. }
  279.  
  280. mainInit();
  281. })();