/* global $ */
// ==UserScript==
// @name Restriction Manager
// @version 0.2
// @description Save, and load, restrictions from local storage.
// @namespace mailto:[email protected]
// @include https://www.waze.com/editor*
// @include https://www.waze.com/*/editor*
// @include https://beta.waze.com/*
// @exclude https://www.waze.com/user/*
// @exclude https://www.waze.com/*/user/*
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAjCAIAAABzZz93AAAABnRSTlMA/wD/AP83WBt9AAAACXBIWXMAAA7EAAAOxAGVKw4bAAABV0lEQVRIiWP8//8/A70ACxl6vm6JgjC4fZaRpJGRVJ/BbYID4q1kotAmkgBpllHoAhIso9BbJFiG3yYi3UFyarx39rRewx0IO6nBZ5IxL/F6ifIZisOffoAzrzzFrub/v7/kW0YqYGRiJtMy4tMFQZUELKM8BZJgGSZQkhbAI4vfcfgsI+gtHWksSRGPLpokEJItoyS2cOnFbhl10wUByygHWJ2LxTIC3pLiMyPXBeiWkRKAIupS+KQxjaJtakSzjwmPHA7AqwNlCKiSaDdKG4RGiRDeSEH4jEY2IQMmkm16fjOvdDmP73Ie391TnxOlA244yQnk3ulH825AmG/K1z0jSS8TA4kBeOfxG5IsgACIFST7TEVWhAzLIID0+sxULkkDwhTpDMKbqzEA4////+mQDhkYGLh9ljExkN4/IM8mhoGpPGnqObjh6F0m6sYfmh9I7p9RAgAFyXyizju5WQAAAABJRU5ErkJggg==
// @resource jqUI_CSS https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css
// @grant none
// @copyright 2018, kjg53
// @license GNU GPL v3
// @author kjg53
// ==/UserScript==
(function() {
var initialized = false;
var lsPrefix = "rtmgr:";
// Map the css class used to identify the three restriction blocks to the direction constants used in the data.
var classToDirection={"forward-restrictions-summary": "FWD",
"reverse-restrictions-summary": "REV",
"bidi-restrictions-summary": "BOTH"};
// Convert the segment's default type to the driving modality that implied the type. Creating a Toll Free restriction
// implies that the segment is otherwise (i.e. defaults) to tolled which is what is then stored in the model.
// Finding the tolled default thereby implies that the current restriction is specifying a toll free rule.
var defaultType2drivingModality = {"TOLL": "DRIVING_TOLL_FREE",
"FREE":"DRIVING_BLOCKED",
"BLOCKED":"DRIVING_ALLOWED"};
// Map the single bit constants used in the weekdays property to the integer numbers encoded in each week days HTML display.
var weekdayBit2Idx = {
1:1,
2:2,
4:3,
8:4,
16:5,
32:6,
64:0
};
// Get a sorted list of saved restrictions found in local storage.
function allSavedRestrictions() {
var all = [];
for(var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (key.indexOf(lsPrefix) == 0) {
key = key.substring(lsPrefix.length);
all.push(key);
}
}
all.sort();
return all;
}
// Convert list of saved restrictions into a string of HTML option elements.
function allSavedRestrictionAsOptions() {
var all = allSavedRestrictions();
return all.length == 0 ? "" : "<option> /<option><option>" + all.join("</option><option>") + "</option>";
}
// Update all restriction selectors to display the saved restrictions returned by allSavedRestrictions
function updateSavedRestrictionSelectors(root) {
$("div.rtmgr div.name select", root).html(allSavedRestrictionAsOptions()).each(resizeDivName);
}
// The content of the div.name element are positioned relative to its location. As a result, the
// div normally collapses to a point in the screen layout. This function expands the div to enclose
// its contents such that other elements are laid out around them.
function resizeDivName(idx, child) {
var div = $(child).parents("div.name").first();
var height = 0;
var width = 0;
div.children().each(function(idx, child) {
child = $(child);
height = Math.max(height, child.height());
width = Math.max(width, child.width());
});
div.width(width).height(height);
}
// Identify the direction of the restrictions associated with the specified button.
function direction(btn) {
var classes = btn.parents("div.restriction-summary-group").attr('class').split(' ');
while(classes.length) {
var cls = classes.pop();
var dir = classToDirection[cls];
if (dir) {
return dir;
}
}
}
function setValue(selector, model, value) {
if (value != null) {
var sel = $(selector, model);
var oldValue = sel.val();
if (oldValue != value) {
sel.val(value);
sel.change();
}
}
}
function setCheck(selector, model, value) {
if (value != null) {
value = !!value;
var sel = $(selector, model);
var oldValue = sel.prop('checked');
if (oldValue != value) {
sel.prop('checked', value);
sel.change();
}
}
}
function setSelector(name, model, value) {
setValue('select[name="' + name + '"]', model, value);
}
function lastFaPlus(modal) {
return $("i.fa-plus", modal).last();
}
function clearMessages() {
$("div.restriction-validatation-region div.rtmgr").remove();
}
function addMessage(text) {
var rvr = $("div.restriction-validation-region");
var rvrul = $("ul", rvr);
if (rvrul.length == 0) {
rvr.append('<div><div class="restriction-validation-title">The Restrictions Manager encountered the following issue.</div><div class="collection-region"><ul></ul></div></div>');
rvrul = $("ul", rvr);
}
rvrul.append('<li class="restriction-validation-error">' + text + '</li>');
}
function initializeRestrictionManager() {
if (initialized) {
return;
}
var observerTarget = document.getElementById("dialog-region");
if (!observerTarget) {
window.console.log("Restriction Manager: waiting for WME...");
setTimeout(initializeRestrictionManager, 1015);
}
// Inject my stylesheet into the head
var sheet = $('head').append('<style type="text/css"/>').children('style').last();
sheet.append('div.rtmgr-column {display: flex; flex-direction: column}');
sheet.append('div.rtmgr-row {display: flex; flex-direction: row; justify-content: space-around}');
sheet.append('div.rtmgr btn {margin-top: 5px}');
sheet.append('div.rtmgr div.name input {width: 250px; position: absolute; left: 0px; top: 0px; z-index: 1}');
sheet.append('div.rtmgr div.name select {width: 275px; position: absolute; left: 0px; top: 0px}');
sheet.append('div.rtmgr div.name {width: 275px; position: relative; left: 0px; top: 0px}');
// create an observer instance
var observer = new MutationObserver(function(mutations) {
var si = W.selectionManager.getSelectedFeatures();
mutations.forEach(function(mutation) {
if("childList" == mutation.type && mutation.addedNodes.length) {
var restrictionsModal = $("div.modal-dialog.restrictions-modal", observerTarget);
if (restrictionsModal) {
var modalTitle = $(restrictionsModal).find("h3.modal-title").first();
var title = modalTitle.text().replace(/[\x00-\x1F\x7F-\x9F]/g, "");
if ("Time based restrictions" == title) {
if (modalTitle.data('rtmgr') === undefined) {
// Flag this modal as having already augmented
modalTitle.data('rtmgr', true);
// Add the UI elements to the modal
$("div.restriction-summary-group div.restriction-summary-title", restrictionsModal)
.append (
""
+ "<div class='rtmgr rtmgr-column'>"
+ "<div class='name'>"
+ "<input type='text'/>"
+ "<select/>"
+ "</div>"
+ "<div class='rtmgr-row'>"
+ "<button class='btn save'>Save</button>"
+ "<button class='btn apply'>Apply</button>"
+ "<button class='btn delete'>Delete</button>"
+ "</div>"
+ "</div>");
// Initialize the saved restriction selectors
updateSavedRestrictionSelectors(restrictionsModal);
// When a selection is made copy it to the overlapping input element to make it visible.
$("div.rtmgr select").change(function(evt) {
var tgt = evt.target;
var txt = $(tgt).parent().children("input");
var text = tgt.options[tgt.selectedIndex].text;
txt.val(text);
});
// Delete action
$("div.rtmgr button.delete", restrictionsModal).click(function(evt) {
var tgt = $(evt.target);
var inp = tgt.parents('div.rtmgr').find("input");
var name = inp.val();
if (name != "") {
localStorage.removeItem(lsPrefix + name);
updateSavedRestrictionSelectors(restrictionsModal);
inp.val("");
}
});
// Save action (only one segment currently selected)
if (si.length == 1) {
$("div.rtmgr button.save", restrictionsModal).click(function(evt) {
var tgt = $(evt.target);
var name = tgt.parents('div.rtmgr').find("input").val();
if (name != "") {
var dir = direction(tgt);
var attrs = si[0].model.getAttributes();
var src = attrs.restrictions;
// Checking for pending updates to the selected segment's restrictions. If found, save a copy of them.
// 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.
for(var i = W.model.actionManager.actions.length; i-- > 0;) {
var action = W.model.actionManager.actions[i];
if (action.model.hasOwnProperty('segments') && action.subActions[0].attributes.id == si[0].model.attributes.id && action.subActions[0].newAttributes.hasOwnProperty('restrictions')) {
src = action.subActions[0].newAttributes.restrictions;
break;
}
}
var restrictions = [];
for (i = 0; i< src.length; i++) {
var restriction = src[i];
if (restriction._direction == dir) {
restrictions.push(restriction);
}
}
restrictions = JSON.stringify(restrictions);
localStorage.setItem(lsPrefix + name, restrictions);
updateSavedRestrictionSelectors(restrictionsModal);
}
});
} else {
$("div.rtmgr button.save", restrictionsModal).click(function(evt) {
clearMessages();
addMessage("Save is only enabled when displaying the restrictions for a SINGLE segment");
});
}
// Apply saved restrictions to the current segment
$("div.rtmgr button.apply", restrictionsModal).click(function(evt) {
var tgt = $(evt.target);
var name = tgt.parents('div.rtmgr').find("input").val();
if (name != "") {
var restrictions = localStorage.getItem(lsPrefix + name);
restrictions = JSON.parse(restrictions);
var rsg = $(evt.target).parents("div.restriction-summary-group").first();
var classes = rsg.attr('class').split(' ');
classes.splice(classes.indexOf('restriction-summary-group'), 1);
// Delete all current restrictions associated with the action's direction
while (true) {
var doDelete = "." + classes[0] + " .restriction-editing-actions i.do-delete";
var deleteRestrictions = $(doDelete, restrictionsModal);
if (deleteRestrictions.length == 0) {
break;
}
deleteRestrictions.eq(0).click();
}
// Create new restrictions
while (restrictions.length) {
var restriction = restrictions.shift();
$("." + classes[0] + " button.do-create", restrictionsModal).click();
setSelector('disposition', restrictionsModal, restriction.disposition);
setSelector('laneType', restrictionsModal, restriction.laneType);
setValue('textarea[name="description"]', restrictionsModal, restriction.description);
if (restriction.timeFrames != null && restriction.timeFrames.length != 0) {
var weekdays = restriction.timeFrames[0].weekdays;
var bit = 1;
for(var idx = 0; idx < 7; idx++) {
var set = weekdays & bit;
set = (set != 0);
setCheck('input#day-ordinal-' + weekdayBit2Idx[bit] + '-checkbox', restrictionsModal, set);
bit <<= 1;
}
if (restriction.timeFrames[0].fromTime && restriction.timeFrames[0].toTime) {
setCheck("input#is-all-day-checkbox", restrictionsModal, false);
setValue("input.timepicker-from-time", restrictionsModal, restriction.timeFrames[0].fromTime);
setValue("input.timepicker-to-time", restrictionsModal, restriction.timeFrames[0].toTime);
}
if (restriction.timeFrames[0].startDate && restriction.timeFrames[0].endDate) {
setCheck("input#is-during-dates-on-radio", restrictionsModal, true);
// Ref: http://www.daterangepicker.com/
var drp = $('input.btn.datepicker', restrictionsModal).data('daterangepicker');
var re = /(\d{4})-(\d{2})-(\d{2})/;
var match = re.exec(restriction.timeFrames[0].startDate);
var startDate = match[2] + "/" + match[3] + "/" + match[1];
match = re.exec(restriction.timeFrames[0].endDate);
var endDate = match[2] + "/" + match[3] + "/" + match[1];
// WME's callback is fired by drp.hide().
drp.show();
drp.setStartDate(startDate);
drp.setEndDate(endDate);
drp.hide();
}
}
var drivingModality;
// if ALL vehicles are blocked then the default type is simply BLOCKED and the modality is blocked.
if ("BLOCKED" == restriction.defaultType && !restriction.driveProfiles.hasOwnProperty("FREE") && !restriction.driveProfiles.hasOwnProperty("BLOCKED")) {
drivingModality = "DRIVING_BLOCKED";
} else {
drivingModality = defaultType2drivingModality[restriction.defaultType];
}
setValue("select.do-change-driving-modality", restrictionsModal, drivingModality);
var driveProfiles, driveProfile, i, j, vehicleType;
if (restriction.driveProfiles.hasOwnProperty("FREE")) {
driveProfiles = restriction.driveProfiles.FREE;
for(i = 0; i < driveProfiles.length; i++) {
driveProfile = driveProfiles[i];
$("div.add-drive-profile-item.do-add-item", restrictionsModal).click();
for(j = 0; j < driveProfile.vehicleTypes.length; j++) {
vehicleType = driveProfile.vehicleTypes[j];
var plus = lastFaPlus(restrictionsModal);
plus.click();
var driveProfileItem = plus.parents("div.drive-profile-item");
$("div.btn-group.open a.do-init-vehicle-type", dpi).click();
$("div.vehicle-type span.restriction-chip-content", driveProfileItem).click();
$('a.do-set-vehicle-type[data-value="' + vehicleType + '"]', driveProfileItem).click();
}
if (driveProfile.numPassengers > 0) {
var plus2 = lastFaPlus(restrictionsModal);
plus2.click();
var dpi = plus2.parents("div.drive-profile-item");
$("div.btn-group.open a.do-init-num-passengers", dpi).click();
if (driveProfile.numPassengers > 2) {
$("a.do-set-num-passengers[data-value='" + driveProfile.numPassengers + "']").click();
}
}
for(var k = 0; k < driveProfile.subscriptions.length; k++) {
var subscription = driveProfile.subscriptions[k];
var plus3 = lastFaPlus(restrictionsModal);
plus3.click();
var driveProfileItem2 = plus3.parents("div.drive-profile-item");
$("div.btn-group.open a.do-init-subscription", driveProfileItem2).click();
$("div.subscription span.restriction-chip-content", driveProfileItem2).click();
$('a.do-set-subscription[data-value="' + subscription + '"]', driveProfileItem2).click();
}
}
} else if (restriction.driveProfiles.hasOwnProperty("BLOCKED")) {
driveProfiles = restriction.driveProfiles.BLOCKED;
for(i = 0; i < driveProfiles.length; i++) {
driveProfile = driveProfiles[i];
if (driveProfile.vehicleTypes.length > 0) {
setCheck('input#all-vehicles-off-radio', restrictionsModal, true);
for(j = 0; j < driveProfile.vehicleTypes.length; j++) {
vehicleType = driveProfile.vehicleTypes[j];
setCheck('input#vehicle-type-' + vehicleType + '-checkbox', restrictionsModal, true);
}
}
}
}
$("div.modal-footer button.do-create", restrictionsModal).click();
}
}
});
}
}
}
}
});
});
// configuration of the observer:
var config = { attributes: false, childList: true, characterData: false, subtree: true };
// pass in the target node, as well as the observer options
observer.observe(observerTarget, config);
initialized = true;
}
setTimeout(initializeRestrictionManager, 1000);
})();