Restriction Manager

Save, and load, restrictions from local storage.

当前为 2018-06-30 提交的版本,查看 最新版本

  1. /* global $ */
  2.  
  3. // ==UserScript==
  4. // @name Restriction Manager
  5. // @version 0.4
  6. // @description Save, and load, restrictions from local storage.
  7. // @namespace mailto:waze.kjg53@gmail.com
  8. // @include https://www.waze.com/editor*
  9. // @include https://www.waze.com/*/editor*
  10. // @include https://beta.waze.com/*
  11. // @exclude https://www.waze.com/user/*
  12. // @exclude https://www.waze.com/*/user/*
  13. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAjCAIAAABzZz93AAAABnRSTlMA/wD/AP83WBt9AAAACXBIWXMAAA7EAAAOxAGVKw4bAAABV0lEQVRIiWP8//8/A70ACxl6vm6JgjC4fZaRpJGRVJ/BbYID4q1kotAmkgBpllHoAhIso9BbJFiG3yYi3UFyarx39rRewx0IO6nBZ5IxL/F6ifIZisOffoAzrzzFrub/v7/kW0YqYGRiJtMy4tMFQZUELKM8BZJgGSZQkhbAI4vfcfgsI+gtHWksSRGPLpokEJItoyS2cOnFbhl10wUByygHWJ2LxTIC3pLiMyPXBeiWkRKAIupS+KQxjaJtakSzjwmPHA7AqwNlCKiSaDdKG4RGiRDeSEH4jEY2IQMmkm16fjOvdDmP73Ie391TnxOlA244yQnk3ulH825AmG/K1z0jSS8TA4kBeOfxG5IsgACIFST7TEVWhAzLIID0+sxULkkDwhTpDMKbqzEA4////+mQDhkYGLh9ljExkN4/IM8mhoGpPGnqObjh6F0m6sYfmh9I7p9RAgAFyXyizju5WQAAAABJRU5ErkJggg==
  14. // @resource jqUI_CSS https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css
  15. // @grant none
  16. // @copyright 2018, kjg53
  17. // @license GNU GPL v3
  18. // @author kjg53
  19. // ==/UserScript==
  20.  
  21. (function() {
  22. var initialized = false;
  23. var lsPrefix = "rtmgr:";
  24.  
  25. // Map the css class used to identify the three restriction blocks to the direction constants used in the data.
  26. var classToDirection={"forward-restrictions-summary": "FWD",
  27. "reverse-restrictions-summary": "REV",
  28. "bidi-restrictions-summary": "BOTH"};
  29.  
  30. // Convert the segment's default type to the driving modality that implied the type. Creating a Toll Free restriction
  31. // implies that the segment is otherwise (i.e. defaults) to tolled which is what is then stored in the model.
  32. // Finding the tolled default thereby implies that the current restriction is specifying a toll free rule.
  33. var defaultType2drivingModality = {"TOLL": "DRIVING_TOLL_FREE",
  34. "FREE":"DRIVING_BLOCKED",
  35. "BLOCKED":"DRIVING_ALLOWED"};
  36.  
  37. // Map the single bit constants used in the weekdays property to the integer numbers encoded in each week days HTML display.
  38. var weekdayBit2Idx = {
  39. 1:1,
  40. 2:2,
  41. 4:3,
  42. 8:4,
  43. 16:5,
  44. 32:6,
  45. 64:0
  46. };
  47.  
  48. var clipboardTarget = "*Clipboard*";
  49.  
  50. // Get a sorted list of saved restrictions found in local storage.
  51. function allSavedRestrictions() {
  52. var all = [];
  53. for(var i = 0; i < localStorage.length; i++) {
  54. var key = localStorage.key(i);
  55.  
  56. if (key.indexOf(lsPrefix) == 0) {
  57. key = key.substring(lsPrefix.length);
  58.  
  59. all.push(key);
  60. }
  61. }
  62. all.sort();
  63.  
  64. return all;
  65. }
  66.  
  67. // Convert list of saved restrictions into a string of HTML option elements.
  68. function allSavedRestrictionAsOptions() {
  69. var all = allSavedRestrictions();
  70. return all.length == 0 ? "" : "<option>" + all.join("</option><option>") + "</option>";
  71. }
  72.  
  73. // Update all restriction selectors to display the saved restrictions returned by allSavedRestrictions
  74. function updateSavedRestrictionSelectors(root) {
  75. $("div.rtmgr div.name select", root).html(allSavedRestrictionAsOptions()).each(resizeDivName);
  76. }
  77.  
  78. // The content of the div.name element are positioned relative to its location. As a result, the
  79. // div normally collapses to a point in the screen layout. This function expands the div to enclose
  80. // its contents such that other elements are laid out around them.
  81. function resizeDivName(idx, child) {
  82. var div = $(child).parents("div.name").first();
  83. var height = 0;
  84. var width = 0;
  85.  
  86. div.children().each(function(idx, child) {
  87. child = $(child);
  88. height = Math.max(height, child.height());
  89. width = Math.max(width, child.width());
  90. });
  91.  
  92. div.width(width).height(height);
  93. }
  94.  
  95.  
  96. // Identify the direction of the restrictions associated with the specified button.
  97. function direction(btn) {
  98. var classes = btn.parents("div.restriction-summary-group").attr('class').split(' ');
  99.  
  100. while(classes.length) {
  101. var cls = classes.pop();
  102. var dir = classToDirection[cls];
  103.  
  104. if (dir) {
  105. return dir;
  106. }
  107. }
  108. }
  109.  
  110.  
  111. function setValue(selector, model, value) {
  112. if (value != null) {
  113. var sel = $(selector, model);
  114. var oldValue = sel.val();
  115. if (oldValue != value) {
  116. sel.val(value);
  117. sel.change();
  118. }
  119. }
  120. }
  121.  
  122. function setCheck(selector, model, value) {
  123. if (value != null) {
  124. value = !!value;
  125. var sel = $(selector, model);
  126. var oldValue = sel.prop('checked');
  127. if (oldValue != value) {
  128. sel.prop('checked', value);
  129. sel.change();
  130. }
  131. }
  132. }
  133.  
  134. function setSelector(name, model, value) {
  135. setValue('select[name="' + name + '"]', model, value);
  136. }
  137.  
  138. function lastFaPlus(modal) {
  139. return $("i.fa-plus", modal).last();
  140. }
  141.  
  142. function clearMessages() {
  143. $("div.restriction-validatation-region div.rtmgr").remove();
  144. }
  145. function addMessage(text) {
  146. var rvr = $("div.restriction-validation-region");
  147. var rvrul = $("ul", rvr);
  148. if (rvrul.length == 0) {
  149. rvr.append('<div><div class="restriction-validation-title">The Restrictions Manager encountered the following issue.</div><div class="collection-region"><ul></ul></div></div>');
  150. rvrul = $("ul", rvr);
  151. }
  152. rvrul.append('<li class="restriction-validation-error">' + text + '</li>');
  153. }
  154.  
  155. var timeRegexp = /(\d\d?):(\d\d?)/
  156.  
  157. function time2Int(time) {
  158. var m = timeRegexp.exec(time);
  159. return m == null ? 0 : (m[1] * 60) + m[2];
  160. }
  161.  
  162. function compareTimeFrames(a, b) {
  163. if (a == null) {
  164. return (b == null ? 0 : -1);
  165. } else if (b == null) {
  166. return 1;
  167. } else {
  168. a = a[0];
  169. b = b[0];
  170.  
  171. var c = time2Int(a.fromTime) - time2Int(b.fromTime);
  172. if (c == 0) {
  173. c = time2Int(a.toTime) - time2Int(b.toTime);
  174. }
  175.  
  176. return c;
  177. }
  178. }
  179.  
  180. function compareRestrictions(a, b) {
  181. var c = compareTimeFrames(a.timeFrames, b.timeFrames);
  182. if (c == 0) {
  183. c = a.defaultType.localeCompare(b);
  184. }
  185. return c;
  186. }
  187.  
  188. function initializeRestrictionManager() {
  189. if (initialized) {
  190. return;
  191. }
  192.  
  193. var observerTarget = document.getElementById("dialog-region");
  194.  
  195. if (!observerTarget) {
  196. window.console.log("Restriction Manager: waiting for WME...");
  197. setTimeout(initializeRestrictionManager, 1015);
  198. }
  199.  
  200. // Inject my stylesheet into the head
  201. var sheet = $('head').append('<style type="text/css"/>').children('style').last();
  202. sheet.append('div.rtmgr-column {display: flex; flex-direction: column}');
  203. sheet.append('div.rtmgr-row {display: flex; flex-direction: row; justify-content: space-around}');
  204. sheet.append('div.rtmgr btn {margin-top: 5px}');
  205. sheet.append('div.rtmgr div.name input {width: 250px; position: absolute; left: 0px; top: 0px; z-index: 1}');
  206. sheet.append('div.rtmgr div.name select {width: 275px; position: absolute; left: 0px; top: 0px}');
  207. sheet.append('div.rtmgr div.name {width: 275px; position: relative; left: 0px; top: 0px}');
  208.  
  209. // create an observer instance
  210. var observer = new MutationObserver(function(mutations) {
  211. var si = W.selectionManager.getSelectedFeatures();
  212.  
  213. mutations.forEach(function(mutation) {
  214. if("childList" == mutation.type && mutation.addedNodes.length) {
  215. var restrictionsModal = $("div.modal-dialog.restrictions-modal", observerTarget);
  216.  
  217. if (restrictionsModal) {
  218. var modalTitle = $(restrictionsModal).find("h3.modal-title").first();
  219. var title = modalTitle.text().replace(/[\x00-\x1F\x7F-\x9F]/g, "");
  220.  
  221. if ("Time based restrictions" == title) {
  222. if (modalTitle.data('rtmgr') === undefined) {
  223. // Flag this modal as having already augmented
  224. modalTitle.data('rtmgr', true);
  225.  
  226. // Add the UI elements to the modal
  227. $("div.restriction-summary-group div.restriction-summary-title", restrictionsModal)
  228. .append (
  229. ""
  230. + "<div class='rtmgr rtmgr-column'>"
  231. + "<div class='name'>"
  232. + "<input type='text'/>"
  233. + "<select/>"
  234. + "</div>"
  235. + "<div class='rtmgr-row'>"
  236. + "<button class='btn save'>Save</button>"
  237. + "<button class='btn apply'>Apply</button>"
  238. + "<button class='btn delete'>Delete</button>"
  239. + "</div>"
  240. + "</div>");
  241.  
  242. // Initialize the saved restriction selectors
  243. updateSavedRestrictionSelectors(restrictionsModal);
  244.  
  245. // When a selection is made copy it to the overlapping input element to make it visible.
  246. $("div.rtmgr select").change(function(evt) {
  247. var tgt = evt.target;
  248. var txt = $(tgt).parent().children("input");
  249. var text = tgt.options[tgt.selectedIndex].text;
  250. txt.val(text);
  251. });
  252.  
  253. // Delete action
  254. $("div.rtmgr button.delete", restrictionsModal).click(function(evt) {
  255. var tgt = $(evt.target);
  256. var inp = tgt.parents('div.rtmgr').find("input");
  257. var name = inp.val();
  258. if (name != "") {
  259. localStorage.removeItem(lsPrefix + name);
  260. updateSavedRestrictionSelectors(restrictionsModal);
  261. inp.val("");
  262. }
  263. });
  264.  
  265. // Save action (only one segment currently selected)
  266. if (si.length == 1) {
  267. $("div.rtmgr button.save", restrictionsModal).click(function(evt) {
  268. var tgt = $(evt.target);
  269. var input = tgt.parents('div.rtmgr').find("input");
  270. var name = input.val();
  271. if (name != "") {
  272. var dir = direction(tgt);
  273. var attrs = si[0].model.getAttributes();
  274. var src = attrs.restrictions;
  275.  
  276. // Checking for pending updates to the selected segment's restrictions. If found, save a copy of them.
  277. // This is a convenience feature that enables an editor to Apply a restriction change to a segment and then store it for re-use without first having to save it on the original segment.
  278. for(var i = W.model.actionManager.actions.length; i-- > 0;) {
  279. var action = W.model.actionManager.actions[i];
  280. if (action.model.hasOwnProperty('subActions') && action.subActions[0].attributes.id == si[0].model.attributes.id && action.subActions[0].newAttributes.hasOwnProperty('restrictions')) {
  281. src = action.subActions[0].newAttributes.restrictions;
  282. break;
  283. }
  284. }
  285.  
  286. var restrictions = [];
  287. for (i = 0; i< src.length; i++) {
  288. var restriction = src[i];
  289. if (restriction._direction == dir) {
  290. restrictions.push(restriction);
  291. }
  292. }
  293.  
  294. restrictions = JSON.stringify(restrictions);
  295. if (clipboardTarget == name) {
  296. input.val(restrictions).select();
  297. document.execCommand('copy');
  298. input.val(clipboardTarget).blur();
  299. } else {
  300. localStorage.setItem(lsPrefix + name, restrictions);
  301. }
  302. updateSavedRestrictionSelectors(restrictionsModal);
  303. }
  304. });
  305. } else {
  306. $("div.rtmgr button.save", restrictionsModal).click(function(evt) {
  307. clearMessages();
  308. addMessage("Save is only enabled when displaying the restrictions for a SINGLE segment");
  309. });
  310. }
  311.  
  312. // Apply saved restrictions to the current segment
  313. $("div.rtmgr button.apply", restrictionsModal).click(function(evt) {
  314. var tgt = $(evt.target);
  315. var input = tgt.parents('div.rtmgr').find("input");
  316. var name = input.val().trim();
  317. if (name != "") {
  318. var restrictions;
  319. if (name.startsWith('[') & name.endsWith(']')) {
  320. restrictions = name;
  321. } else {
  322. restrictions = localStorage.getItem(lsPrefix + name);
  323. }
  324. restrictions = JSON.parse(restrictions).sort(compareRestrictions);
  325.  
  326. var rsg = $(evt.target).parents("div.restriction-summary-group").first();
  327. var classes = rsg.attr('class').split(' ');
  328. classes.splice(classes.indexOf('restriction-summary-group'), 1);
  329.  
  330. // Delete all current restrictions associated with the action's direction
  331. while (true) {
  332. var doDelete = "." + classes[0] + " .restriction-editing-actions i.do-delete";
  333. var deleteRestrictions = $(doDelete, restrictionsModal);
  334.  
  335. if (deleteRestrictions.length == 0) {
  336. break;
  337. }
  338.  
  339. deleteRestrictions.eq(0).click();
  340. }
  341.  
  342. // Create new restrictions
  343. while (restrictions.length) {
  344. var restriction = restrictions.shift();
  345.  
  346. $("." + classes[0] + " button.do-create", restrictionsModal).click();
  347.  
  348. setSelector('disposition', restrictionsModal, restriction.disposition);
  349. setSelector('laneType', restrictionsModal, restriction.laneType);
  350. setValue('textarea[name="description"]', restrictionsModal, restriction.description);
  351.  
  352. if (restriction.timeFrames != null && restriction.timeFrames.length != 0) {
  353. var weekdays = restriction.timeFrames[0].weekdays;
  354.  
  355. var bit = 1;
  356. for(var idx = 0; idx < 7; idx++) {
  357. var set = weekdays & bit;
  358. set = (set != 0);
  359. setCheck('input#day-ordinal-' + weekdayBit2Idx[bit] + '-checkbox', restrictionsModal, set);
  360. bit <<= 1;
  361. }
  362.  
  363. if (restriction.timeFrames[0].fromTime && restriction.timeFrames[0].toTime) {
  364. setCheck("input#is-all-day-checkbox", restrictionsModal, false);
  365. setValue("input.timepicker-from-time", restrictionsModal, restriction.timeFrames[0].fromTime);
  366. setValue("input.timepicker-to-time", restrictionsModal, restriction.timeFrames[0].toTime);
  367. }
  368.  
  369. if (restriction.timeFrames[0].startDate && restriction.timeFrames[0].endDate) {
  370. setCheck("input#is-during-dates-on-radio", restrictionsModal, true);
  371.  
  372. // Ref: http://www.daterangepicker.com/
  373. var drp = $('input.btn.datepicker', restrictionsModal).data('daterangepicker');
  374.  
  375. var re = /(\d{4})-(\d{2})-(\d{2})/;
  376. var match = re.exec(restriction.timeFrames[0].startDate);
  377. var startDate = match[2] + "/" + match[3] + "/" + match[1];
  378.  
  379. match = re.exec(restriction.timeFrames[0].endDate);
  380. var endDate = match[2] + "/" + match[3] + "/" + match[1];
  381.  
  382. // WME's callback is fired by drp.hide().
  383. drp.show();
  384. drp.setStartDate(startDate);
  385. drp.setEndDate(endDate);
  386. drp.hide();
  387. }
  388. }
  389.  
  390. var drivingModality;
  391. // if ALL vehicles are blocked then the default type is simply BLOCKED and the modality is blocked.
  392. if ("BLOCKED" == restriction.defaultType && !restriction.driveProfiles.hasOwnProperty("FREE") && !restriction.driveProfiles.hasOwnProperty("BLOCKED")) {
  393. drivingModality = "DRIVING_BLOCKED";
  394. } else {
  395. drivingModality = defaultType2drivingModality[restriction.defaultType];
  396. }
  397.  
  398. setValue("select.do-change-driving-modality", restrictionsModal, drivingModality);
  399.  
  400. var driveProfiles, driveProfile, i, j, vehicleType, plus, driveProfileItem, subscription;
  401. if (restriction.driveProfiles.hasOwnProperty("FREE")) {
  402. driveProfiles = restriction.driveProfiles.FREE;
  403. for(i = 0; i < driveProfiles.length; i++) {
  404. driveProfile = driveProfiles[i];
  405.  
  406. $("div.add-drive-profile-item.do-add-item", restrictionsModal).click();
  407.  
  408. for(j = 0; j < driveProfile.vehicleTypes.length; j++) {
  409. vehicleType = driveProfile.vehicleTypes[j];
  410.  
  411. plus = lastFaPlus(restrictionsModal);
  412. plus.click();
  413. driveProfileItem = plus.parents("div.drive-profile-item");
  414. $("div.btn-group.open a.do-init-vehicle-type", driveProfileItem).click();
  415.  
  416. $("div.vehicle-type span.restriction-chip-content", driveProfileItem).click();
  417.  
  418. $('a.do-set-vehicle-type[data-value="' + vehicleType + '"]', driveProfileItem).click();
  419. }
  420.  
  421. if (driveProfile.numPassengers > 0) {
  422. plus = lastFaPlus(restrictionsModal);
  423. plus.click();
  424. driveProfileItem = plus.parents("div.drive-profile-item");
  425. $("div.btn-group.open a.do-init-num-passengers", driveProfileItem).click();
  426.  
  427. if (driveProfile.numPassengers > 2) {
  428. $("a.do-set-num-passengers[data-value='" + driveProfile.numPassengers + "']").click();
  429. }
  430. }
  431.  
  432. for(var k = 0; k < driveProfile.subscriptions.length; k++) {
  433. subscription = driveProfile.subscriptions[k];
  434.  
  435. plus = lastFaPlus(restrictionsModal);
  436. plus.click();
  437. driveProfileItem = plus.parents("div.drive-profile-item");
  438. $("div.btn-group.open a.do-init-subscription", driveProfileItem).click();
  439.  
  440.  
  441. $("div.subscription span.restriction-chip-content", driveProfileItem).click();
  442.  
  443. $('a.do-set-subscription[data-value="' + subscription + '"]', driveProfileItem).click();
  444. }
  445. }
  446. } else if (restriction.driveProfiles.hasOwnProperty("BLOCKED")) {
  447. driveProfiles = restriction.driveProfiles.BLOCKED;
  448.  
  449. for(i = 0; i < driveProfiles.length; i++) {
  450. driveProfile = driveProfiles[i];
  451.  
  452. if (driveProfile.vehicleTypes.length > 0) {
  453. setCheck('input#all-vehicles-off-radio', restrictionsModal, true);
  454.  
  455. for(j = 0; j < driveProfile.vehicleTypes.length; j++) {
  456. vehicleType = driveProfile.vehicleTypes[j];
  457.  
  458. setCheck('input#vehicle-type-' + vehicleType + '-checkbox', restrictionsModal, true);
  459. }
  460. }
  461. }
  462. }
  463.  
  464. $("div.modal-footer button.do-create", restrictionsModal).click();
  465. }
  466. }
  467. });
  468. }
  469. }
  470. }
  471. }
  472. });
  473. });
  474.  
  475. // configuration of the observer:
  476. var config = { attributes: false, childList: true, characterData: false, subtree: true };
  477.  
  478. // pass in the target node, as well as the observer options
  479. observer.observe(observerTarget, config);
  480.  
  481. initialized = true;
  482. }
  483.  
  484. setTimeout(initializeRestrictionManager, 1000);
  485. })();