WME Closure Details

Provide access to all closure details within the closures list

  1. // ==UserScript==
  2. // @name WME Closure Details
  3. // @namespace http://www.tomputtemans.com/
  4. // @description Provide access to all closure details within the closures list
  5. // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor.*$/
  6. // @icon data:image/png;base64,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
  7. // @version 1.3.0
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. /* global W, $, I18n */
  12.  
  13. var styleElement; // Style element to reuse whenever it gets removed by the WME (user login, for example)
  14.  
  15. async function onWmeReady(e) {
  16. if (e && e.user === null) {
  17. return;
  18. }
  19. if (!W.loginManager.user) {
  20. W.loginManager.events.register('login', null, onWmeReady);
  21. W.loginManager.events.register('loginStatus', null, onWmeReady);
  22. // Double check as event might have triggered already
  23. if (!W.loginManager.user) {
  24. return;
  25. }
  26. }
  27. console.log('Initialize script');
  28. observeContentsPane();
  29. applyStyles();
  30. }
  31.  
  32. function observeContentsPane() {
  33. function handleMutations(mutations) {
  34. mutations.forEach(function(mutation) {
  35. var closureBlocks = mutation.target.querySelectorAll('.closure-item');
  36. var selectedIDs = W.selectionManager.getSelectedFeatures().filter(function(obj) {
  37. return obj.layer?.name == 'segments';
  38. }).map(function(obj) {
  39. return obj.attributes?.wazeFeature?.id;
  40. });
  41. var selectedClosures = W.model.roadClosures.getObjectArray().filter(function(closure) {
  42. return selectedIDs.indexOf(closure.attributes.segID) != -1;
  43. });
  44. for (var i = 0; i < closureBlocks.length; i++) {
  45. let closureBlock = closureBlocks[i];
  46. closureBlock.addEventListener('click', removeAllTooltips);
  47. if (closureBlock.querySelector('.menu-initiator')) { // To be replaced with optional chaining somewhere in the future (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)
  48. closureBlock.querySelector('.menu-initiator').addEventListener('click', removeAllTooltips);
  49. }
  50. var buttons = closureBlock.querySelectorAll('a');
  51. for (var j = 0; j < buttons.length; j++) {
  52. buttons[j].addEventListener('click', removeAllTooltips);
  53. }
  54. var matchedClosure = selectedClosures.find(function(closure) {
  55. return getLocalizedTime(closure.attributes.startDate) == getTimeFromBlock(closureBlock.querySelector('.start-date')) &&
  56. getLocalizedTime(closure.attributes.endDate) == getTimeFromBlock(closureBlock.querySelector('.end-date'));
  57. });
  58. if (matchedClosure) {
  59. var description;
  60. if (matchedClosure.attributes.reason !== '') {
  61. description = '<strong class="description">' + matchedClosure.attributes.reason + '</strong>';
  62. } else {
  63. description = '<em class="description">No description set</em>';
  64. }
  65. description += '<table class="details"><tbody>';
  66. if (matchedClosure.attributes.provider) {
  67. description += '<tr><th>Provided by</th><td>' + matchedClosure.attributes.provider + '</td></tr>';
  68. }
  69. description += '<tr><th>Created by</th><td>' + getUsername(matchedClosure.attributes.createdBy) + '</td></tr><tr><th>Created on</th><td>' + I18n.l('time.formats.long', matchedClosure.attributes.createdOn) + '</td></tr>';
  70. if (matchedClosure.attributes.updatedBy) {
  71. description += '<tr><th>Updated by</th><td>' + getUsername(matchedClosure.attributes.updatedBy) + '</td></tr>';
  72. }
  73. if (matchedClosure.attributes.updatedOn) {
  74. description += '<tr><th>Updated on</th><td>' + I18n.l('time.formats.long', matchedClosure.attributes.updatedOn) + '</td></tr>';
  75. }
  76. description += '<tr><td colspan="2" style="text-align:center"><em>' + (matchedClosure.attributes.permanent ? 'Ignores traffic' : 'Listens to traffic') + '</em></td></tr></tbody></table></div>';
  77. $(closureBlock).tooltip({
  78. placement: 'right',
  79. trigger: 'hover',
  80. html: true,
  81. title: description
  82. });
  83. }
  84. }
  85. });
  86. if (document.querySelector('.contents .closures')) {
  87. (new MutationObserver(handleMutations)).observe(document.querySelector('.contents .closures'), {
  88. childList: true
  89. });
  90. }
  91. }
  92. (new MutationObserver(handleMutations)).observe(document.querySelector('.contents'), {
  93. childList: true
  94. });
  95. }
  96.  
  97. function removeAllTooltips() {
  98. var tooltips = document.querySelectorAll('.tooltip');
  99. if (tooltips.length > 0) {
  100. for (var tooltip of tooltips) {
  101. tooltip.parentNode.removeChild(tooltip);
  102. }
  103. }
  104. }
  105.  
  106. function getTimeFromBlock(node) {
  107. return node.querySelector('.date').textContent + ' ' + node.querySelector('.time').textContent;
  108. }
  109.  
  110. function getLocalizedTime(date) {
  111. var splitDate = date.split(' ');
  112. return I18n.l('date.formats.default', splitDate[0]) + ' ' + splitDate[1];
  113. }
  114.  
  115. function getUsername(id) {
  116. var user = W.model.users.getObjectById(id);
  117. if (user) {
  118. return user.attributes.userName;
  119. } else {
  120. return id + ' (user not loaded)';
  121. }
  122. }
  123.  
  124. function applyStyles() {
  125. if (!styleElement) {
  126. styleElement = document.createElement('style');
  127. styleElement.textContent = `
  128. div.tooltip {
  129. z-index: 998 !important;
  130. }
  131. .tooltip .tooltip-inner {
  132. width: 250px;
  133. max-width: 400px;
  134. }
  135. .tooltip-inner .description {
  136. font-size: 120%;
  137. }
  138. .tooltip-inner .details {
  139. border-width: 0;
  140. text-align: left;
  141. width: 100%;
  142. }
  143. .tooltip-inner .details th {
  144. text-align: right;
  145. padding-right: 0.4em;
  146. color: #fff;
  147. }
  148. `;
  149. }
  150. if (!styleElement.parentNode) {
  151. document.head.appendChild(styleElement);
  152. }
  153. }
  154.  
  155. function onWmeInitialized() {
  156. if (W.userscripts?.state?.isReady) {
  157. console.log('W is ready and in "wme-ready" state. Proceeding with initialization.');
  158. onWmeReady();
  159. } else {
  160. console.log('W is ready, but not in "wme-ready" state. Adding event listener.');
  161. document.addEventListener('wme-ready', onWmeReady, { once: true });
  162. }
  163. }
  164.  
  165. function bootstrap() {
  166. if (!W) {
  167. console.log('W is not available. Adding event listener.');
  168. document.addEventListener('wme-initialized', onWmeInitialized, { once: true });
  169. } else {
  170. onWmeInitialized();
  171. }
  172. }
  173.  
  174. bootstrap();