Waze LiveMap Options

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

目前為 2018-05-02 提交的版本,檢視 最新版本

// ==UserScript==
// @name        Waze LiveMap Options
// @namespace   WazeDev
// @version     2017.12.22.002
// @description Adds options to LiveMap to alter the Waze-suggested routes.
// @author      MapOMatic
// @include     /^https:\/\/www.waze.com\/livemap/
// @license     GNU GPL v3
// ==/UserScript==

/* global W */
/* global Node */

(function() {
    'use strict';
    var EXPANDED_MAX_HEIGHT = '200px';

    var _settings = {
        'lmo-tolls': {checked:false},
        'lmo-freeways': {checked:false},
        'lmo-ferries': {checked:false},
        'lmo-difficult-turns':{checked:false},
        'lmo-unpaved-roads': {checked:true},
        'lmo-long-unpaved-roads': {checked:false},
        'lmo-u-turns':{checked:false, opposite:true},
        'lmo-hov':{checked:false, opposite:true},
        'lmo-real-time-traffic':{checked:true},
        'lmo-hide-traffic':{checked:false},
        'lmo-day': 'today',
        'lmo-hour': 'now',
        collapsed: false
    };
    // Store the onAfterItemAdded function.  It is removed and re-added, to prevent the
    // LiveMap api from moving the map to the boundaries of the routes every time
    // an option is checked.
    var _onAfterItemAdded =  W.controller._routePaths.onAfterItemAdded;

    function checked(id, optionalSetTo) {
        var $elem = $('#' + id);
        if (typeof optionalSetTo !== 'undefined') {
            $elem.prop('checked', optionalSetTo);
        } else {
            return $elem.prop('checked');
        }
    }

    function getDateTimeOffset() {
        var hour = $('#lmo-hour').val();
        var day  = $('#lmo-day').val();
        if (hour === '---') hour = 'now';
        if (day  === '---') day = 'today';
        if (hour === '') hour = 'now';
        if (day  === '') day = 'today';

        var t = new Date();
        var thour = (t.getHours() * 60) + t.getMinutes();
        var tnow = (t.getDay() * 1440) + thour;
        var tsel = tnow;

        if (hour === 'now') {
            if (day === "0")     tsel = (parseInt(day) * 1440) + thour;
            if (day === "1")     tsel = (parseInt(day) * 1440) + thour;
            if (day === "2")     tsel = (parseInt(day) * 1440) + thour;
            if (day === "3")     tsel = (parseInt(day) * 1440) + thour;
            if (day === "4")     tsel = (parseInt(day) * 1440) + thour;
            if (day === "5")     tsel = (parseInt(day) * 1440) + thour;
            if (day === "6")     tsel = (parseInt(day) * 1440) + thour;
        } else {
            if (day === "today") tsel = (t.getDay()    * 1440) + parseInt(hour);
            if (day === "0")     tsel = (parseInt(day) * 1440) + parseInt(hour);
            if (day === "1")     tsel = (parseInt(day) * 1440) + parseInt(hour);
            if (day === "2")     tsel = (parseInt(day) * 1440) + parseInt(hour);
            if (day === "3")     tsel = (parseInt(day) * 1440) + parseInt(hour);
            if (day === "4")     tsel = (parseInt(day) * 1440) + parseInt(hour);
            if (day === "5")     tsel = (parseInt(day) * 1440) + parseInt(hour);
            if (day === "6")     tsel = (parseInt(day) * 1440) + parseInt(hour);
        }

        var diff = tsel - tnow;
        if (diff < -3.5 * 1440)
            diff += 7 * 1440;
        else if (diff > 3.5 * 1440)
            diff -= 7 * 1440;

        return diff;
    }

    function updateTimes() {
        var realTimeTraffic = checked('lmo-real-time-traffic');
        var $routeTimes = $('.routes .route-time');
        for (var idx=0; idx<$routeTimes.length; idx++) {
            var time = getRouteTime(idx, realTimeTraffic);
            var $routeTime = $routeTimes.eq(idx);
            var contents = $routeTime.contents();
            contents[contents.length-1].remove();
            $routeTime.append(' ' + time);
        }
    }

    function fetchRoutes() {
        var routeSearch = W.controller._routeSearch;
        if (routeSearch.to.address.attributes.latlng && routeSearch.from.address.attributes.latlng) {//($('div#origin input.query').val() && $('div#destination input.query').val()) {
            $('.lmo-control').prop('disabled',true);
            // HACK - Temporarily remove the onAfterItemAdded function, to prevent map from moving.
            W.controller._routePaths.onAfterItemAdded = null;
            routeSearch.fetchRoutes();
        }
    }

    function addOptions() {
        if (!$('#lmo-table').length) {
            $('.search-forms').after(
                $('<div>', {class:'lmo-options-header'}).append(
                    $('<span>').text('Change routing options'),
                    $('<i>', {class:'fa fa.fa-angle-down fa.fa-angle-up'}).addClass(_settings.collapsed ? 'fa-angle-down' : 'fa-angle-up')
                ),
                $('<div>', {class: 'lmo-options-container'}).css({maxHeight:_settings.collapsed ? '0px' : EXPANDED_MAX_HEIGHT}).append(
                    $('<table>', {class: 'lmo-table'}).append(
                        [['Avoid:',['Tolls','Freeways','Ferries','HOV','Unpaved roads','Long unpaved roads','Difficult turns','U-Turns']], ['Options:',['Real-time traffic','Hide traffic']]].map(rowItems => {
                            var rowID = rowItems[0].toLowerCase().replace(/[ :]/g,'');
                            return $('<tr>', {id:'lmo-row-' + rowID}).append(
                                $('<td>').append($('<span>', {id:'lmo-header-' + rowID, class:'lmo-table-header-text'}).text(rowItems[0])),
                                $('<td>', {class: 'lmo-settings-cell'}).append(
                                    rowItems[1].map((text) => {
                                        var idName = text.toLowerCase().replace(/ /g, '-');
                                        var id = 'lmo-' + idName;
                                        return $('<span>', {class:'lmo-control-container'}).append(
                                            $('<input>', {id:id, type:'checkbox', class:'lmo-control'}).prop('checked',_settings[id].checked), $('<label>', {for:id}).text(text)
                                        );
                                    })
                                )
                            );
                        })
                    )
                )
            );
            $('#lmo-header-avoid').css({color:'#c55'});
            $('label[for="lmo-u-turns"').attr('title','Note: this is not an available setting in the app');

            var timeArray = [['Now','now']];
            for (var i=0; i<48; i++) {
                var t = i * 30;
                var min = t % 60;
                var hr = Math.floor(t / 60);
                var str = (hr < 10 ? ('0') : '') + hr + ':' + (min === 0 ? '00' : min);
                timeArray.push([str, t.toString()]);
            }
            $('#lmo-row-options td.lmo-settings-cell').append(
                $('<div>').append(
                    $('<label>', {for:'lmo-day', style:'font-weight: normal;'}).text('Day'),
                    $('<select>', {id: 'lmo-day', class:'lmo-control', style:'margin-left: 4px; margin-right: 8px; padding: 0px; height: 22px;'}).append(
                        [
                            ['Today','today'],
                            ['Monday','1'],
                            ['Tuesday','2'],
                            ['Wednesday','3'],
                            ['Thursday','4'],
                            ['Friday','5'],
                            ['Saturday','6'],
                            ['Sunday','0']
                        ].map(val => $('<option>', {value:val[1]}).text(val[0]))
                    ),
                    $('<label>', {for:'lmo-hour', style:'font-weight: normal;'}).text('Time'),
                    $('<select>', {id: 'lmo-hour', class:'lmo-control', style:'margin-left: 4px; margin-right: 8px; padding: 0px; height: 22px;'}).append(
                        timeArray.map(val => $('<option>', {value:val[1]}).text(val[0]))
                    )
                )
            );

            // Set up events
            $('.lmo-options-header').click(function() {
                var $container = $('.lmo-options-container');
                var collapsed = $container.css('max-height') === '0px';
                $('.lmo-options-header i').removeClass(collapsed ? 'fa-angle-down' : 'fa-angle-up').addClass(collapsed ? 'fa-angle-up' : 'fa-angle-down');
                $container.css({maxHeight: collapsed ? EXPANDED_MAX_HEIGHT : '0px'});
                _settings.collapsed = !collapsed;
            });
            $('.lmo-control').change(function() {
                var id = this.id;
                if (id === 'lmo-hour' || id === 'lmo-day') {
                    fetchRoutes();
                } else {
                    var isChecked = checked(id);
                    _settings[id].checked = isChecked;
                    if (id === 'lmo-real-time-traffic') {
                        updateTimes();
                    } else if (id === 'lmo-hide-traffic') {
                        W.controller._mapModel.features.enableJams(!isChecked);
                    } else {
                        if (id === 'lmo-long-unpaved-roads') {
                            if (isChecked) {
                                checked('lmo-unpaved-roads', false);
                                _settings['lmo-unpaved-roads'].checked = false;
                            }
                        } else if (id === 'lmo-unpaved-roads') {
                            if (isChecked) {
                                checked('lmo-long-unpaved-roads', false);
                                _settings['lmo-long-unpaved-roads'].checked = false;
                            }
                        }
                        fetchRoutes();
                    }
                }
            });
        }
    }

    var observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            for (var i = 0; i < mutation.addedNodes.length; i++) {
                var addedNode = mutation.addedNodes[i];
                if (addedNode.nodeType === Node.ELEMENT_NODE){
                    var $addedNode = $(addedNode);
                    if (addedNode.className === 'search-forms') {
                        addOptions();
                    } else if ($addedNode.hasClass('route-info')) {
                        updateTimes();
                    } else if ($addedNode.is('div.routing-time')) {
                        $addedNode.remove();
                    }
                }
            }
            mutation.removedNodes.forEach(nd => {
                if ($(nd).hasClass('s-loading')) {
                    $('.lmo-control').prop("disabled", false);
                    W.controller._routePaths.onAfterItemAdded = _onAfterItemAdded;
                }
            });
        });
    });
    observer.observe($('.leaflet-top')[0], { childList: true, subtree: true });

    $('div.routing-time').remove();
    addOptions();

    function getRouteTime(routeIdx, realTimeTraffic) {
        var sec = 0;
        W.controller._routes.models[routeIdx].attributes.results.forEach(result => {
            sec += realTimeTraffic ? result.crossTime : result.crossTimeWithoutRealTime;
        });
        var hours = Math.floor(sec/3600);
        sec -= hours * 3600;
        var min = Math.floor(sec/60);
        sec -= min * 60;
        return (hours > 0 ? hours + 'h ' : '') + (min > 0 ? min + 'm ' : '') + sec + 's';
    }

    $.ajaxPrefilter(function(request, originalRequest, jqXHR) {
        if (originalRequest.url === '/RoutingManager/routingRequest') {
            // Remove all options from the request (everything after '&options=')
            var baseData = request.data.replace(request.data.match(/&options=(.*)/)[1],'');
            var options = [];
            [['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 => {
                var id = 'lmo-' + optionInfo[0];
                var enableOption = checked(id);
                if (_settings[id].opposite) enableOption = !enableOption;
                options.push(optionInfo[1] + ':' + (enableOption ? 't' : 'f'));
            });
            if (checked('lmo-long-unpaved-roads')) {
                options.push('AVOID_LONG_TRAILS:t');
            } else if (checked('lmo-unpaved-roads')) {
                options.push('AVOID_TRAILS:t');
            } else {
                options.push('AVOID_LONG_TRAILS:f');
            }
            baseData = baseData.replace(/&at=0/,'&at=' + getDateTimeOffset());
            request.data = baseData + encodeURIComponent(options.join(','));
        }
    });

    var transTime = '0.2s';
    var css = [
        '.lmo-options-header { margin-top: 4px; cursor: pointer; color: #59899e; font-size: 11px; font-weight: 600; }',
        '.lmo-options-header i { margin-left: 5px; }',
        '.lmo-options-container { max-height: 500px; overflow: hidden; transition: max-height ' + transTime + '; -moz-transition: max-height ' + transTime + '; -webkit-transition: max-height ' + transTime + '; -o-transition: max-height ' + transTime + '; }',
        '.lmo-table { margin-top: 4px; font-size: 12px; }',
        '.lmo-table td { padding: 4px 10px 0px 10px; }',
        '.lmo-table-header-text { margin: 0px; font-weight: 600; }',
        '.lmo-control-container { margin-right: 8px; }'
    ].join('\n');
    $('head').append( $('<style>', {type:'text/css'}).html(css) );
})();