Waze LiveMap Options

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

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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) );
})();