Waze LiveMap Options

Adds options to LiveMap to alter the Waze-suggested routes.

目前为 2018-07-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Waze LiveMap Options
  3. // @namespace WazeDev
  4. // @version 2018.07.24.002
  5. // @description Adds options to LiveMap to alter the Waze-suggested routes.
  6. // @author MapOMatic
  7. // @include /^https:\/\/www.waze.com\/.*livemap/
  8. // @contributionURL https://github.com/WazeDev/Thank-The-Authors
  9. // @license GNU GPL v3
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. /* global W */
  14. /* global Node */
  15.  
  16. (function() {
  17. 'use strict';
  18. const EXPANDED_MAX_HEIGHT = '200px';
  19. const TRANS_TIME = '0.2s';
  20. const CSS = [
  21. '.lmo-options-header { margin-top: 4px; cursor: pointer; color: #59899e; font-size: 11px; font-weight: 600; }',
  22. '.lmo-options-header i { margin-left: 5px; }',
  23. '.lmo-options-container { max-height: 500px; overflow: hidden; transition: max-height ' + TRANS_TIME + '; -moz-transition: max-height ' + TRANS_TIME + '; -webkit-transition: max-height ' + TRANS_TIME + '; -o-transition: max-height ' + TRANS_TIME + '; }',
  24. '.lmo-table { margin-top: 4px; font-size: 12px; border-collapse: collapse; }',
  25. '.lmo-table td { padding: 4px 10px 4px 10px; border: 1px solid #ddd; border-radius: 6px; }',
  26. '.lmo-table-header-text { margin: 0px; font-weight: 600; }',
  27. '.lmo-control-container { margin-right: 8px; }',
  28. '.lmo-control-container label { line-height: 18px; vertical-align: text-bottom; }',
  29. '.lmo-table input[type="checkbox"] { margin-right: 2px; }',
  30. '.lmo-table td.lmo-header-cell { padding-left: 4px; padding-right: 4px; }'
  31. ].join('\n');
  32.  
  33. let _fitBounds;
  34. let _settings = {
  35. 'lmo-tolls': {checked:false},
  36. 'lmo-freeways': {checked:false},
  37. 'lmo-ferries': {checked:false},
  38. 'lmo-difficult-turns':{checked:false},
  39. 'lmo-unpaved-roads': {checked:true},
  40. 'lmo-long-unpaved-roads': {checked:false},
  41. 'lmo-u-turns':{checked:false, opposite:true},
  42. 'lmo-hov':{checked:false, opposite:true},
  43. 'lmo-hide-traffic':{checked:false},
  44. 'lmo-day': 'today',
  45. 'lmo-hour': 'now',
  46. collapsed: false
  47. };
  48.  
  49. function checked(id, optionalSetTo) {
  50. let $elem = $('#' + id);
  51. if (typeof optionalSetTo !== 'undefined') {
  52. $elem.prop('checked', optionalSetTo);
  53. } else {
  54. return $elem.prop('checked');
  55. }
  56. }
  57.  
  58. function getDateTimeOffset() {
  59. let hour = $('#lmo-hour').val();
  60. let day = $('#lmo-day').val();
  61. if (hour === '---') hour = 'now';
  62. if (day === '---') day = 'today';
  63. if (hour === '') hour = 'now';
  64. if (day === '') day = 'today';
  65.  
  66. let t = new Date();
  67. let thour = (t.getHours() * 60) + t.getMinutes();
  68. let tnow = (t.getDay() * 1440) + thour;
  69. let tsel = tnow;
  70.  
  71. if (hour === 'now') {
  72. if (day === '0') tsel = (parseInt(day) * 1440) + thour;
  73. if (day === '1') tsel = (parseInt(day) * 1440) + thour;
  74. if (day === '2') tsel = (parseInt(day) * 1440) + thour;
  75. if (day === '3') tsel = (parseInt(day) * 1440) + thour;
  76. if (day === '4') tsel = (parseInt(day) * 1440) + thour;
  77. if (day === '5') tsel = (parseInt(day) * 1440) + thour;
  78. if (day === '6') tsel = (parseInt(day) * 1440) + thour;
  79. } else {
  80. if (day === 'today') tsel = (t.getDay() * 1440) + parseInt(hour);
  81. if (day === '0') tsel = (parseInt(day) * 1440) + parseInt(hour);
  82. if (day === '1') tsel = (parseInt(day) * 1440) + parseInt(hour);
  83. if (day === '2') tsel = (parseInt(day) * 1440) + parseInt(hour);
  84. if (day === '3') tsel = (parseInt(day) * 1440) + parseInt(hour);
  85. if (day === '4') tsel = (parseInt(day) * 1440) + parseInt(hour);
  86. if (day === '5') tsel = (parseInt(day) * 1440) + parseInt(hour);
  87. if (day === '6') tsel = (parseInt(day) * 1440) + parseInt(hour);
  88. }
  89.  
  90. let diff = tsel - tnow;
  91. if (diff < -3.5 * 1440)
  92. diff += 7 * 1440;
  93. else if (diff > 3.5 * 1440)
  94. diff -= 7 * 1440;
  95.  
  96. return diff;
  97. }
  98.  
  99. function getRouteTime(routeIdx) {
  100. let sec = W.app.map.routing._state.routes[routeIdx].getSeconds()
  101. let hours = Math.floor(sec/3600);
  102. sec -= hours * 3600;
  103. let min = Math.floor(sec/60);
  104. sec -= min * 60;
  105. return (hours > 0 ? hours + ' h ' : '') + (min > 0 ? min + ' min ' : '') + sec + ' sec';
  106. }
  107.  
  108. function updateTimes() {
  109. let $routeTimes = $('.wm-route-item__time');
  110. for (let idx=0; idx<$routeTimes.length; idx++) {
  111. let time = getRouteTime(idx);
  112. let $routeTime = $routeTimes.eq(idx);
  113. let contents = $routeTime.contents();
  114. contents[contents.length-1].remove();
  115. $routeTime.append(' ' + time);
  116. }
  117. }
  118.  
  119. function fetchRoutes() {
  120. // Does nothing if "from" and "to" haven't been specified yet.
  121. if (W.app.map.routing._state.from &&W.app.map.routing._state.to) {
  122. // HACK - Temporarily remove the onAfterItemAdded function, to prevent map from moving.
  123. W.app.map.fitBounds = function() {};
  124.  
  125. // Trigger the route search.
  126. W.app.map.routing.findRoutes();
  127. }
  128. }
  129.  
  130. function addOptions() {
  131. if (!$('#lmo-table').length) {
  132. $('.wm-route-search').after(
  133. $('<div>', {class:'lmo-options-header'}).append(
  134. $('<span>').text('Change routing options'),
  135. $('<i>', {class:'fa fa.fa-angle-down fa.fa-angle-up'}).addClass(_settings.collapsed ? 'fa-angle-down' : 'fa-angle-up')
  136. ),
  137. $('<div>', {class: 'lmo-options-container'}).css({maxHeight:_settings.collapsed ? '0px' : EXPANDED_MAX_HEIGHT}).append(
  138. $('<table>', {class: 'lmo-table'}).append(
  139. [['Avoid:',['Tolls','Freeways','Ferries','HOV','Unpaved roads','Long unpaved roads','Difficult turns','U-Turns']], ['Options:',['Hide traffic']]].map(rowItems => {
  140. let rowID = rowItems[0].toLowerCase().replace(/[ :]/g,'');
  141. return $('<tr>', {id:'lmo-row-' + rowID}).append(
  142. $('<td>', {class: 'lmo-header-cell'}).append($('<span>', {id:'lmo-header-' + rowID, class:'lmo-table-header-text'}).text(rowItems[0])),
  143. $('<td>', {class: 'lmo-settings-cell'}).append(
  144. rowItems[1].map((text) => {
  145. let idName = text.toLowerCase().replace(/ /g, '-');
  146. let id = 'lmo-' + idName;
  147. return $('<span>', {class:'lmo-control-container'}).append(
  148. $('<input>', {id:id, type:'checkbox', class:'lmo-control'}).prop('checked',_settings[id].checked), $('<label>', {for:id}).text(text)
  149. );
  150. })
  151. )
  152. );
  153. })
  154. )
  155. )
  156. );
  157. $('#lmo-header-avoid').css({color:'#c55'});
  158. $('label[for="lmo-u-turns"').attr('title','Note: this is not an available setting in the app');
  159.  
  160. let timeArray = [['Now','now']];
  161. for (let i=0; i<48; i++) {
  162. let t = i * 30;
  163. let min = t % 60;
  164. let hr = Math.floor(t / 60);
  165. let str = (hr < 10 ? ('0') : '') + hr + ':' + (min === 0 ? '00' : min);
  166. timeArray.push([str, t.toString()]);
  167. }
  168. $('#lmo-row-options td.lmo-settings-cell').append(
  169. $('<div>', {class: 'lmo-date-time'}).append(
  170. $('<label>', {for:'lmo-day', style:'font-weight: normal;'}).text('Day'),
  171. $('<select>', {id: 'lmo-day', class:'lmo-control', style:'margin-left: 4px; margin-right: 8px; padding: 0px; height: 22px;'}).append(
  172. [
  173. ['Today','today'],
  174. ['Monday','1'],
  175. ['Tuesday','2'],
  176. ['Wednesday','3'],
  177. ['Thursday','4'],
  178. ['Friday','5'],
  179. ['Saturday','6'],
  180. ['Sunday','0']
  181. ].map(val => $('<option>', {value:val[1]}).text(val[0]))
  182. ),
  183. $('<label>', {for:'lmo-hour', style:'font-weight: normal;'}).text('Time'),
  184. $('<select>', {id: 'lmo-hour', class:'lmo-control', style:'margin-left: 4px; margin-right: 8px; padding: 0px; height: 22px;'}).append(
  185. timeArray.map(val => $('<option>', {value:val[1]}).text(val[0]))
  186. )
  187. )
  188. );
  189.  
  190. // Set up events
  191. $('.lmo-options-header').click(function() {
  192. let $container = $('.lmo-options-container');
  193. let collapsed = $container.css('max-height') === '0px';
  194. $('.lmo-options-header i').removeClass(collapsed ? 'fa-angle-down' : 'fa-angle-up').addClass(collapsed ? 'fa-angle-up' : 'fa-angle-down');
  195. $container.css({maxHeight: collapsed ? EXPANDED_MAX_HEIGHT : '0px'});
  196. _settings.collapsed = !collapsed;
  197. });
  198. $('.lmo-control').change(function() {
  199. let id = this.id;
  200. if (id === 'lmo-hour' || id === 'lmo-day') {
  201. fetchRoutes();
  202. } else {
  203. let isChecked = checked(id);
  204. _settings[id].checked = isChecked;
  205. if (id === 'lmo-hide-traffic') {
  206. if (isChecked) {
  207. W.app.geoRssLayer._jamsLayer.remove();
  208. } else {
  209. W.app.geoRssLayer._jamsLayer.addTo(W.app.map);
  210. }
  211. } else {
  212. if (id === 'lmo-long-unpaved-roads') {
  213. if (isChecked) {
  214. checked('lmo-unpaved-roads', false);
  215. _settings['lmo-unpaved-roads'].checked = false;
  216. }
  217. } else if (id === 'lmo-unpaved-roads') {
  218. if (isChecked) {
  219. checked('lmo-long-unpaved-roads', false);
  220. _settings['lmo-long-unpaved-roads'].checked = false;
  221. }
  222. }
  223. fetchRoutes();
  224. }
  225. }
  226. });
  227. }
  228. }
  229.  
  230. function installHttpRequestInterceptor() {
  231. // Special thanks to Twister-UK for finding this code example...
  232. // original code from https://stackoverflow.com/questions/42578452/can-one-use-the-fetch-api-as-a-request-interceptor
  233. window.fetch = (function (origFetch) {
  234. return function myFetch(req) {
  235. let url = arguments[0];
  236. if(url.indexOf('/routingRequest?') !== -1)
  237. {
  238. // Remove all options from the request (everything after '&options=')
  239. let baseData = url.replace(url.match(/&options=(.*)/)[1],'');
  240. // recover stuff after the &options bit...
  241. let otherBits = '&returnGeometries' + url.split('&returnGeometries')[1];
  242. let options = [];
  243. [['tolls','AVOID_TOLL_ROADS'],['freeways','AVOID_PRIMARIES'],['ferries','AVOID_FERRIES'],['difficult-turns','AVOID_DANGEROUS_TURNS'],['u-turns','ALLOW_UTURNS'],['hov','ADD_HOV_ROUTES']].forEach(optionInfo => {
  244. let id = 'lmo-' + optionInfo[0];
  245. let enableOption = checked(id);
  246. if (_settings[id].opposite) enableOption = !enableOption;
  247. options.push(optionInfo[1] + ':' + (enableOption ? 't' : 'f'));
  248. });
  249. if (checked('lmo-long-unpaved-roads')) {
  250. options.push('AVOID_LONG_TRAILS:t');
  251. } else if (checked('lmo-unpaved-roads')) {
  252. options.push('AVOID_TRAILS:t');
  253. } else {
  254. options.push('AVOID_LONG_TRAILS:f');
  255. }
  256. baseData = baseData.replace(/\?at=0/,'?at=' + getDateTimeOffset());
  257. url = baseData + encodeURIComponent(options.join(',')) + otherBits;
  258. arguments[0] = url;
  259. }
  260.  
  261. let result = origFetch.apply(this, arguments);
  262. ////result.then(someFunctionToDoSomething);
  263. return result; // or return the result of the `then` call
  264. };
  265. })(fetch);
  266. }
  267.  
  268. function init() {
  269. // Insert CSS styling.
  270. $('head').append( $('<style>', {type:'text/css'}).html(CSS) );
  271.  
  272. // Add the xmlhttp request interceptor, so we can insert our own options into the routing requests.
  273. installHttpRequestInterceptor();
  274.  
  275. // Add all of the DOM stuff for this script.
  276. addOptions();
  277.  
  278. // Watch for the "waiting" spinner so we can disable and enable things while LM is fetching routes.
  279. let observer = new MutationObserver(mutations => {
  280. mutations.forEach(mutation => {
  281. if (mutation.attributeName === 'class') {
  282. let waitingSpinner = !$(mutation.target).hasClass('wm-hidden');
  283. $('.lmo-control').prop('disabled', waitingSpinner);
  284. if (!waitingSpinner) {
  285. W.app.map.fitBounds = _fitBounds;
  286. }
  287. }
  288. });
  289. });
  290. observer.observe($('.wm-route-search__spinner')[0], { childList: false, subtree: false, attributes: true });
  291.  
  292.  
  293. // Watch for routes being displayed, so we can update the displayed times.
  294. observer = new MutationObserver(mutations => {
  295. mutations.forEach(mutation => {
  296. for (var i = 0; i < mutation.addedNodes.length; i++) {
  297. let addedNode = mutation.addedNodes[i];
  298. if (addedNode.nodeType === Node.ELEMENT_NODE && $(addedNode).hasClass('wm-route-list__routes')) {
  299. updateTimes();
  300. }
  301. }
  302. });
  303. });
  304. observer.observe($('.wm-route-list')[0], { childList: true, subtree: true });
  305.  
  306. // Remove the div that contains the native LiveMap options for setting departure time.
  307. $('div.wm-route-schedule').remove();
  308.  
  309. // Remove the routing tip (save some space).
  310. $('div.wm-routing__tip').remove();
  311.  
  312. // Store the fitBounds function. It is removed and re-added, to prevent the
  313. // LiveMap api from moving the map to the boundaries of the routes every time
  314. // an option is checked.
  315. _fitBounds = W.app.map.fitBounds;
  316. }
  317.  
  318. // Run the script.
  319. init();
  320. })();