WME Show Alt Names

Shows alt names for selected segments

当前为 2015-05-16 提交的版本,查看 最新版本

/* global wLib */
/* global W */
/* global $ */
/* global jQuery */
/* global OL */

// ==UserScript==
// @name            WME Show Alt Names
// @description     Shows alt names for selected segments
// @version         0.30
// @author          SAR85
// @copyright       SAR85
// @license         CC BY-NC-ND
// @grant           none
// @include         https://www.waze.com/editor/*
// @include         https://www.waze.com/*/editor/*
// @include         https://editor-beta.waze.com/*
// @namespace       https://greasyfork.org/users/9321
// @require			https://greasyfork.org/scripts/9794-wlib/code/wLib.js?version=52323
// ==/UserScript==

var altLayer, $altDiv, betaEditor, $optionsDiv, $altTable,
	nameArray = [], alternateObjectsArray = [], selectedSegments = [];

/**
 * Returns a random color in rgba() notation.
 * @param {Number} opacity The desirected opacity (a value of the color).
 * returns {String} The rgba() color.
 */
function randomRgbaColor(opacity) {
	opacity = opacity || 0.8;
	function random255() {
		return Math.floor(Math.random() * 255);
	}
	return 'rgba(' + random255() + ',' + random255() + ',' + random255() + ',' + opacity + ')';
}
/**
 * Resets the renderIntent of all features on altLayer.
 * @param {String} intent Optional parameter specifying the intent. The default
 * parameter is 'default'.
 */
function resetRenderIntent(intent) {
    var i, n;
    intent = intent || 'default';
    for (i = 0, n = altLayer.features.length; i < n; i++) {
        altLayer.features[i].renderIntent = intent;
    }
}
/**
 * Pans the map to a segment specified by its ID.
 * @param {Number} id The ID of the segment.
 */
function panToSegment(id) {
	var segment = id && W.model.segments.get(id);
	return segment && W.map.moveTo(segment.geometry.getBounds().getCenterLonLat());
}
/**
 * Selects a segment specified by its ID.
 * @param {Number} id The ID of the segment.
 */
function selectSegment(id) {
	var seg = id && W.model.segments.get(id);
	return seg && W.selectionManager.select([seg]);
}
/** Event handler for changing the highlight color for a segment.
 * @param {Event} event
 */
function changeHighlightColor(event) {
	var i, n, $this = $(this);
	for (i = 0, n = nameArray.length; i < n; i++) {
		if (nameArray[i].name === $this.text()) {
			nameArray[i].color = randomRgbaColor();
			colorTable();
			$this.trigger('mouseenter', { singleSegment: false });
			break;
		}
	}
}
/**
 * Lookup function for the display color of a specified road type. Will return
 * the color for the experimental layer if it is activated, otherwise it will 
 * return the color for the old roads layer.
 * @param {Number} type The roadType to look up.
 * @returns {Object} Object of form {typeString: 'RoadTypeName', typeColor: '#FFFFFF'}, 
 * where RoadTypeName is an abbreviated form of the name of the road type and the type 
 * color is the hex value of the display color.
 */
function getRoadColor(type) {
    var roadTypes = {
		1: { name: 'St', color: '#FFFFFF', expColor: '#FFFFDD' },
		2: { name: 'PS', color: '#CBA12E', expColor: '#FDFAA7' },
		3: { name: 'Fwy', color: '#387FB8', expColor: '#6870C3' },
		4: { name: 'Rmp', color: '#8FB838', expColor: '#B3BFB3' },
		5: { name: 'Trl', color: '#E6E6E6', expColor: '#B0A790' },
		6: { name: 'MH', color: '#C13040', expColor: '#469FBB' },
		7: { name: 'mH', color: '#ECE589', expColor: '#69BF88' },
		8: { name: 'Dirt', color: '#E6E6E6', expColor: '#867342' },
		10: { name: 'Bdwk', color: '#E6E6E6', expColor: '#9A9A9A' },
		16: { name: 'Stwy', color: '#E6E6E6', expColor: '#9A9A9A' },
		17: { name: 'PvR', color: '#E6E6E6', expColor: '#BEBA6C' },
		18: { name: 'RR', color: '#E6E6E6', expColor: '#B2B6B4' },
		19: { name: 'Rwy', color: '#E6E6E6', expColor: '#222222' },
		20: { name: 'PLR', color: '#E6E6E6', expColor: '#ABABAB' }
		//add ferry
	},
		roadExpName = betaEditor ? 'Roads' : 'Roads experimental',
        roadsExpLayerVisible = W.map.getLayersByName(roadExpName)[0].getVisibility();
    if (type && undefined !== typeof roadTypes[type]) {
        return {
            typeString: roadTypes[type].name,
            typeColor: roadsExpLayerVisible ? roadTypes[type].expColor : roadTypes[type].color
		};
    } else {
        return { typeString: 'error', typeColor: roadTypes[1] };
    }
}
/**
 * Data structure for segment information used to build highlight layer features and
 * the alternate names table.
 * @class
 * @param {Waze.Feature.Vector.Segment} The segment feature on which to base the new instance.
 */
function Alternate(baseFeature) {
	var i, n, street, city;
	if (!baseFeature) {
		return;
	}

	this.segmentID = baseFeature.model.attributes.id;
	
	// Make a feature for highlighting on the map.
	this.layerFeature = new OL.Feature.Vector(baseFeature.geometry.clone(), baseFeature.model.attributes);
	
	// Store segment name information.
	street = W.model.streets.get(this.layerFeature.attributes.primaryStreetID);
	city = street && W.model.cities.get(street.cityID);
	this.primaryName = street ? street.name || 'No name' : 'error';
	this.primaryCity = city ? city.name || 'No city' : 'error';

	this.alternates = [];
	for (i = 0, n = this.layerFeature.attributes.streetIDs.length; i < n; i++) {
		street = W.model.streets.get(this.layerFeature.attributes.streetIDs[i]);
		city = street && W.model.cities.get(street.cityID);
		this.alternates.push({
			name: street ? street.name || 'No name' : 'error',
			city: city ? city.name || 'No city' : 'error'
		});
	}

	// Make a table row for displaying segment data.
	this.tableRow = this.createTableRow();
	
	// Add name info to attributes of layer feature and add the feature to the layer.
	// (For compatibility with highlighting functions--old method)
	this.layerFeature.attributes.alt = this.alternates;
	this.layerFeature.attributes.primary = { name: this.primaryName, city: this.primaryCity };
}
Alternate.prototype = /** @lends Alternate.prototype */ {
	createTableRow: function () {
		var i, n, $row, $cell, roadType;

		$row = $('<tr/>').attr('id', 'alt' + this.segmentID);
		
		//add road type to row
		roadType = getRoadColor(this.layerFeature.attributes.roadType);
		$cell = $('<td/>')
			.css('border-right', 'none')
			.append($('<div/>')
			.addClass('altTable-roadType')
			.css('background-color', roadType.typeColor)
			.text(roadType.typeString))
			.append($('<div/>')
			.css({ 'text-align': 'center', 'font-size': '0.8em' })
			.text(this.layerFeature.attributes.length + ' m')
			);
		$row.append($cell);
        
		//add id to row
		$cell = $('<td/>')
			.addClass('altTable-id').css('border-left', 'none')
			.append($('<div/>')
			.text(this.segmentID));
		$row.append($cell);
		
		//add primary name and city to row
		$cell = $('<td/>').addClass('altTable-primary')
			.append($('<div/>')
			.addClass('altTable-primary-name')
			.text(this.primaryName))
			.append($('<div/>')
			.addClass('altTable-primary-city')
			.text(this.primaryCity)
			);
		$row.append($cell);
		
		//add alt names and cities to row
		for (i = 0, n = this.alternates.length; i < n; i++) {
			$cell = $('<td/>').addClass('altTable-alt')
				.append($('<div/>').addClass('altTable-alt-name').text(this.alternates[i].name))
				.append($('<div/>').addClass('altTable-alt-city').text(this.alternates[i].city)
				);
			$row.append($cell);
		}
		return $row;
	}
};

function colorTable() {
	'use strict';
    var i,
		n,
		$table = $('#altTable');

    $table.find('.altTable-primary, .altTable-alt').each(function (index1) {
		var $this = $(this),
			text = $this.text(),
			match = false,
			color;

		for (i = 0, n = nameArray.length; i < n; i++) {
			if (nameArray[i].name === text) {
				$this.css('background-color', nameArray[i].color);
				match = true;
				break;
			}
		}
		if (match === false) {
			color = randomRgbaColor();
			$this.css('background-color', color);
			nameArray.push({ name: text, color: color });
		}
		match = false;
	});
}

function populateTable(maxAlternates) {
	'use strict';
	var i, n, j, m, $row;
	$altTable.find('tbody').empty();
	$('.altTable-header-alt').remove();
	for (i = 0, n = selectedSegments.length; i < n; i++) {
		$row = selectedSegments[i].tableRow.clone();
		for (j = selectedSegments[i].alternates.length, m = maxAlternates; j < m; j++) {
			$row.append($('<td/>').addClass('altTable-placeholder'));
		}
		$altTable.append($row);
	}
	for (i = 1, n = maxAlternates; i <= n; i++) {
		$('#altTable-header').append($('<th/>')
			.addClass('altTable-header-alt')
			.text('Alt ' + i));
	}
	colorTable();
}

function colorFeatures(nameToMatch, color) {
    'use strict';
    var i,
		n,
		j,
		t,
		colorValues,
		feature,
		names,
		nameCityCombined;
    
    //remove opacity from color so it can be controlled by layer style
    colorValues = color.match(/\d+/g);
    color = 'rgb(' + colorValues[0] + ',' + colorValues[1] + ',' + colorValues[2] + ')';

    for (i = 0, n = altLayer.features.length; i < n; i++) {
        feature = altLayer.features[i];
        //combine primary and alt names in one array
        names = feature.attributes.alt.clone();
        names.push(feature.attributes.primary);
        //test names for match
        for (j = 0, t = names.length; j < t; j++) {
            //combine street and city name (as in text of table cell)
            nameCityCombined = names[j].name + names[j].city;
            if (nameCityCombined === nameToMatch) {
                feature.attributes.bgColor = color;
                feature.renderIntent = 'highlight';
            }
        }
    }
}

function colorSegment(id, color) {
    'use strict';
    var i, n, feature;
    color = color || 'rgba(0, 0, 0, 0.8)';
    for (i = 0, n = altLayer.features.length; i < n; i++) {
        feature = altLayer.features[i];
        if (feature.attributes.id == id) {
            feature.attributes.bgColor = color;
            feature.renderIntent = 'highlight';
            break;
        }
    }
}

function applyHighlighting(event) {
    var $this1;
    switch (event.type) {
        case 'mouseenter':
            $this1 = $(this);
            if (event.data.singleSegment) {
                colorSegment($this1.text());
            } else {
                colorFeatures($this1.text(), $this1.css('background-color'));
            }
            $('#altTable tbody td').each(function (index) {
				var $this2 = $(this);
				if ($this1.text() === $this2.text()) {
					$this2.parent().addClass('altTable-selected');
				}
            });
            break;
        case 'mouseleave':
            resetRenderIntent();
            $('#altTable tr').each(function (index) {
                $(this).removeClass('altTable-selected');
            });
            break;
    }
    altLayer.redraw();
}

function checkSelection() {
	var i, n, j, m, alternate, thisItem, maxAlternates = 0, selectedItems;
	selectedSegments = [];
	if (W.selectionManager.hasSelectedItems() && altLayer.getVisibility()) {
		selectedItems = W.selectionManager.selectedItems;
		if (selectedItems.length > 1) {
			$('#altAutoSelect').show();
		}
		for (i = 0, n = selectedItems.length; i < n; i++) {
			thisItem = selectedItems[i];
			if (thisItem.model.type === 'segment') {
				for (j = 0, m = alternateObjectsArray.length; j < m; j++) {
					if (alternateObjectsArray[j].segmentID === thisItem.model.attributes.id) {
						if (alternateObjectsArray[j].layerFeature.attributes.updatedOn !==
							thisItem.model.attributes.updatedOn) {
							alternateObjectsArray.splice(j, 1);
						} else {
							alternate = alternateObjectsArray[j];
							continue;
						}
					}
				}
				if (!alternate) {
					alternate = new Alternate(thisItem);
					alternateObjectsArray.push(alternate);
				}
				selectedSegments.push(alternate);
				altLayer.addFeatures(alternate.layerFeature);
				if (maxAlternates < alternate.alternates.length) {
					maxAlternates = alternate.alternates.length;
				}
				alternate = null;
			}
		}
		populateTable(maxAlternates);
		$altDiv.fadeIn();
	} else {
		$altDiv.fadeOut();
		$('#altAutoSelect').hide();
		altLayer.removeAllFeatures();
		if (alternateObjectsArray.length > 50) {
			alternateObjectsArray = [];
		}
	}
}

function performAutoSelect() {
	'use strict';
	var i,
		options,
		route,
		segmentsToSelect = [],
		selection = W.selectionManager.selectedItems,
		n = selection.length;
	function routeCallback() {
		var segIDs, seg;
		segIDs = this.getRouteSegmentIDs()[0];
		segIDs.forEach(function (item) {
			seg = W.model.segments.get(item);
			if (seg) {
				segmentsToSelect.push(seg);
			}
		});
		if (this.last) {
			W.selectionManager.select(segmentsToSelect);
		}
	}
	if (n > 0) {
		options = {
			fastest: $('#altFastest').prop('checked'),
			tolls: $('#altAvoidTolls').prop('checked'),
			freeways: $('#altAvoidFreeways').prop('checked'),
			dirt: $('#altAvoidDirt').prop('checked'),
			longtrails: $('#altAvoidLongDirt').prop('checked'),
			uturns: $('#altAllowUturns').prop('checked')
		};
		for (i = 0; i < n - 1; i++) {
			if ('segment' === selection[i].model.type && 'segment' === selection[i + 1].model.type) {
				route = new wLib.Model.RouteSelection(selection[i], selection[i + 1], routeCallback, options);
				route.last = i === n - 2 ? true : false;
			}
		}
	}
}

function updateAlert() {
	var altVersion = "0.30",
		alertOnUpdate = true,
		versionChanges = 'WME Show Alt Names has been updated to ' + altVersion + '.\n';
	versionChanges += 'Changes:\n';
	versionChanges += '[*]Auto Select Tool: Multi-point routes can now be selected.\n';
	versionChanges += '[*]Auto Select Tool: Added route options.\n';
	versionChanges += '[*]New feature: Adds segment length to table display.\n';
	versionChanges += '[*]Removed zoom limit: table is now displayed at any zoom when a segment is selected.\n';
	versionChanges += '[*]Bug fix: Off-screen and out-of-range segments should now work.\n';
	versionChanges += '[*]Table sorting functionality has been removed.';
	if (alertOnUpdate && window.localStorage && window.localStorage.altVersion !== altVersion) {
		window.localStorage.altVersion = altVersion;
		alert(versionChanges);
	}
}

function init() {
	var altStyleMap, css, $header, optionsHTML, $optionsDiv, $optionsElement, $row;

	css = '.altTable, .altTable th, .altTable td {border: 1px solid white; padding: 3px; border-collapse: collapse; -moz-user-select: -moz-none; -khtml-user-select: none; -webkit-user-select: none;}\n';
    css += '.altTable-id {text-align: center; border-left: none;}\n';
    css += '.altTable-roadType {border-radius: 10px; color: black; text-shadow: 1px 1px 0 #fff,-1px -1px 0 #fff,1px -1px 0 #fff,-1px 1px 0 #fff,0px 1px 0 #fff,1px 0px 0 #fff,0px -1px 0 #fff,-1px 0px 0 #fff; border: 1px solid white; font-size: 0.8em; text-align: center; padding: 0 3px 0 3px; min-width: 32px;}';
    css += 'tr.altTable-selected > .altTable-id {font-weight: bold; background-color: white; color: black;}\n';

	css += '#altDiv {display: none; position: absolute; left: 6px; bottom: 60px; height: auto; width: auto; ';
	css += 'overflow-y: scroll; overflow-x: hidden; white-space: nowrap; background-color: rgba(0,0,0,0.8); color: white; padding: 5px; ';
	css += 'z-index: 1001; border-radius: 5px; max-height: 50%;}\n';

    // scroll bar CSS
    css += '#altDiv::-webkit-scrollbar {width: 15px; border-radius: 5px;}\n';
    css += '#altDiv::-webkit-scrollbar-track {border-radius: 5px; background: none; width: 10px;}\n';
    css += '#altDiv::-webkit-scrollbar-thumb {background-color: white; border-radius: 5px; border: 2px solid black;}\n';
	css += '#altDiv::-webkit-scrollbar-corner {background: none;}\n';

	// buttons css
	css += '.altOptions-button {margin: 0 0 3px 3px; height: 2em; font-size: 0.8em;}\n';
	
	// Options Menu CSS
	css += '#optionsDiv td {padding-right: 5px;}';
	css += '#optionsDiv table input {margin-right: 3px;}';
	
	//add css to page
	$('<style/>').html(css).appendTo($(document.head));

	// Make some buttons.
	$optionsElement = $('<div/>').attr('id', 'altOptions');
	$optionsElement.append(
		$('<button/>')
			.attr('id', 'altAutoSelect')
			.css('display', 'none')
			.addClass('altOptions-button')
			.text('Auto Select')
			.click(performAutoSelect)
		);
	$optionsElement.append(
		$('<button/>')
			.addClass('altOptions-button')
			.css('float', 'right')
			.text('Show Options')
			.click(function () {
			if ($optionsDiv.css('display') === 'none') {
				$optionsDiv.show();
				$(this).text('Hide Options');
			} else {
				$optionsDiv.hide();
				$(this).text('Show Options');
			}
		})
		);
	
	// Make the options menu.
	optionsHTML = '<html> <body> <div> <form> <table> <thead> <tr> <td colspan="2" style="font-weight: bold;">Auto Selection Route Options</td> </tr> </thead> <tbody> <tr> <td> <input type="checkbox" id="altAvoidTolls"></input>Avoid toll roads</td> <td> <input type="checkbox" id="altAvoidFreeways" </input>Avoid freeways</td> </tr> <tr> <td> <input type="checkbox" id="altAvoidLongDirt"></input>Avoid long dirt roads</td> <td> <input type="checkbox" id="altAvoidDirt"></input>Avoid dirt roads</td> </tr> <tr> <td> <input type="checkbox" id="altAllowUturns"></input>Allow U-turns</td> <td> <input type="checkbox" id="altFastest"></input>Fastest route</td> </tr> </tbody> </table> </form> </div> </body>  </html>';
	$optionsDiv = $('<div/>')
		.attr('id', 'optionsDiv')
		.css({
		'display': 'none',
		'border': '1px solid white',
		'padding': '3px',
		'margin': '0 0 3px 0'
	});
	$optionsDiv.append(optionsHTML);
	
	// Make the table to hold segment information.
	$altTable = $('<table/>').attr('id', 'altTable').addClass('altTable');
	$header = $('<thead/>');
	$row = $('<tr/>').attr('id', 'altTable-header');
	$row.append($('<th/>').attr('colspan', '2').text('Segment ID'));
	$row.append($('<th/>').text('Primary'));
	$header.append($row);
	$altTable.append($header);
	$altTable.append('<tbody/>');
	
	// Make the main div to hold script content.
	$altDiv = $('<div/>').attr('id', 'altDiv');
	$altDiv.append($optionsElement);
	$altDiv.append($optionsDiv);
	$altDiv.append($altTable);
	$altDiv.appendTo($('#WazeMap'));
    $altDiv.on('mouseenter mouseleave', 'td.altTable-primary, td.altTable-alt', { singleSegment: false }, applyHighlighting);
    $altDiv.on('dblclick', 'td.altTable-primary, td.altTable-alt', null, changeHighlightColor);
	$altDiv.on('mouseenter mouseleave', 'td.altTable-id', { singleSegment: true }, applyHighlighting);
	$altDiv.on('click', 'td.altTable-id', null, function () {
		panToSegment($(this).text());
	});
	$altDiv.on('dblclick', 'td.altTable-id', null, function () {
		selectSegment($(this).text());
	});
	
	// Create the map layer for segment highlighting.
    altStyleMap = new OL.StyleMap({
        default: new OL.Style({
            stroke: false
        }),
        highlight: new OL.Style({
			stroke: true,
            strokeWidth: 20,
            strokeColor: '${bgColor}',
            strokeOpacity: 1,
            strokeLinecap: 'round'
        })
    });
	altLayer = new OL.Layer.Vector('WME Show Alt Names', { styleMap: altStyleMap });
    altLayer.events.register('visibilitychanged', null, checkSelection);
    W.map.addLayer(altLayer);
	
	//check for beta editor due to road layer name differences
	if (location.href.match(/-beta/)) {
		betaEditor = true;
	}
	
	//register WME event listeners
	W.loginManager.events.register('afterloginchanged', null, init);
	W.selectionManager.events.register('selectionchanged', null, checkSelection);
	W.map.getLayersByName('Roads')[0].events.register('visibilitychanged', null, checkSelection);
	if (betaEditor) {
		W.map.getLayersByName('Roads')[0].events.register('visibilitychanged', null, checkSelection);
	} else {
		W.map.getLayersByName('Roads experimental')[0].events.register('visibilitychanged', null, checkSelection);
	}
	
	// Ready to go. Alert user to any updates and check for selected segments.
	updateAlert();
	checkSelection();
}

function bootstrap() {
	var bGreasemonkeyServiceDefined = false;
	try {
		if ("object" === typeof Components.interfaces.gmIGreasemonkeyService) {
			bGreasemonkeyServiceDefined = true;
		}
	} catch (err) {
		/* Ignore. */
	}
	if (undefined === typeof unsafeWindow || !bGreasemonkeyServiceDefined) {
		unsafeWindow = (function () {
			var dummyElem = document.createElement('p');
			dummyElem.setAttribute('onclick', 'return window;');
			return dummyElem.onclick();
		})();
	}
	/* begin running the code! */
	if (undefined !== typeof $ &&
		$('#WazeMap').length !== 0 &&
		undefined !== typeof W.selectionManager.events.register &&
		undefined !== typeof W.loginManager.events.register) {
		window.setTimeout(init, 100);
	} else {
		window.setTimeout(function () {
			bootstrap();
		}, 1000);
	}
}

window.setTimeout(bootstrap, 100);