Waze Edit Count Monitor

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

当前为 2024-07-14 提交的版本,查看 最新版本

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