WME Junction Angle Info

Show the angle between two selected (and connected) segments

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name				WME Junction Angle Info
// @namespace			https://github.com/milkboy/WME-ja
// @description			Show the angle between two selected (and connected) segments
// @include				/^https:\/\/(www|editor-beta)\.waze\.com\/(.{2,6}\/)?editor\/.*$/
// @version				1.8.4
// @grant				none
// @copyright			2015 Michael Wikberg <[email protected]>
// @license				CC-BY-NC-SA
// ==/UserScript==

/**
 * Copyright 2015 Michael Wikberg <[email protected]>
 * WME Junction Angle Info extension is licensed under a Creative Commons
 * Attribution-NonCommercial-ShareAlike 3.0 Unported License.
 *
 * Contributions by:
 *	2014 Paweł Pyrczak "tkr85" <[email protected]>
 *	2014 "AlanOfTheBerg" <[email protected]>
 *	2014 "berestovskyy" <?>
 *	2015 "FZ69617" <?>
 */

function run_ja() {

	/*
	 * First some variable and enumeration definitions
	 */
	var junctionangle_version = "1.8.4";
	var junctionangle_debug = 1;	//0: no output, 1: basic info, 2: debug 3: crazy debug
	var $;

	var ja_last_restart = 0;
	var ja_roundabout_points = [];
	var ja_options = {};

	var ja_routing_type = {
		BC: "junction_none",
		KEEP: "junction_keep",
		TURN: "junction",
		EXIT: "junction_exit",
		PROBLEM: "junction_problem",
		ERROR: "junction_error",
		ROUNDABOUT: "junction_roundabout"
	};
	
	var ja_road_type = {
		//Streets
		STREET: 1,
		PRIMARY_STREET: 2,
		//Highways
		RAMP: 4,
		FREEWAY: 3,
		MAJOR_HIGHWAY: 6,
		MINOR_HIGHWAY: 7,
		//Other drivable
		DIRT_ROAD: 8,
		FERRY: 14,
		PRIVATE_ROAD: 17,
		PARKING_LOT_ROAD: 20,
		//Non-drivable
		WALKING_TRAIL: 5,
		PEDESTRIAB_BOARDWALK: 10,
		STAIRWAY: 16,
		RAILROAD: 18,
		RUNWAY: 19
	};
	
	var ja_vehicle_types = {
		TRUCK: 1,
		PUBLIC: 2,
		TAXI: 4,
		BUS: 8,
		HOV2: 16,
		HOV3: 32,
		RV: 64,
		TOWING: 128,
		MOTORBIKE: 256,
		PRIVATE: 512,
		HAZ: 1024
	};

	var ja_settings = {
		angleMode: { elementType: "select", elementId: "_jaSelAngleMode", defaultValue: "aAbsolute", options: ["aAbsolute", "aDeparture"]},
		guess: { elementType: "checkbox", elementId: "_jaCbGuessRouting", defaultValue: false },
		noInstructionColor: { elementType: "color", elementId: "_jaTbNoInstructionColor", defaultValue: "#ffffff", group: "guess"},
		keepInstructionColor: { elementType: "color", elementId: "_jaTbKeepInstructionColor", defaultValue: "#cbff84", group: "guess"},
		turnInstructionColor: { elementType: "color", elementId: "_jaTbTurnInstructionColor", defaultValue: "#4cc600", group: "guess"},
		exitInstructionColor: { elementType: "color", elementId: "_jaTbExitInstructionColor", defaultValue: "#6cb5ff", group: "guess"},
		problemColor: { elementType: "color", elementId: "_jaTbProblemColor", defaultValue: "#a0a0a0", group: "guess"},
		roundaboutOverlayDisplay: { elementType: "select", elementId: "_jaSelRoundaboutOverlayDisplay", defaultValue: "rOverNever", options: ["rOverNever","rOverSelected","rOverAlways"]},
		roundaboutColor: { elementType: "color", elementId: "_jaTbRoundaboutColor", defaultValue: "#ff8000", group: "roundaboutOverlayDisplay"},
		roundaboutOverlayColor: { elementType: "color", elementId: "_jaTbRoundaboutOverlayColor", defaultValue: "#aa0000", group: "roundaboutOverlayDisplay"},
		decimals: { elementType: "number", elementId: "_jaTbDecimals", defaultValue: 0, min: 0, max: 2},
		pointSize: { elementType: "number", elementId: "_jaTbPointSize", defaultValue: 12, min: 6, max: 20}
	};


	/*
	 * Main logic functions
	 */

	function junctionangle_init() {

		//Listen for selected nodes change event
		window.Waze.selectionManager.events.register("selectionchanged", null, ja_calculate);

		window.Waze.model.segments.events.on({
			"objectschanged": ja_calculate,
			"objectsremoved": ja_calculate
		});
		window.Waze.model.nodes.events.on({
			"objectschanged": ja_calculate,
			"objectsremoved": ja_calculate
		});


		//HTML changes after login, even though the page is not reloaded. Better do init again.
		window.Waze.loginManager.events.register("afterloginchanged", null, junctionangle_init);

		//Recalculate on zoom end also
		window.Waze.map.events.register("zoomend", null, ja_calculate);

		ja_load();
		ja_loadTranslations();

		/**
		 * Add JAI tab configuration options
		 */
		var ja_settings_dom = document.createElement("div");
		var ja_settings_dom_panel = document.createElement("div");
		var ja_settings_dom_content = document.createElement("div");
		ja_settings_dom_panel.className = "side-panel-section";
		ja_settings_dom_content.className = "tab-content";
		var ja_settings_header = document.createElement('h4');
		ja_settings_header.appendChild(document.createTextNode(ja_getMessage("settingsTitle")));
		ja_settings_dom_content.appendChild(ja_settings_header);

		var style = document.createElement('style');
		style.appendChild(document.createTextNode(''
				+ '#jaOptions label { display: inline; }\n'
				+ '#jaOptions input, select { margin-right: 5px; }\n'
			));

		var form = document.createElement('form');
		var section = document.createElement('div');
		section.className = "form-group";
		form.className = "attributes-form side-panel-section";
		section.id = "jaOptions";
		ja_log("---------- Creating settings HTML ----------", 2);
		Object.getOwnPropertyNames(ja_settings).forEach(function (a,b,c) {
			var setting = ja_settings[a];
			var ja_controls_container = document.createElement('div');
			var ja_input = document.createElement('input');
			var ja_label = document.createElement('label');
			ja_controls_container.className = "controls-container";
			ja_input.type = setting['elementType'];
			switch (setting['elementType']) {
				case 'color':
					ja_input.id = setting['elementId'];
					ja_controls_container.appendChild(ja_input);
					break;
				case 'number':
					ja_input.id = setting['elementId'];
					ja_input.setAttribute("min", setting['min']);
					ja_input.setAttribute("max", setting['max']);
					ja_controls_container.appendChild(ja_input);
					break;
				case 'text':
					ja_input.id = setting['elementId'];
					ja_input.size = (setting['max'] ? setting['max'] : 8);
					ja_input.maxlength = (setting['max'] ? setting['max'] : 7);
					ja_controls_container.appendChild(ja_input);
					break;
				case 'checkbox':
					ja_input.id = setting['elementId'];
					ja_input.onchange = function() { ja_onchange(this) };
					ja_controls_container.appendChild(ja_input);
					break;
				case 'select':
					ja_input = document.createElement('select'); //Override <input> with <select>
					ja_input.id = setting['elementId'];
					for(var i = 0; i < setting["options"].length; i++) {
						var ja_select_option = document.createElement('option');
						ja_select_option.value = setting["options"][i];
						ja_select_option.appendChild(document.createTextNode(ja_getMessage(setting["options"][i])));
						ja_input.appendChild(ja_select_option);
					}
					ja_input.onchange = function() { ja_onchange(this) };
					ja_controls_container.appendChild(ja_input);
					break;
			}

			ja_label.setAttribute("for", setting['elementId']);
			ja_label.appendChild(document.createTextNode(ja_getMessage(a)));
			ja_controls_container.appendChild(ja_label);

			section.appendChild(ja_controls_container);
		});
		section.appendChild(document.createElement('br'));
		
		var ja_apply_button = document.createElement('button');
		ja_apply_button.type = "button";
		ja_apply_button.className = "btn btn-default";
		ja_apply_button.addEventListener("click", ja_save, true);
		ja_apply_button.appendChild(document.createTextNode(ja_getMessage("apply")));
		
		var ja_reset_button = document.createElement('button');
		ja_reset_button.type = "button";
		ja_reset_button.className = "btn btn-default";
		ja_reset_button.addEventListener("click", ja_reset, true);
		ja_reset_button.appendChild(document.createTextNode(ja_getMessage("resetToDefault")));
		
		section.appendChild(ja_apply_button);
		section.appendChild(document.createTextNode(" "));
		section.appendChild(ja_reset_button);

		form.appendChild(section);
		ja_settings_dom_content.appendChild(form);

		var userTabs = document.getElementById('user-info');
		var navTabs = userTabs.getElementsByClassName('nav-tabs', userTabs)[0];
		var tabContent = userTabs.getElementsByClassName('tab-content', userTabs)[0];

		ja_settings_dom.id = "sidepanel-ja";
		ja_settings_dom.className = "tab-pane";

		ja_settings_dom_content.style.paddingTop = "0";

		ja_settings_dom.appendChild(style);

		ja_settings_dom_panel.appendChild(ja_settings_dom_content);
		ja_settings_dom.appendChild(ja_settings_dom_panel);
		
		//Add some version info etc
		var ja_info = document.createElement('ul');
		ja_info.className = "list-unstyled -side-panel-section";
		ja_info.style.fontSize = "11px";
		
		var ja_version_elem = document.createElement('li');
		ja_version_elem.appendChild(document.createTextNode(ja_getMessage("name") + ": v" + junctionangle_version));
		ja_info.appendChild(ja_version_elem);

		ja_info.appendChild(ja_helplink('https://wiki.waze.com/wiki/Roundabouts/USA#Understanding_navigation_instructions', 'roundaboutnav'));
		ja_info.appendChild(ja_helplink('https://github.com/milkboy/WME-ja/issues', 'ghissues'));

		ja_settings_dom.appendChild(ja_info);

		if(tabContent != null) {
			tabContent.appendChild(ja_settings_dom);
		} else {
			ja_log("Could not append setting to tabContent!?!", 1);
		}

		jatab = document.createElement('li');
		jatab.innerHTML = '<!--suppress HtmlUnknownAnchorTarget --><a href="#sidepanel-ja" data-toggle="tab">JAI</a>';
		if(navTabs != null)
			navTabs.appendChild(jatab);

		//Add support for translations. Default (and fallback) is "en".
		//Note, don't make typos in "acceleratorName", as it has to match the layer name (with whitespace removed
		// to actually work. Took me a while to figure that out...
		I18n.translations[window.I18n.locale].layers.name["junction_angles"] = ja_getMessage("name");

		/**
		 * Initialize JAI OpenLayers vector layer
		 */
		if (window.Waze.map.getLayersBy("uniqueName","junction_angles").length == 0) {

			// Create a vector layer and give it your style map.
			ja_mapLayer = new window.OpenLayers.Layer.Vector(ja_getMessage("name"), {
				displayInLayerSwitcher: true,
				uniqueName: "junction_angles",
				shortcutKey: "S+j",
				accelerator: "toggle" + ja_getMessage("name").replace(/\s+/g,''),
				className: "junction-angles",
				styleMap: new window.OpenLayers.StyleMap(ja_style())
			});

			window.Waze.map.addLayer(ja_mapLayer);
			ja_log("version " + junctionangle_version + " loaded.", 0);

			ja_log(window.Waze.map, 3);
			ja_log(window.Waze.model, 3);
			ja_log(window.Waze.loginManager, 3);
			ja_log(window.Waze.selectionManager, 3);
			ja_log(ja_mapLayer, 3);
			ja_log(window.OpenLayers, 3);
		} else {
			ja_log("Oh, nice.. We already had a layer?", 3);
		}

		ja_apply();
		ja_calculate();
	}

	/**
	 *
	 * @param node Junction node
	 * @param s_in_a "In" segment id
	 * @param s_out_a "Out" segment id
	 * @param angles array of segment absolute angles [0] angle, [1] segment id, 2[?]
	 * @returns {string}
	 */
	function ja_guess_routing_instruction(node, s_in_a, s_out_a, angles) {
		ja_log("Guessing routing instructions from " + s_in_a + " via node " + node.attributes.id + " to " + s_out_a,2);
		ja_log(node, 4);
		ja_log(s_in_a, 4);
		ja_log(s_out_a, 4);
		ja_log(angles, 3);
		var s_in_id = s_in_a;
		var s_out_id = s_out_a;

		s_in_a = window.$.grep(angles, function(element, index){
			return element[1] == s_in_a;
		});
		s_out_a = window.$.grep(angles, function(element, index){
			return element[1] == s_out_a;
		});

		var s_n = {}, s_in, s_out = {}, street_n = {}, street_in;
		node.attributes.segIDs.forEach(function(element, index, array) {
			if (element == s_in_id) {
				s_in = node.model.segments.get(element);
				street_in = ja_get_streets(element);
				//Set empty name for streets if not defined
				if(typeof street_in.primary === 'undefined') { street_in.primary = {}; }
				if(typeof street_in.primary.name === 'undefined') {
					street_in.primary['name'] = "";
				}
			} else {
				if(element == s_out_id) {
					//store for later use
					s_out[element] = node.model.segments.get(element);
					//Set empty name for streets if not defined
					if(typeof s_out[element].primary === 'undefined') {
						s_out[element]['primary'] = { name: "" };
					}
				}
				s_n[element] = node.model.segments.get(element);
				street_n[element] = ja_get_streets(element);
				if(typeof street_n[element].primary === 'undefined') {
					street_n[element]['primary'] = { name: ""};
				}
			}
		});

		ja_log(s_n, 3);
		ja_log(street_n,3);
		ja_log(s_in,3);
		ja_log(street_in,2);

		var angle = ja_angle_diff(s_in_a[0], (s_out_a[0]), false);
		ja_log("Turn angle is: " + angle, 2);

		//No other possible turns
		if(node.attributes.segIDs.length <= 2) {
			ja_log("Only one possible turn", 2);
			return ja_routing_type.BC;
		} //No instruction

		/*
		 *
		 * Here be dragons!
		 *
		 */
		if(!ja_is_turn_allowed(s_in, node, s_out[s_out_id])) {
			//Turn is disallowed!
			return ja_routing_type.ERROR;
		}

		if(Math.abs(angle) <= 44) {
			ja_log("Turn is <= 44", 2);

			/*
			 * Filter out disallowed turns. FIXME: Need to check for disallowed turns somehow!
			 */
			ja_log("Original angles and street_n:", 2);
			ja_log(angles, 2);
			ja_log(street_n, 2);
			ja_log(s_n, 2);
			angles = angles.filter(function (a,b,c) {
				ja_log("Filtering angle: " + ja_angle_diff(s_in_a,a[0],false), 2);
				if(typeof s_n[a[1]] !== 'undefined'
					&& ja_is_turn_allowed(s_in, node, s_n[a[1]])
                    && Math.abs(ja_angle_diff(s_in_a,a[0],false)) <= 45
                    ) {
					ja_log(true, 4);
					return true;
				} else {
					ja_log(false, 4);
					if(street_n[a[1]]) {
						delete s_n[a[1]];
						delete street_n[a[1]];
					}
					return false;
				}
			});
			ja_log("Filtered angles and street_n:", 2);
			ja_log(angles, 2);
			ja_log(street_n, 2);
			ja_log(s_n, 2);

			if(angles.length <= 1) {
				ja_log("Only one allowed turn left", 2);
				return ja_routing_type.BC;
			} //No instruction

			/*
			 * Apply simplified BC check
			 */
			var bc_matches = {}, bc_prio = 0, bc_count = 0;
			var bc_collect = function(a, prio) {
				ja_log("Potential BC = " + prio, 2);
				ja_log(a, 2);
				if (bc_prio == 0 || prio < bc_prio) {
					bc_matches = {};
					bc_prio = prio;
					bc_count = 0;
				}
				if (prio == bc_prio) {
					bc_matches[a[1]] = a;
					bc_count++;
				}
				ja_log("BC candidates:", 2);
				ja_log(bc_matches, 2);
			};

			//Collect all matching unrestricted <45 turns
			for(k=0; k< angles.length; k++) {
				var a = angles[k];

				ja_log("Checking angle " + k, 2);
				ja_log(a, 2);

				var tmp_angle = ja_angle_diff(s_in_a[0], a[0], false);
				ja_log(tmp_angle, 2);

				var tmp_s_out = {};
				tmp_s_out[a[1]] = s_n[a[1]];
				var tmp_street_out = {};
				tmp_street_out[a[1]] = street_n[a[1]];

				if(s_out_id == a[1] || (
					Math.abs(tmp_angle) < 45 &&  //Angle is < 45
					Math.abs(ja_angle_diff(a[0], s_out_a[0], true)) > 1 //Arbitrarily chosen angle for "overlapping" segments.
					)){
					ja_log("Found BC eligible turn <= 44", 2);

					if(ja_primary_name_and_type_match(street_in, tmp_street_out)) {
						bc_collect(a, 1);
					} else if(ja_alt_name_match(street_in, tmp_street_out)
								&& ja_segment_type_match(s_in, tmp_s_out)) {
						bc_collect(a, 2);
					} else if(ja_primary_name_match(street_in, tmp_street_out)
								|| ja_cross_name_match(street_in, tmp_street_out)) {
						bc_collect(a, 3);
					} else if(ja_alt_name_match(street_in, tmp_street_out)) {
						bc_collect(a, 4);
					} else if(ja_segment_type_match(s_in, tmp_s_out)) {
						bc_collect(a, 5);
					} else {
						//Non-BC
					}
				}
			}

			if (bc_matches[s_out_id] !== undefined && bc_count == 1) {
				ja_log("\"straight\": no instruction", 2);
				return ja_routing_type.BC
			}

			ja_log("No BC logic.............................", 2);

			// Primary to non-primary
			if(ja_is_primary_road(s_in) && !ja_is_primary_road(s_out[s_out_id])) {
				ja_log("Primary to non-primary == exit", 2);
				if(s_in.model.isLeftHand ? (angle > 0 ) : (angle < 0)) {
					return ja_routing_type.EXIT;
				}
			}

			// Ramp to non-primary or non-ramp
			if(ja_is_ramp(s_in) && !ja_is_primary_road(s_out[s_out_id]) && !ja_is_ramp(s_out[s_out_id]) ) {
				ja_log("Ramp to non-primary and non-ramp == exit", 2);
				if(s_in.model.isLeftHand ? (angle > 0 ) : (angle < 0)) {
					return ja_routing_type.EXIT;
				}
			}

			ja_log("DEFAULT: keep", 2);
			return ja_routing_type.KEEP;
		} else if(Math.abs(angle) <= 46) {
			ja_log("Angle is in gray zone 44-46", 2);
			return ja_routing_type.PROBLEM;
		} else {
			ja_log("Normal turn", 2);
			return ja_routing_type.TURN; //Normal turn (left|right)
		}
	}

	function ja_calculate_real() {
		ja_log("Actually calculating now", 2);
		var ja_start_time = Date.now();
		ja_roundabout_points = [];
		ja_log(window.Waze.map, 3);
		if(typeof ja_mapLayer === 'undefined') { return 1;}
		//clear old info
		ja_mapLayer.destroyFeatures();

		if(ja_getOption("roundaboutOverlayDisplay") == "rOverAlways") ja_draw_roundabout_overlay();

		//try to show all angles for all selected segments
		if (window.Waze.selectionManager.selectedItems.length == 0) return 1;
		ja_log("Checking junctions for " + window.Waze.selectionManager.selectedItems.length + " segments", 2);
		var ja_nodes = [];

		window.Waze.selectionManager.selectedItems.forEach(function(element, index, array) {
			ja_log(element, 3);
			switch (element.model.type) {
				case "node":
					ja_nodes.push(element.model.attributes.id);
					break;
				case "segment":
					//segments selected?
					if (element.model.attributes.fromNodeID != null &&
						ja_nodes.indexOf(element.model.attributes.fromNodeID) == -1) {
						ja_nodes.push(element.model.attributes.fromNodeID);
					}
					if (element.model.attributes.toNodeID != null &&
						ja_nodes.indexOf(element.model.attributes.toNodeID) == -1) {
						ja_nodes.push(element.model.attributes.toNodeID);
					}
					break;
				case "venue":
					break;
				default:
					ja_log("Found unknown item type: " + element.model.type, 2);
					break;
			}
			ja_log(ja_nodes, 2);
		});

		//Figure out if we have a selected roundabout and do some magic
		var ja_selected_roundabouts = {};

		ja_nodes.forEach(function(node) {
			ja_log(window.Waze.model.nodes.get(node), 3);
			
			var tmp_s = null;
			var tmp_junctionID = null;
			if(window.Waze.model.nodes.get(node) == null || typeof window.Waze.model.nodes.get(node).attributes.segIDs === 'undefined') return;
			window.Waze.model.nodes.get(node).attributes.segIDs.forEach(function(segment) {
				ja_log(segment, 3);
				
				if(window.Waze.model.segments.get(segment).attributes.junctionID) {
						ja_log("WE ARE IN OR AROUND A ROUNDABOUT: " + window.Waze.model.segments.get(segment).attributes.junctionID, 3);
						tmp_junctionID = window.Waze.model.segments.get(segment).attributes.junctionID;
				} else {
					tmp_s = segment;
					tmp_n = node;
				}
				ja_log("tmp_s: " + (tmp_s === null ? 'null' : tmp_s), 3);
			});
			ja_log("final tmp_s: " + (tmp_s === null ? 'null' : tmp_s), 3);
			if(tmp_junctionID === null) return;
			if(!ja_selected_roundabouts.hasOwnProperty(tmp_junctionID)) {
				ja_selected_roundabouts[tmp_junctionID] = { 'in_s': tmp_s, 'in_n': tmp_n, 'out_s': null, 'out_n': null, 'p': window.Waze.model.junctions.get(tmp_junctionID).geometry };
			} else {
				ja_selected_roundabouts[tmp_junctionID].out_s = tmp_s;
				ja_selected_roundabouts[tmp_junctionID].out_n = node;
			}
		});

		//Do some fancy painting for the roundabouts...
		for(var tmp_roundabout in ja_selected_roundabouts) {
			ja_log(tmp_roundabout, 3);
			ja_log(ja_selected_roundabouts[tmp_roundabout], 3);

			//New roundabouts don't have coordinates yet..
			if(typeof ja_selected_roundabouts[tmp_roundabout].p === 'undefined' ||
				ja_selected_roundabouts[tmp_roundabout].out_n === null
				) {
				continue;
			}

			//Draw circle overlay for this roundabout
			if(ja_getOption("roundaboutOverlayDisplay") == "rOverSelected") ja_draw_roundabout_overlay(tmp_roundabout);

			//Transform LonLat to actual layer projection
			var tmp_roundabout_center = ja_coordinates_to_point(ja_selected_roundabouts[tmp_roundabout].p.coordinates);
			var angle = ja_angle_between_points(
				window.Waze.model.nodes.get(ja_selected_roundabouts[tmp_roundabout].in_n).geometry,
				tmp_roundabout_center,
				window.Waze.model.nodes.get(ja_selected_roundabouts[tmp_roundabout].out_n).geometry
			);
			ja_mapLayer.addFeatures([
				new window.OpenLayers.Feature.Vector(
					tmp_roundabout_center,
					{ 
						angle: ja_round(angle) + '°',
						ja_type: ja_is_roundabout_normal(tmp_roundabout, ja_selected_roundabouts[tmp_roundabout].in_n) ? ja_routing_type.TURN : ja_routing_type.ROUNDABOUT
					}
				)
			]);
		}

		//Start looping through selected nodes
		for (var i = 0; i < ja_nodes.length; i++) {
			node = window.Waze.model.nodes.get(ja_nodes[i]);
			if (node == null || !node.hasOwnProperty('attributes')) {
				//Oh oh.. should not happen? We want to use a node that does not exist
				ja_log("Oh oh.. should not happen?",2);
				ja_log(node, 2);
				ja_log(ja_nodes[i], 2);
				ja_log(window.Waze.model, 3);
				ja_log(window.Waze.model.nodes, 3);
				continue;
			}
			//check connected segments
			var ja_current_node_segments = node.attributes.segIDs;
			ja_log(node, 2);

			//ignore of we have less than 2 segments
			if (ja_current_node_segments.length <= 1) {
				ja_log("Found only " + ja_current_node_segments.length + " connected segments at " + ja_nodes[i] + ", not calculating anything...", 2);
				continue;
			}

			ja_log("Calculating angles for " + ja_current_node_segments.length + " segments", 2);

			var angles = [];
			var ja_selected_segments_count = 0;
			var ja_selected_angles = [];

			ja_current_node_segments.forEach(function (nodeSegment, j) {
				s = window.Waze.model.segments.objects[nodeSegment];
				if(typeof s === 'undefined') {
					//Meh. Something went wrong, and we lost track of the segment. This needs a proper fix, but for now
					// it should be sufficient to just restart the calculation
					ja_log("Failed to read segment data from model. Restarting calculations.", 1);
					if(ja_last_restart == 0) {
						ja_last_restart = new Date().getTime();
						setTimeout(function(){ja_calculate();}, 500);
					}
					return 4;
				}
				a = ja_getAngle(ja_nodes[i], s);
				ja_log("Segment " + nodeSegment + " angle is " + a, 2);
				angles[j] = [a, nodeSegment, s != null ? s.isSelected() : false];
				if (s != null ? s.isSelected() : false) {
					ja_selected_segments_count++;
				}
			});

			//make sure we have the selected angles in correct order
			ja_log(ja_current_node_segments, 3);
			window.Waze.selectionManager.selectedItems.forEach(function (selectedSegment, selectedIndex, selectedItems) {
				var selectedSegmentId = selectedSegment.model.attributes.id;
				ja_log("Checking if " + selectedSegmentId + " is in current node", 3);
				if(ja_current_node_segments.indexOf(selectedSegmentId) >= 0) {
					ja_log("It is!", 3);
					//find the angle
					for(j=0; j < angles.length; j++) {
						if(angles[j][1] == selectedSegmentId) {
							ja_selected_angles.push(angles[j]);
							break;
						}
					}
				} else {
					ja_log("It's not..", 3);
				}
			});


			ja_log(angles, 3);

			var ja_label_distance;
			switch (window.Waze.map.zoom) {
				case 10:
					ja_label_distance = 2.8;
					break;
				case 9:
					ja_label_distance = 4;
					break;
				case 8:
					ja_label_distance = 8;
					break;
				case 7:
					ja_label_distance = 15;
					break;
				case 6:
					ja_label_distance = 25;
					break;
				case 5:
					ja_label_distance = 40;
					break;
				case 4:
					ja_label_distance = 80;
					break;
				case 3:
					ja_label_distance = 150;
					break;
				case 2:
					ja_label_distance = 300;
					break;
				case 1:
					ja_label_distance = 400;
					break;
			}

			ja_label_distance = ja_label_distance * (1+(0.2*parseInt(ja_getOption("decimals"))));

			ja_log("zoom: " + window.Waze.map.zoom + " -> distance: " + ja_label_distance, 2);

			var a, ha, point;
			//if we have two connected segments selected, do some magic to get the turn angle only =)
			if (ja_selected_segments_count == 2) {
				var ja_extra_space_multiplier = 1;

				a = ja_angle_diff(ja_selected_angles[0][0], ja_selected_angles[1][0], false);

				ha = (parseFloat(ja_selected_angles[0][0]) + parseFloat(ja_selected_angles[1][0]))/2;
				if(
					(Math.abs(ja_selected_angles[0][0]) + Math.abs(ja_selected_angles[1][0])) > 180
				&& (
						(ja_selected_angles[0][0] < 0 && ja_selected_angles[1][0] > 0)
						|| (ja_selected_angles[0][0] > 0 && ja_selected_angles[1][0] < 0))
					) ha += 180;

				if (Math.abs(a) > 120) {
					ja_log("Sharp angle", 2);
					ja_extra_space_multiplier = 2;
				}

				//Move point a bit if it's on the top (Bridge icon will obscure it otherwise)
				if(ha > 40 && ha < 120) ja_extra_space_multiplier = 2;


				ja_log("Angle between " + ja_selected_angles[0][1] + " and " + ja_selected_angles[1][1] + " is " + a + " and position for label should be at " + ha, 2);

				//Guess some routing instructions based on segment types, angles etc
				var ja_junction_type = ja_routing_type.TURN; //Default to old behavior
				
				if(ja_getOption("guess")) {
					ja_log(ja_selected_angles, 2);
					ja_log(angles, 2);
					ja_junction_type = ja_guess_routing_instruction(node, ja_selected_angles[0][1], ja_selected_angles[1][1], angles);
					ja_log("Type is: " + ja_junction_type, 2);
				}
				//put the angle point
				point = new window.OpenLayers.Geometry.Point(
						node.geometry.x + (ja_extra_space_multiplier * ja_label_distance * Math.cos((ha * Math.PI) / 180)),
						node.geometry.y + (ja_extra_space_multiplier * ja_label_distance * Math.sin((ha * Math.PI) / 180))
				);
				ja_draw_marker(point, node, ja_label_distance, a, ha, true, ja_junction_type);
			}
			else {
				//sort angle data (ascending)
				angles.sort(function (a, b) {
					return a[0] - b[0]
				});
				ja_log(angles, 3);
				ja_log(ja_selected_segments_count, 3);

				//get all segment angles
				angles.forEach(function(angle, j) {
					a = (360 + (angles[(j + 1) % angles.length][0] - angle[0])) % 360;
					ha = (360 + ((a / 2) + angle[0])) % 360;
					var a_in = angles.filter(function(a,b,c) {
						"use strict";
						if(a[2]) return true;
					})[0];

					//Show only one angle for nodes with only 2 connected segments and a single selected segment
					// (not on both sides). Skipping the one > 180
					if (
						(ja_selected_segments_count == 1
							&& angles.length == 2
							&& (Math.abs(a) > 180
								|| (Math.abs(a)%180 == 0 && j == 0 )
								)
							)
						|| (ja_getOption("angleMode") == "aDeparture" && angles[j][1] == a_in[1])) {
						ja_log("Skipping marker, as we need only one of them", 2);
					} else if(ja_getOption("angleMode") == "aDeparture") {
						ja_log("Angle in:",2);
						ja_log(a_in,2);
						//ja_log(window.Waze.model.segments.objects[a_in[1]], 2);
						ja_log(ja_guess_routing_instruction(node, a_in[1], angle[1], angles), 2);
						ha = angle[0];
						a = ja_angle_diff(a_in[0], angles[j][0], false);
						//ha = (360 + ((a / 2) + angle[0])) % 360; //FIXME
						point = new window.OpenLayers.Geometry.Point(
								node.geometry.x + (ja_label_distance * 2 * Math.cos((ha * Math.PI) / 180)),
								node.geometry.y + (ja_label_distance * 2 * Math.sin((ha * Math.PI) / 180))
						);
						ja_draw_marker(point, node, ja_label_distance, a, ha, true, ja_guess_routing_instruction(node, a_in[1], angle[1], angles));
					} else {
						ja_log("Angle between " + angle[1] + " and " + angles[(j + 1) % angles.length][1] + " is " + a + " and position for label should be at " + ha, 3);
						point = new window.OpenLayers.Geometry.Point(
								node.geometry.x + (ja_label_distance * Math.cos((ha * Math.PI) / 180)),
								node.geometry.y + (ja_label_distance * Math.sin((ha * Math.PI) / 180))
						);
						ja_draw_marker(point, node, ja_label_distance, a, ha);
					}
				});
			}
		}

		ja_last_restart = 0;
		var ja_end_time = Date.now();
		ja_log("Calculation took " + String(ja_end_time - ja_start_time) + " ms", 2);
	}


	/*
	 * Drawing functions
	 */
	/**
	 *
	 * @param point Estimated point for marker
	 * @param node Node the marker is for
	 * @param ja_label_distance Arbitrary distance to be used in moving markers further away etc
	 * @param a Angle to display
	 * @param ha Angle to marker from node (FIXME: either point or ha is probably unnecessary)
	 * @param withRouting true: show routing guessing markers, false: show "normal" angle markers
	 * @param ja_junction_type If using routing, this needs to be set to the desired type
	 */
	function ja_draw_marker(point, node, ja_label_distance, a, ha, withRouting, ja_junction_type) {
		"use strict";


		//Try to estimate of the point is "too close" to another point (or maybe something else in the future; like turn restriction arrows or something)
		var ja_tmp_distance = ja_label_distance;
		ja_log("Starting distance estimation", 3);
		while(ja_mapLayer.features.some(function(feature, index){
			if(typeof feature.attributes.ja_type !== 'undefined'  && feature.attributes.ja_type !== 'roundaboutoverlay') {
				//Arbitrarily chosen minimum distance.. Should actually use the real bounds of the markers, but that didn't work out.. Bounds are always 0..
				if(ja_label_distance / 1.4 > feature.geometry.distanceTo(point)) {
					ja_log(ja_label_distance / 1.5 > feature.geometry.distanceTo(point) + " is kinda close..", 3);
					return true;
				}
			}
			return false;
		})) {
			ja_tmp_distance = ja_tmp_distance + ja_label_distance / 4; //add 1/4 of the original distance and hope for the best =)
			ja_log("setting distance to " + ja_tmp_distance, 2);
			point = new window.OpenLayers.Geometry.Point(
					node.geometry.x + (ja_tmp_distance * Math.cos((ha * Math.PI) / 180)), node.geometry.y + (ja_tmp_distance * Math.sin((ha * Math.PI) / 180))
			);
		}
		ja_log("Done distance estimation", 3);

		var anglePoint = withRouting ?
			new window.OpenLayers.Feature.Vector(
			point
				, { angle: (a>0?"<":"") + ja_round(Math.abs(a)) + "°" + (a<0?">":""), ja_type: ja_junction_type }
		): new window.OpenLayers.Feature.Vector(
			point
			, { angle: ja_round(a) + "°", ja_type: "generic" }
		);
		ja_log(anglePoint, 3);

		//Don't paint points inside an overlaid roundabout
		if(ja_roundabout_points.some(function (rondaboutPoint){
			return rondaboutPoint.containsPoint(point);
		})) return;

		//Draw a line to the point
		ja_mapLayer.addFeatures([
				new window.OpenLayers.Feature.Vector(
					new window.OpenLayers.Geometry.LineString([node.geometry, point]),
					{},
					{strokeOpacity: 0.6, strokeWidth: 1.2, strokeDashstyle: "solid", strokeColor: "#ff9966"}
				)
			]
		);

		//push the angle point
		ja_mapLayer.addFeatures([anglePoint]);

	}

	function ja_draw_roundabout_overlay(junctionId) {
		window.Waze.model.junctions.getObjectArray().forEach(function (element, index, array){
			ja_log(element, 3);
			//Check if we want a specific junction. FIXME: this should actually be done by a direct select, instead of looping through all..
			if(typeof junctionId !== "undefined" && junctionId != element.id) {
				return;
			}
			var nodes = {};
			element.segIDs.forEach(function(s) {
				var seg = window.Waze.model.segments.get(s);
				ja_log(seg, 3);
				nodes[seg.attributes.fromNodeID] = window.Waze.model.nodes.get(seg.attributes.fromNodeID);
				nodes[seg.attributes.toNodeID] = window.Waze.model.nodes.get(seg.attributes.toNodeID);
			});

			ja_log(nodes, 3);
			var center = ja_coordinates_to_point(element.geometry.coordinates);
			ja_log(center, 3);
			var distances = [];
			Object.getOwnPropertyNames(nodes).forEach(function(name) {
				ja_log("Checking " + name + " distance", 3);
				var dist = Math.sqrt(Math.pow(nodes[name].attributes.geometry.x - center.x, 2) + Math.pow(nodes[name].attributes.geometry.y - center.y, 2));
				distances.push(dist);
			});
			ja_log(distances, 3);
			ja_log("Mean distance is " + distances.reduce(function(a,b){return a + b;}) / distances.length, 3);

			var circle = window.OpenLayers.Geometry.Polygon.createRegularPolygon(
				center,
					distances.reduce(function(a,b){return a + b;}) / distances.length,
				40,
				0
			);
			var roundaboutCircle = new window.OpenLayers.Feature.Vector(circle, {'ja_type': 'roundaboutoverlay'});
			ja_roundabout_points.push(circle);
			ja_mapLayer.addFeatures([roundaboutCircle]);
		});
	}


	/*
	 * Segment and routing helpers
	 */

	/**
	 * Check if segment in type matches any other segments
	 * @param segment_in
	 * @param segments
	 * @returns {boolean}
	 */
	function ja_segment_type_match(segment_in, segments) {
		ja_log(segment_in, 2);
		ja_log(segments, 2);

		return Object.getOwnPropertyNames(segments).some(function (segment_n_id, index, array) {
			var segment_n = segments[segment_n_id];
			ja_log("PT Checking element " + index, 2);
			ja_log(segment_n, 2);
			if(segment_n.attributes.id == segment_in.attributes.id) return false;
			ja_log("PT checking sn.rt " + segment_n.attributes.roadType +
				" vs i.pt: " + segment_in.attributes.roadType, 2);
			return (segment_n.attributes.roadType == segment_in.attributes.roadType);
		});
	}

	function ja_is_primary_road(seg) {
		var t = seg.attributes.roadType;
		return t == ja_road_type.FREEWAY || t == ja_road_type.MAJOR_HIGHWAY || t == ja_road_type.MINOR_HIGHWAY;
	}

	function ja_is_ramp(seg) {
		var t = seg.attributes.roadType;
		return t == ja_road_type.RAMP;
	}

	function ja_is_turn_allowed(s_from, via_node, s_to) {
		ja_log("Allow from " + s_from.attributes.id + " to " + s_to.attributes.id + " via " + via_node.attributes.id + "? "
			+ via_node.isTurnAllowedBySegDirections(s_from, s_to) + " | " + s_from.isTurnAllowed(s_to, via_node), 2);

		//Is there a driving direction restriction?
		if(!via_node.isTurnAllowedBySegDirections(s_from, s_to)) {
			ja_log("Driving direction restricion applies", 3);
			return false;
		}

		//Is turn allowed by other means (e.g. turn restrictions)?
		if(!s_from.isTurnAllowed(s_to, via_node)) {
			ja_log("Other restricion applies", 3);
			return false;
		}

		if(s_to.attributes.fromNodeID == via_node.attributes.id) {
			ja_log("FWD direction",3);
			return ja_is_car_allowed_by_restrictions(s_to.attributes.fwdRestrictions);
		} else {
			ja_log("REV direction",3);
			return ja_is_car_allowed_by_restrictions(s_to.attributes.revRestrictions);
		}
	}

	function ja_is_car_allowed_by_restrictions(restrictions) {
		ja_log("Checking restrictions for cars", 2);
		if(typeof restrictions === 'undefined' || restrictions == null || restrictions.length == 0) {
			ja_log("No car type restrictions to check...", 3);
			return true;
		}
		ja_log(restrictions, 3);

		return !restrictions.some(function(element, index, array) {
			ja_log("Checking restriction " + element, 3);
			var ret = element.allDay //All day restriction
				&& element.days == 127	//Every week day
				&& ( element.vehicleTypes == -1 //All vehicle types
					|| element.vehicleTypes & ja_vehicle_types.PRIVATE //or at least private cars
					);
			if (ret) {
				ja_log("There is an all-day-all-week restriction", 3);
				var fromDate = Date.parse(element.fromDate);
				var toDate = Date.parse(element.toDate);
				ja_log("From: " + fromDate + ", to: " + toDate + ". " + ret, 3);
				if(isNaN(fromDate && isNaN(toDate))) {
					ja_log("No start nor end date defined");
					return false;
				}
				var fRes, tRes;
				if(!isNaN(fromDate) && new Date() > fromDate) {
					ja_log("From date is in the past", 3);
					fRes = 2;
				} else if(isNaN(fromDate)) {
					ja_log("From date is invalid/not set", 3);
					fRes = 1;
				} else {
					ja_log("From date is in the future: " + fromDate, 3);
					fRes = 0;
				}
				if(!isNaN(toDate) && new Date() < toDate) {
					ja_log("To date is in the future", 3);
					tRes = 2;
				} else if(isNaN(toDate)) {
					ja_log("To date is invalid/not set", 3);
					tRes = 1;
				} else {
					ja_log("To date is in the past: " + toDate, 3);
					tRes = 0;
				}
				// Car allowed unless
				//  - toDate is in the future and fromDate is unset or in the past
				//  - fromDate is in the past and toDate is unset in the future
				// Hope I got this right ;)
				return (fRes <= 1 && tRes <= 1);
			}
			return ret;
		});
	}

	/**
	 * From wiki:
	 * A Cross-match is when the primary name of one segment is identical to the alternate name of an adjacent segment. It had the same priory as a Primary name match.
	 * In order for a Cross match to work there must be at least one alt name on both involved segments (even though they don't necessarily match each other).
	 * It will work even if the are no Primary names on those segments.
	 * It will not work if all three segments at a split have a matching Primary name or a matching Alternate name.
	 * @param street_in
	 * @param streets
	 * @returns {boolean}
	 */
	function ja_cross_name_match(street_in, streets) {
		ja_log("CN: init", 2);
		ja_log(street_in, 2);
		ja_log(streets, 2);
		return Object.getOwnPropertyNames(streets).some(function (street_n_id, index, array) {
			var street_n_element = streets[street_n_id];
			ja_log("CN: Checking element " + index, 2);
			ja_log(street_n_element, 2);
			return (street_in.secondary.some(function (street_in_secondary, index2, array2){
				ja_log("CN2a: checking n.p: " + street_n_element.primary.name + " vs in.s: " + street_in_secondary.name, 2);
				return street_n_element.primary.name == street_in_secondary.name;
			}) || street_n_element.secondary.some(function (street_n_secondary, index2, array2) {
				ja_log("CN2b: checking in.p: " + street_in.primary.name + " vs n.s: " + street_n_secondary.name, 2);
			}));
		});
	}

	function ja_alt_name_match(street_in, streets) {
		return Object.getOwnPropertyNames(streets).some(function (street_n_id, index, array) {
			var street_n_element = streets[street_n_id];
			ja_log("AN alt name check: Checking element " + index, 2);
			ja_log(street_n_element, 2);

			if(street_in.secondary.length == 0) return false;
			if(street_n_element.secondary.length == 0) return false;

			return street_in.secondary.some(function (street_in_secondary, index2, array2) {
				ja_log("AN2 checking element " + index2, 2);
				ja_log(street_in_secondary, 2);
				return street_n_element.secondary.some(function (street_n_secondary_element, index3,  array3) {
					ja_log("AN3 Checking in.s: " + street_in_secondary.name + " vs n.s." + index3 + ": " + street_n_secondary_element.name, 2);
					return street_in_secondary.name == street_n_secondary_element.name;
				});
			});
		});
	}

	function ja_primary_name_and_type_match(street_in, streets, exceptStreet) {
		ja_log("PNT", 2);
		ja_log(street_in, 2);
		return Object.getOwnPropertyNames(streets).some(function (id, index, array) {
			ja_log("PNT Checking element " + index, 2);
			ja_log(streets[id], 2);
			ja_log("Checking exception", 2);
			ja_log(exceptStreet, 2);
			var exempt = exceptStreet !== undefined && exceptStreet[id] != null && exceptStreet[id] != id;
			ja_log(exempt, 2);
			return (!exempt && streets[id].primary.name == street_in.primary.name
				&& streets[id].primary.type == street_in.primary.type);
		});
	}

	function ja_primary_name_match(street_in, streets) {
		ja_log("PN", 2);
		ja_log(street_in, 2);
		ja_log(streets, 2);
		return Object.getOwnPropertyNames(streets).some(function (id, index, array) {
			element = streets[id];
			ja_log("PN Checking element " + index + " of " + array.length, 2);
			ja_log(element, 2);
			return (element.primary.name == street_in.primary.name);
		});
	}

	function ja_get_streets(segmentId) {
		var primary = window.Waze.model.streets.objects[window.Waze.model.segments.objects[segmentId].attributes.primaryStreetID];
		var secondary = [];
		window.Waze.model.segments.objects[segmentId].attributes.streetIDs.forEach(function (element, index, array) {
			secondary.push(window.Waze.model.streets.objects[element]);
		});
		ja_log(primary, 3);
		ja_log(secondary, 3);
		return { primary: primary, secondary: secondary };
	}



	/*
	 * Misc math and map element functions
	 */

	/**
	 *
	 * @param p0 From point
	 * @param p1 Center point
	 * @param p2 To point
	 * @returns {number}
	 */
	function ja_angle_between_points(p0,p1,p2) {
		ja_log("p0 " + p0,3);
		ja_log("p1 " + p1,3);
		ja_log("p2 " + p2,3);
		var a = Math.pow(p1.x-p0.x,2) + Math.pow(p1.y-p0.y,2);
		var b = Math.pow(p1.x-p2.x,2) + Math.pow(p1.y-p2.y,2);
		var c = Math.pow(p2.x-p0.x,2) + Math.pow(p2.y-p0.y,2);
		var angle = Math.acos((a+b-c) / Math.sqrt(4*a*b)) / (Math.PI / 180);
		ja_log("angle is " + angle,3);
		return angle;
	}

	/**
	 * get absolute (or turn) angle between 2 inputs.
	 * 0,90,true  -> 90	 0,90,false -> -90
	 * 0,170,true -> 170	0,170,false -> -10
	 * @param aIn absolute s_in angle (from node)
	 * @param aOut absolute s_out angle (from node)
	 * @param absolute return absolute or turn angle?
	 * @returns {number}
	 */
	function ja_angle_diff(aIn, aOut, absolute) {
		var a = parseFloat(aOut) - parseFloat(aIn);
		if(a > 180) a -= 360;
		if(a < -180) a+= 360;
		return absolute ? a : (a > 0 ? a - 180 : a + 180);
	}

	function ja_is_roundabout_normal(junctionID, n_in) {
		ja_log("Check normal roundabout", 3);
		var junction = window.Waze.model.junctions.get(junctionID);
		var nodes = {};
		var numValidExits = 0;
		junction.segIDs.forEach(function(element, index, array){
			var s = window.Waze.model.segments.get(element);
			ja_log("index: " + index, 3);
			//ja_log(s, 3);
			if(!nodes.hasOwnProperty(s.attributes.toNodeID)) {
				ja_log("Adding node id: " + s.attributes.toNodeID, 3);
				//Check if node has allowed exits
				var allowed = false;
				var currNode = window.Waze.model.nodes.get(s.attributes.toNodeID);
				ja_log(currNode, 3);
				currNode.attributes.segIDs.forEach(function(element2, index2, array2){
					var s_exit = window.Waze.model.segments.get(element2);
					ja_log(s_exit, 3);
					if(s_exit.attributes.junctionID !== null) {
						//part of the junction.. Ignoring
						ja_log(s_exit.attributes.id + " is in the roundabout. ignoring", 3);
					} else {
						ja_log("Checking: " +s_exit.attributes.id, 3);
						if(currNode.isTurnAllowedBySegDirections(s, s_exit)) {
							//Exit possibly allowed
							ja_log("YAY", 3);
							allowed = true;
						} else {
							ja_log("NAY", 3);
						}
					}
				});
				if(allowed) {
					numValidExits++;
					nodes[s.attributes.toNodeID] = window.Waze.model.nodes.get(s.attributes.toNodeID);
				}
			}
		});

		var is_normal = true;
		ja_log(n_in, 3);
		ja_log(junction, 3);
		ja_log(nodes, 3);

		//If we have more than 4 possible exits, the roundabout is non-normal, and we don't want to paint the offending angles.
		if(numValidExits > 4) return false;

		for(var n in nodes) {
			ja_log("Checking " + n, 3);
			if(n == n_in) {
				ja_log("Not comparing to n_in ;)", 3);
			} else {
				var angle = ja_angle_between_points(
					window.Waze.model.nodes.get(n_in).geometry,
					ja_coordinates_to_point(junction.geometry.coordinates),
					window.Waze.model.nodes.get(n).geometry
				);
				ja_log("Angle is: " + angle, 3);
				ja_log("Normalized angle is: " + (angle%90), 3);
				//angle = Math.abs((angle%90 - 90))
				angle = Math.abs((angle%90))
				ja_log("Angle is: " + angle, 3);
				// 90 +/- 15 is considered "normal"
				if(angle <= 15 || 90-angle <= 15) {
					ja_log("turn is normal", 3);
				} else {
					ja_log("turn is NOT normal", 3);
					is_normal = false;
					//Push a marker on the node to show which exit is "not normal"
					ja_mapLayer.addFeatures([
							new window.OpenLayers.Feature.Vector(
								window.Waze.model.nodes.get(n).geometry,
								{
									angle: '±' + ja_round(Math.min(angle, 90-angle)),
									ja_type: ja_routing_type.ROUNDABOUT
								}
							)]
					);
				}
			}
		}
		return is_normal;
	}


	/**
	 * Helper to get get correct projections for roundabout center point
	 */

	function ja_coordinates_to_point(coordinates) {
		return window.OpenLayers.Projection.transform(
			new window.OpenLayers.Geometry.Point(
				coordinates[0],
				coordinates[1]
			),
			"EPSG:4326",
			ja_mapLayer.projection.projCode
		);
	}

	function ja_get_first_point(segment) {
		return segment.geometry.components[0];
	}

	function ja_get_last_point(segment) {
		return segment.geometry.components[segment.geometry.components.length - 1];
	}

	function ja_get_second_point(segment) {
		return segment.geometry.components[1];
	}

	function ja_get_next_to_last_point(segment) {
		return segment.geometry.components[segment.geometry.components.length - 2];
	}

	//get the absolute angle for a segment end point
	function ja_getAngle(ja_node, ja_segment) {
		ja_log("node: " + ja_node, 2);
		ja_log("segment: " + ja_segment, 2);
		if (ja_node == null || ja_segment == null) return null;
		if (ja_segment.attributes.fromNodeID == ja_node) {
			ja_dx = ja_get_second_point(ja_segment).x - ja_get_first_point(ja_segment).x;
			ja_dy = ja_get_second_point(ja_segment).y - ja_get_first_point(ja_segment).y;
		} else {
			ja_dx = ja_get_next_to_last_point(ja_segment).x - ja_get_last_point(ja_segment).x;
			ja_dy = ja_get_next_to_last_point(ja_segment).y - ja_get_last_point(ja_segment).y;
		}
		ja_log(ja_node + " / " + ja_segment + ": dx:" + ja_dx + ", dy:" + ja_dy, 2);
		ja_angle = Math.atan2(ja_dy, ja_dx);
		return ((ja_angle * 180 / Math.PI)) % 360;
	}
	
	/**
	 * Decimal adjustment of a number. Borrowed (with some modifications) from
	 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
	 * ja_round(55.55, -1); // 55.6
	 * ja_round(55.549, -1); // 55.5
	 * ja_round(55, 1); // 60
	 * ja_round(54.9, 1); // 50
	 *
	 * @param	{String}	type	The type of adjustment.
	 * @param	{Number}	value	The number.
	 * @param	{Integer}	exp		The exponent (the 10 logarithm of the adjustment base).
	 * @returns	{Number}			The adjusted value.
	 */
	function ja_round(value) {
		// If the exp is undefined or zero...
		var ja_rounding = -parseInt(ja_getOption("decimals"));
		if (typeof ja_rounding === 'undefined' || +ja_rounding === 0) {
			return Math.round(value);
		}
		value = +value;
		// If the value is not a number or the exp is not an integer...
		if (isNaN(value) || !(typeof ja_rounding === 'number' && ja_rounding % 1 === 0)) {
			return NaN;
		}
		// Shift
		value = value.toString().split('e');
		value = Math.round(+(value[0] + 'e' + (value[1] ? (+value[1] - ja_rounding) : -ja_rounding)));
		// Shift back
		value = value.toString().split('e');
		return +(value[0] + 'e' + (value[1] ? (+value[1] + ja_rounding) : ja_rounding));
	}


	/*
	 * WME interface helper functions
	 */

	function ja_getOption(name) {
		ja_log("Loading option: " + name, 2);
		if(!ja_options.hasOwnProperty(name) || typeof ja_options[name] === 'undefined') {
			ja_options[name] = ja_settings[name]['defaultValue'];
		}
		
		ja_log("Got value: " + ja_options[name], 2);
		return ja_options[name];
	}

	function ja_setOption(name, val) {
		ja_options[name] = val;
		if(localStorage) {
			localStorage.setItem("wme_ja_options", JSON.stringify(ja_options));
		}
		ja_log(ja_options,3);
	}

	var ja_onchange = function(e) {
		"use strict";
		switch(e.id) {
			case ja_settings['guess'].elementId:
				Object.getOwnPropertyNames(ja_settings).forEach(function (a,b,c) {
					var setting = ja_settings[a];
					if(setting['group'] && setting['group'] == 'guess') {
						ja_log(a + ": " + !e.checked , 3);
						document.getElementById(setting["elementId"]).disabled = !e.checked;
						//document.getElementById(setting["elementId"]).parentNode.disabled = !e.checked;
						document.getElementById(setting["elementId"]).parentNode.style.color =
							e.checked ? "black" : "lightgrey";
					}
				});
				break;
			case ja_settings['roundaboutOverlayDisplay'].elementId:
				Object.getOwnPropertyNames(ja_settings).forEach(function (a,b,c) {
					var setting = ja_settings[a];
					if(setting['group'] && setting['group'] == 'roundaboutOverlayDisplay') {
						ja_log(a +": " + e.value, 0);
						document.getElementById(setting["elementId"]).disabled = e.value == "rOverNever";
						//document.getElementById(setting["elementId"]).parentNode.disabled = !e.checked;
						document.getElementById(setting["elementId"]).parentNode.style.color =
							e.value != "rOverNever" ? "black" : "lightgrey";
					}
				});
				break;
			default:
				ja_log("Nothing to do for " + e.id, 0);
		}
	};

	var ja_load = function loadJAOptions() {
		ja_log("Should load settings now.", 2);
		if(localStorage != null) {
			ja_log("We have local storage! =)",2);
			try {
				ja_options = JSON.parse(localStorage.getItem("wme_ja_options"));
			} catch (e){
				ja_log("Loading settings failed.. " + e.message, 2);
				ja_options = null;
			}
		}
		if(ja_options == null) {
			ja_reset();
		} else {
			ja_log(ja_options, 2);
			setTimeout(function(){ja_apply();}, 500);
		}
	};

	var ja_save = function saveJAOptions() {
		ja_log("Saving settings", 2);
		Object.getOwnPropertyNames(ja_settings).forEach(function (a,b,c) {
			var setting = ja_settings[a];
			ja_log(setting, 2);
			switch (setting['elementType']) {
				case "checkbox":
					ja_setOption(a, document.getElementById(setting['elementId']).checked);
					break;
				case "color":
					var re = /^#[0-9a-f]{6}$/;
					if(re.test(document.getElementById(setting['elementId']).value)) {
						ja_setOption(a, document.getElementById(setting['elementId']).value);
					} else {
						ja_setOption(a, ja_settings[a]['default']);
					}
					break;
				case "number":
					var val = document.getElementById(setting['elementId']).value;
					if(!isNaN(val) && val == parseInt(val) && val >= setting['min'] && val <= setting['max']) {
						ja_setOption(a, document.getElementById(setting['elementId']).value);
					} else {
						ja_setOption(a, ja_settings[a]['default']);
					}
					break;
				case "text":
				case "select":
					ja_setOption(a, document.getElementById(setting['elementId']).value);
					break;
			}
		});
		ja_apply();
		return false;
	};

	var ja_apply = function applyJAOptions() {
		ja_log("Applying stored (or default) settings", 2);
		if(typeof window.Waze.map.getLayersBy("uniqueName","junction_angles")[0] === 'undefined') {
			ja_log("WME not ready yet, trying again in 400 ms", 2);
			setTimeout(function(){ja_apply();}, 400);
			return;
		}
		if(document.getElementById("sidepanel-ja") != null) {
			ja_log(Object.getOwnPropertyNames(ja_settings), 2);
			Object.getOwnPropertyNames(ja_settings).forEach(function (a,b,c) {
				var setting = ja_settings[a];
				ja_log(a, 2);
				ja_log(setting, 2);
				ja_log(document.getElementById(setting['elementId']), 2);
				switch (setting['elementType']) {
					case "checkbox":
						document.getElementById(setting['elementId']).checked = ja_getOption(a);
						document.getElementById(setting['elementId']).onchange();
						break;
					case "color":
					case "number":
					case "text":
						document.getElementById(setting['elementId']).value = ja_getOption(a);
						break;
					case "select":
						document.getElementById(setting['elementId']).value = ja_getOption(a);
						document.getElementById(setting['elementId']).onchange();
						break;
				}
			});
		} else {
			ja_log("WME not ready (no settings tab)", 2);
		}
		window.Waze.map.getLayersBy("uniqueName","junction_angles")[0].styleMap = ja_style();
		ja_calculate_real();
		ja_log(ja_options, 2);
	};

	var ja_reset = function resetJAOptions() {
		ja_log("Resetting settings", 2);
		if(localStorage != null) {
			localStorage.removeItem("wme_ja_options");
		}
		ja_options = {};
		ja_apply();
		return false;
	};

	function ja_helplink(url, text) {
		var elem = document.createElement('li');
		var l = document.createElement('a');
		l.href = url;
		l.target = "_blank";
		l.appendChild(document.createTextNode(ja_getMessage(text)));
		elem.appendChild(l);
		return elem;
	}

	var ja_calculation_timer = {
		start: function() {
			ja_log("Starting timer", 2);
			this.cancel();
			var ja_calculation_timer_self = this;
			this.timeoutID = window.setTimeout(function(){ja_calculation_timer_self.calculate();}, 200);
		},

		calculate: function() {
			ja_calculate_real();
			delete this.timeoutID;
		},

		cancel: function() {
			if(typeof this.timeoutID == "number") {
				window.clearTimeout(this.timeoutID);
				ja_log("Cleared timeout ID" + this.timeoutID, 2);
				delete this.timeoutID;
			}
		}

	};

	function ja_calculate() {
		ja_calculation_timer.start();
	}

	function ja_get_style_rule(routingType, fillColorOption) {
		return new window.OpenLayers.Rule(
			{
				filter: new window.OpenLayers.Filter.Comparison({
					type: window.OpenLayers.Filter.Comparison.EQUAL_TO,
					property: "ja_type",
					value: routingType
				}),
				symbolizer: {
					pointRadius: 3 + parseInt(ja_getOption("pointSize"), 10) + (parseInt(ja_getOption("decimals")) > 0 ? 5 * parseInt(ja_getOption("decimals")) : 0),
					fontSize: "12px",
					fillColor: ja_getOption(fillColorOption),
					strokeColor: "#183800"
				}
			})
	}

	function ja_style() {
		ja_log("Point radius will be: " + (parseInt(ja_getOption("pointSize"), 10)) + (parseInt(ja_getOption("decimals") > 0 ? 5 * parseInt(ja_getOption("decimals")) : 0)), 2);
		return new window.OpenLayers.Style({
			fillColor: "#ffcc88",
			strokeColor: "#ff9966",
			strokeWidth: 2,
			label: "${angle}",
			fontWeight: "bold",
			pointRadius: parseInt(ja_getOption("pointSize"), 10) + (parseInt(ja_getOption("decimals")) > 0 ? 5 * parseInt(ja_getOption("decimals")) : 0),
			fontSize: "10px"
		}, {
			rules: [
				new window.OpenLayers.Rule({
					symbolizer: {
					}
				}),
				ja_get_style_rule(ja_routing_type.TURN, "turnInstructionColor"),
				ja_get_style_rule(ja_routing_type.BC, "noInstructionColor"),
				ja_get_style_rule(ja_routing_type.KEEP, "keepInstructionColor"),
				ja_get_style_rule(ja_routing_type.EXIT, "exitInstructionColor"),
				ja_get_style_rule(ja_routing_type.PROBLEM, "problemColor"),
				ja_get_style_rule(ja_routing_type.ERROR, "problemColor"),
				ja_get_style_rule(ja_routing_type.ROUNDABOUT, "roundaboutColor"),

				new window.OpenLayers.Rule(
					{
						filter: new window.OpenLayers.Filter.Comparison({
							type: window.OpenLayers.Filter.Comparison.EQUAL_TO,
							property: "ja_type",
							value: "roundaboutoverlay"
						}),
						symbolizer: {
							pointRadius: 3 + parseInt(ja_getOption("pointSize"), 10) + (parseInt(ja_getOption("decimals")) > 0 ? 5 * parseInt(ja_getOption("decimals")) : 0),
							fontSize: "12px",
							fillColor: ja_getOption("roundaboutOverlayColor"),
							fillOpacity: 0.1,
							strokeColor: ja_getOption("roundaboutOverlayColor"),
							label: ""
						}
					})

			]
		});
	}


	/*
	 * Translation helpers
	 */

	function ja_getMessage(key) {
		return I18n.translate('ja.' + key);
	}
	
	function ja_loadTranslations() {
		ja_log("Loading translations",2);
		I18n.translations[window.I18n.defaultLocale].ja = {};
		def = I18n.translations[window.I18n.defaultLocale].ja;
		sv = {};
		fi = {};
		//Default language (English)
		def["name"] = "Junction Angles";
		def["settingsTitle"] = "Junction Angle settings";
		def["apply"] = "Apply";
		def["resetToDefault"] = "Reset to default";
		def["aAbsolute"] = "Absolute";
		def["aDeparture"] = "Departure";
		def["angleMode"] = "Angle mode";
		def["guess"] = "Estimate routing instructions";
		def["noInstructionColor"] = "Color for best continuation";
		def["keepInstructionColor"] = "Color for keep prompt";
		def["exitInstructionColor"] = "Color for exit prompt";
		def["turnInstructionColor"] = "Color for turn prompt";
		def["problemColor"] = "Color for angles to avoid";
		def["roundaboutColor"] = "Color for roundabouts (with non-straight exits)";
		def["roundaboutOverlayColor"] = "Color for roundabout overlay";
		def["roundaboutOverlayDisplay"] = "Show roundabout circle";
		def["rOverNever"] = "Never";
		def["rOverSelected"] = "When selected";
		def["rOverAlways"] = "Always";
		def["decimals"] = "Number of decimals";
		def["pointSize"] = "Base point size";

		def["roundaboutnav"] = "WIKI: Roundabouts";
		def["ghissues"] = "JAI issue tracker";

		//Finnish (Suomi)
		fi["name"] = "Risteyskulmat";
		fi["settingsTitle"] = "Rysteyskulmien asetukset";
		fi["apply"] = "Aseta";
		fi["resetToDefault"] = "Palauta";
		fi["aAbsolute"] = "Absoluuttinen";
		fi["aDeparture"] = "Käännös";
		fi["angleMode"] = "Kulmien näyttö";
		fi["guess"] = "Arvioi reititysohjeet";
		fi["noInstructionColor"] = "ohjeeton \"Suora\"-väri";
		fi["keepInstructionColor"] = "\"Poistu\"-ohjeen väri";
		fi["exitInstructionColor"] = "\"poistu\"-ohjeen väri";
		fi["turnInstructionColor"] = "\"Käänny\"-ohjeen väri";
		fi["problemColor"] = "Vältettävien kulmien väri";
		fi["roundaboutColor"] = "Liikenneympyrän (jolla ei-suoria kulmia) ohjeen väri";
		fi["roundaboutOverlayColor"] = "Liikenneympyrän korostusväri";
		fi["roundaboutOverlayDisplay"] = "Korosta liikenneympyrä";
		fi["rOverNever"] = "Ei ikinä";
		fi["rOverSelected"] = "Kun valittu";
		fi["rOverAlways"] = "Aina";
		fi["decimals"] = "Desimaalien määrä";
		fi["pointSize"] = "Ympyrän peruskoko";

		//Swedish (Svenska)
		sv["name"] = "Korsningsvinklar";
		sv["settingsTitle"] = "Inställningar för korsningsvinklar";
		sv["apply"] = "Godkänn";
		sv["resetToDefault"] = "Återställ";
		sv["aAbsolute"] = "Absolut";
		sv["aDeparture"] = "Sväng";
		sv["angleMode"] = "Vinkelvisning";
		sv["guess"] = "Gissa navigeringsinstruktioner";
		sv["noInstructionColor"] = "Färg för \"ingen instruktion\"";
		sv["keepInstructionColor"] = "Färg för\"håll höger/vänster\"-instruktion";
		sv["exitInstructionColor"] = "Färg för \"ta av\"-instruktion";
		sv["turnInstructionColor"] = "Färg för \"sväng\"-instruktion";
		sv["problemColor"] = "Färg för vinklar att undvika";
		sv["roundaboutColor"] = "Färg för rondell (med icke-räta vinklar)";
		sv["roundaboutOverlayColor"] = "Färg för rondellcirkel";
		sv["roundaboutOverlayDisplay"] = "Visa cirkel på rondell";
		sv["rOverNever"] = "Aldrig";
		sv["rOverSelected"] = "När vald";
		sv["rOverAlways"] = "Alltid";
		sv["decimals"] = "Decimaler";
		sv["pointSize"] = "Cirkelns basstorlek";
		
		//Apply
		switch (I18n.locale) {
			case 'sv':
				I18n.translations['sv'].ja = sv;
				break;
			case 'fi':
				I18n.translations['fi'].ja = fi;
				break;
		}
	}


	/*
	 * Bootstrapping and logging
	 */

	function ja_bootstrap(retries) {
		retries = retries || 0;
		//If Waze has not been defined in 10 seconds, it probably won't work anyway.
		if(retries >= 10) {
			ja_log("Failed to bootstrap 10 times. Giving up.", 0);
			return;
		}

		try {
			if ((typeof window.Waze.map !== 'undefined') && ('undefined' !== typeof window.Waze.map.events.register) &&
				('undefined' !== typeof window.Waze.selectionManager.events.register ) &&
				('undefined' !== typeof window.Waze.loginManager.events.register)) {
				setTimeout(function(){junctionangle_init();}, 500);
			} else {
				setTimeout(function(){ja_bootstrap(++retries);}, 1000);
			}
		} catch (err) {
			setTimeout(function(){ja_bootstrap(++retries);}, 1000);
		}
	}

	function ja_log(ja_log_msg, ja_log_level) {

		//##NO_FF_START##
		if(typeof ja_log_level === 'undefined') ja_log_level = 1;
		if (ja_log_level <= junctionangle_debug) {
			if (typeof ja_log_msg == "object") {
				console.debug(ja_log_msg);
			}
			else {
				console.debug("WME Junction Angle: " + ja_log_msg);
			}
		}
		//##NO_FF_END##
	}

	ja_bootstrap();

}

//Dynamically create, add and run the script in the real page context. We really do need access to many of the objects...
var DLscript = document.createElement("script");
DLscript.textContent = '' +
	run_ja.toString() + ' \n' +
	'run_ja();';
DLscript.setAttribute("type", "application/javascript");
document.body.appendChild(DLscript);