WME Junction Angle Info

Show the angle between two selected (and connected) segments

目前為 2015-04-23 提交的版本,檢視 最新版本

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 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.9.0
// @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" <?>
 *	2015 "wlodek76" <?>
 */

/*jshint eqnull:true, nonew:true, nomen:true, curly:true, latedef:true, unused:strict, noarg:true, loopfunc:true */
/*jshint trailing:true, forin:true, noempty:true, maxparams:7, maxerr:100, eqeqeq:true, strict:true, undef:true */
/*jshint bitwise:true, newcap:true, immed:true, onevar:true, browser:true, nonbsp:true, freeze:true */
/*global I18n, console*/


function run_ja() {
	"use strict";

	/*
	 * First some variable and enumeration definitions
	 */
	var junctionangle_version = "1.9.0";

	var junctionangle_debug = 1;	//0: no output, 1: basic info, 2: debug 3: verbose debug, 4: insane debug

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

	var ja_routing_type = {
		BC: "junction_none",
		KEEP: "junction_keep",
		KEEP_LEFT: "junction_keep_left",
		KEEP_RIGHT: "junction_keep_right",
		TURN: "junction",
		// UNUSED? FZ69617: now we have a display logic implemented for it, but currently I cannot predict whether
		// we'll need it or not
		EXIT: "junction_exit",
		EXIT_LEFT: "junction_exit_left",
		EXIT_RIGHT: "junction_exit_right",
		PROBLEM: "junction_problem",
		ERROR: "junction_error",
		ROUNDABOUT: "junction_roundabout",
		ROUNDABOUT_EXIT: "junction_roundabout_exit"
	};

	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,
		PEDESTRIAN_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: "aDeparture", options: ["aAbsolute", "aDeparture"]},
		angleDisplay: { elementType: "select", elementId: "_jaSelAngleDisplay", defaultValue: "displayFancy", options: ["displayFancy", "displaySimple"]},
		angleDisplayArrows: { elementType: "select", elementId: "_jaSelAngleDisplayArrows", defaultValue: "<>", options: ["<>", "⇦⇨", "⇐⇒", "←→", "⇐⇒⇖⇗", "←→↖↗"]},
		guess: { elementType: "checkbox", elementId: "_jaCbGuessRouting", defaultValue: true },
		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}
	};

	var ja_arrow = {
		get_arrow: function(at) {
			var arrows = ja_getOption("angleDisplayArrows");
			return arrows[at % arrows.length];
		},
		left: function() { return this.get_arrow(0); },
		right: function() { return this.get_arrow(1); },
		left_up: function() { return this.get_arrow(2); },
		right_up: function() { return this.get_arrow(3); }
	};

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

		//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) {
			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_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_controls_container.appendChild(ja_input);
					break;
			}

			ja_input.onchange = function() { ja_onchange(this); };

			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_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_reset_button);

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

		var userTabs = document.getElementById('user-info');
		var navTabs = userTabs.getElementsByClassName('nav-tabs')[0];
		var tabContent = userTabs.getElementsByClassName('tab-content')[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);

		//Add some useful links
		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);

		tabContent.appendChild(ja_settings_dom);

		var 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){
			return element[1] === s_in_a;
		});
		s_out_a = window.$.grep(angles, function(element){
			return element[1] === s_out_a;
		});

		var s_n = {}, s_in = null, s_out = {}, street_n = {}, street_in = null;
		node.attributes.segIDs.forEach(function(element) {
			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);
		if (s_in === null || street_in === null) {
			//Should never happen, but adding to make code validation happy
			return ja_routing_type.PROBLEM;
		}

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

		//Check turn possibility first
		if(!ja_is_turn_allowed(s_in, node, s_out[s_out_id])) {
			ja_log("Turn is disallowed!", 2);
			return ja_routing_type.ERROR;
		}

		//Roundabout - no true instruction guessing here!
		if (s_in.attributes.junctionID) {
			if (s_out[s_out_id].attributes.junctionID) {
				ja_log("Roundabout continuation - no instruction", 2);
				return ja_routing_type.BC;
			} else {
				ja_log("Roundabout exit - no instruction", 2);
				//exit just to visually distinguish from roundabout continuation
				return ja_routing_type.ROUNDABOUT_EXIT;
			}
		} else if (s_out[s_out_id].attributes.junctionID) {
			ja_log("Roundabout entry - no instruction", 2);
			//no instruction since it's normally the only continuation - true instruction can be computed for
			//entry-exit selection only
			return ja_routing_type.BC;
		}

		//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(Math.abs(angle) <= 44) {
			ja_log("Turn is <= 44", 2);

			/*
			 * Filter out disallowed and non-"BC eligible" turns.
			 */
			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) {
				ja_log("Filtering angle: " + ja_angle_diff(s_in_a, a[0], false), 2);
				if(s_out_id === a[1] ||
					(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 //Any angle above 45 is not eligible
						)) {
					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 logic
			 */
			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 (prio > bc_prio) { //highest priority wins now
					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);
			};

			//Check each eligible turn against routing rules
			for(var 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(ja_primary_name_match(street_in, tmp_street_out) && ja_segment_type_match(s_in, tmp_s_out)) {
					ja_log("BC primary name and type match", 2);
					bc_collect(a, 3);
				} else if(ja_alt_name_match(street_in, tmp_street_out) && ja_segment_type_match(s_in, tmp_s_out)) {
					ja_log("BC alt name and type match", 2);
					bc_collect(a, 3);
				} else if(ja_primary_name_match(street_in, tmp_street_out) || ja_cross_name_match(street_in, tmp_street_out)) {
					ja_log("BC primary name or cross name match", 2);
					bc_collect(a, 2);
				} else if(ja_alt_name_match(street_in, tmp_street_out)) {
					ja_log("BC alt name match", 2);
					bc_collect(a, 2);
				} else if(ja_segment_type_match(s_in, tmp_s_out)) {
					ja_log("BC type match", 2);
					bc_collect(a, 1);
				}
				//Else: Non-BC
			}

			//If s-out is the only BC, that's it.
			if (bc_matches[s_out_id] !== undefined && bc_count === 1) {
				ja_log("\"straight\": no instruction", 2);
				return ja_routing_type.BC;
			}

			ja_log("BC logic did not apply; using old default rules instead.", 2);

			// wlodek76: FIXING KEEP LEFT/RIGHT regarding to left most segment
			// WIKI WAZE: When there are more than two segments less than 45.04°, only the left most segment will be
			// KEEP LEFT, all the rest will be KEEP RIGHT
			if (true || angles.length > 2) { //FZ69617: "more than two..."
						//FIXME: 'true' added to temporarily ignore this condition
						//without this many "keep left"s changed into "exit right"
						//but I'm finally not sure whether we can safely ignore the precondition from Wiki?

				//FZ69617: Sort angles in left most first order
				ja_log("Unsorted angles", 4);
				ja_log(angles, 4);
				angles.sort(function(a, b) { return angle_to_s_in(a[0], s_in_a[0][0]) - angle_to_s_in(b[0],s_in_a[0][0]); });
				ja_log("Sorted angles", 4);
				ja_log(angles, 4);

				if (angles[0][1] === s_out_id) { //s-out is left most segment

					//wlodek76: KEEP LEFT/RIGHT overlapping case
					//WIKI WAZE: If the left most segment is overlapping another segment, it will also be KEEP RIGHT.
					if (!ja_overlapping_angles(angles[0][0], angles[1][0])) {
						ja_log("Left most <45 segment: keep left", 2);
						return ja_routing_type.KEEP_LEFT;
					}
				}
			}

			//FZ69617: Two overlapping segments logic
			//WAZE WIKI: If the only two segments less than 45.04° overlap each other, neither will get an instruction.
			if (angles.length === 2 && ja_overlapping_angles(angles[0][0], angles[1][0])) {
				ja_log("Two overlapping segments: no instruction", 2);
				return ja_routing_type.BC;  //PROBLEM?
			}

			//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);
				return s_in.model.isLeftHand ? ja_routing_type.EXIT_LEFT : ja_routing_type.EXIT_RIGHT;
			}

			//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);
				return s_in.model.isLeftHand ? ja_routing_type.EXIT_LEFT : ja_routing_type.EXIT_RIGHT;
			}

			ja_log("DEFAULT: keep right", 2);
			return ja_routing_type.KEEP_RIGHT;
		} 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) {
			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, tmp_n = null, 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("Roundabout detected: " + 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) {
			if (ja_selected_roundabouts.hasOwnProperty(tmp_roundabout)) {
				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++) {
			var 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);
			ja_log(ja_current_node_segments, 3);

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

			ja_current_node_segments.forEach(function (nodeSegment, j) {
				var 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) {
				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!", 4);
					//find the angle
					for(var j=0; j < angles.length; j++) {
						if(angles[j][1] === selectedSegmentId) {
							ja_selected_angles.push(angles[j]);
							break;
						}
					}
				} else {
					ja_log("It's not..", 4);
				}
			});


			ja_log(angles, 3);

			var ja_label_distance;
			/*
			 * Define a base distance to markers, depending on the zoom level
			 */
			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 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, 3);

				//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);
				}
				//get the initial marker 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) {
						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 &&
						a >=180 &&
						ja_getOption("angleMode") !== "aDeparture"
						) {
						ja_log("Skipping marker, as we need only one of them", 2);
						return;
					}
					if(ja_getOption("angleMode") === "aDeparture" && ja_selected_segments_count > 0) {
						if(a_in[1] === angle[1]) {
							ja_log("in == out. skipping.", 2);
							return;
						}
						ja_log("Angle in:",2);
						ja_log(a_in,2);
						ja_log(ja_guess_routing_instruction(node, a_in[1], angle[1], angles), 2);
						//FIXME: we might want to try to keep the marker on the segment, instead of just
						//in the direction of the first part
						ha = angle[0];
						a = ja_angle_diff(a_in[0], angles[j][0], false);
						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_getOption("guess") ?
								ja_guess_routing_instruction(node, a_in[1], angle[1], angles) : ja_routing_type.TURN);
					} 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) {

		//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){
			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;
		})) {
			//add 1/4 of the original distance and hope for the best =)
			ja_tmp_distance = ja_tmp_distance + ja_label_distance / 4;
			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("Distance estimation done", 3);

		var angleString = ja_round(Math.abs(a)) + "°";

		//FZ69617: Add direction arrows for turn instructions only
		if (ja_getOption("angleDisplay") === "displaySimple") {
			switch(ja_junction_type) {
				case ja_routing_type.TURN:
					angleString = a > 0 ? ja_arrow.left() + angleString : angleString + ja_arrow.right();
					break;
				case ja_routing_type.EXIT:
				case ja_routing_type.KEEP:
					angleString = a > 0 ? ja_arrow.left_up() + angleString : angleString + ja_arrow.right_up();
					break;
				case ja_routing_type.EXIT_LEFT:
				case ja_routing_type.KEEP_LEFT:
					angleString = ja_arrow.left_up() + angleString;
					break;
				case ja_routing_type.EXIT_RIGHT:
				case ja_routing_type.KEEP_RIGHT:
					angleString = angleString + ja_arrow.right_up();
					break;
			}
		} else {
			switch(ja_junction_type) {
				case ja_routing_type.TURN:
					angleString = (a > 0 ? ja_arrow.left() : ja_arrow.right()) + "\n" + angleString;
					break;
				case ja_routing_type.EXIT:
				case ja_routing_type.KEEP:
					angleString = (a > 0 ? ja_arrow.left_up() : ja_arrow.right_up()) + "\n" + angleString;
					break;
				case ja_routing_type.EXIT_LEFT:
				case ja_routing_type.KEEP_LEFT:
					angleString = ja_arrow.left_up() + "\n" + angleString;
					break;
				case ja_routing_type.EXIT_RIGHT:
				case ja_routing_type.KEEP_RIGHT:
					angleString = ja_arrow.right_up() + "\n" + angleString;
					break;
			}
		}
		var anglePoint = withRouting ?
			new window.OpenLayers.Feature.Vector(
				point,
				{ angle: angleString, 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 (roundaboutPoint){
			return roundaboutPoint.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){
			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) {
			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 restriction 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 restriction 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) {
			/*jshint bitwise: false*/
			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) {
			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){
				ja_log("CN2a: checking n.p: " + street_n_element.primary.name +
					" vs in.s: " + street_in_secondary.name, 2);

				//wlodek76: CROSS-MATCH works when two compared segments contain at least one ALT NAME
				//when alt name is empty cross-match does not work
				if (street_n_element.secondary.length === 0) { return false; }

				return street_n_element.primary.name === street_in_secondary.name;
			}) || street_n_element.secondary.some(function (street_n_secondary) {
				ja_log("CN2b: checking in.p: " + street_in.primary.name + " vs n.s: " + street_n_secondary.name, 2);

				//wlodek76: CROSS-MATCH works when two compared segments contain at least one ALT NAME
				//when alt name is empty cross-match does not work
				if (street_in.secondary.length === 0) { return false; }

				//wlodek76: missing return from checking primary name with alternate names
				return street_in.primary.name === street_n_secondary.name;
			}));
		});
	}

	function ja_alt_name_match(street_in, streets) {
		return Object.getOwnPropertyNames(streets).some(function (street_n_id, index) {
			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) {
				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) {
					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_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) {
			var 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) {
			secondary.push(window.Waze.model.streets.objects[element]);
		});
		ja_log(primary, 3);
		ja_log(secondary, 3);
		return { primary: primary, secondary: secondary };
	}

	/**
	 * Checks whether the two segments (connected at the same node) overlap each other.
	 * @param a1 Angle of the 1st segment
	 * @param a2 Angle of the 2nd segment
	 */
	function ja_overlapping_angles(a1, a2) {
		// If two angles are close < 2 degree they are overlapped.
		// Method of recognizing overlapped segment by server is unknown for me yet, I took this from WME Validator
		// information about this.
		// TODO: verify overlapping check on the side of routing server.
		return Math.abs(ja_angle_diff(a1, a2, true)) < 2.0;
	}


	/*
	 * 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 angle_to_s_in(a, s_in_angle) {
		ja_log("Comparing out-angle " + a + " to in-angle " + s_in_angle, 4);
		var diff = ja_angle_diff(a, s_in_angle, true);
		ja_log("Diff is " + diff + ", returning: " + (diff < 0 ? diff + 360 : diff), 4);
		return diff < 0 ? diff + 360 : diff;
	}

	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) {
			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) {
					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("Exit allowed", 3);
							allowed = true;
						} else {
							ja_log("Exit not allowed", 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) {
			if (nodes.hasOwnProperty(n)) {
				ja_log("Checking " + n, 3);
				if (String(n) === String(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; }
		var ja_dx, ja_dy;
		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);
		var 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); with 1 decimal // 55.6
	 * ja_round(55.549); with 1 decimal // 55.5
	 * ja_round(55); with -1 decimals // 60
	 * ja_round(54.9); with -1 decimals // 50
	 *
	 * @param	{Number}	value	The number.
	 * @returns	{Number}			The adjusted value.
	 */
	function ja_round(value) {
		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
		var valueArray = value.toString().split('e');
		value = Math.round(+(valueArray[0] + 'e' + (valueArray[1] ? (+valueArray[1] - ja_rounding) : -ja_rounding)));
		// Shift back
		valueArray = value.toString().split('e');
		return +(valueArray[0] + 'e' + (valueArray[1] ? (+valueArray[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;
		}
		//Check for invalid values
		//Select values
		if(ja_settings[name].elementType === "select" && ja_settings[name].options.lastIndexOf(ja_options[name]) < 0) {
			ja_log(ja_settings[name].options, 2);
			ja_log("Found invalid value for setting " + name + ": " + ja_options[name] + ". Using default.", 2);
			ja_options[name] = ja_settings[name].defaultValue;
		}
		//Color values
		else if(ja_settings[name].elementType === "color" && String(ja_options[name]).match(/#[0-9a-f]{6}/) == null) {
			ja_log("Found invalid value for setting " + name + ": \"" + ja_options[name] + "\". Using default.", 2);
			ja_options[name] = ja_settings[name].defaultValue;
		}
		//Numeric values
		else if(ja_settings[name].elementType === "number") {
			var minValue = typeof ja_settings[name].min === 'undefined' ? Number.MIN_VALUE : ja_settings[name].min;
			var maxValue = typeof ja_settings[name].max === 'undefined' ? Number.MAX_VALUE : ja_settings[name].max;
			if(isNaN(ja_options[name]) || ja_options[name] < minValue || ja_options[name] > maxValue) {
				ja_log("Found invalid value for setting " + name + ": \"" + ja_options[name] + "\". Using default.", 2);
				ja_options[name] = ja_settings[name].defaultValue;
			}
		}
		//Checkboxes
		else if(ja_settings[name].elementType === "checkbox" && ja_options[name] !== true && ja_options[name] !== false) {
			ja_log("Found invalid value for setting " + name + ": \"" + ja_options[name] + "\". Using default.", 2);
			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) {
		ja_log(e, 3);
		var applyPending = false;
		var settingName = Object.getOwnPropertyNames(ja_settings).filter(function(a){
			ja_log(ja_settings[a], 4);
			return ja_settings[a].elementId === e.id;
		})[0];
		ja_log(settingName, 3);
		switch(ja_settings[settingName].elementType) {
			case "checkbox":
				ja_log("Checkbox setting " + e.id + ": stored value is: " + ja_options[settingName] + ", new value: " + e.checked, 3);
				if (ja_options[settingName] !== e.checked) { applyPending = true; }
				break;
			case "select":
			case "color":
			case "number":
				ja_log("Setting " + e.id + ": stored value is: " + ja_options[settingName] + ", new value: " + e.value, 3);
				if (String(ja_options[settingName]) !== String(e.value)) { applyPending = true; }
				break;
			default:
				ja_log("Unknown setting " + e.id + ": stored value is: " + ja_options[settingName] + ", new value: " + e.value, 3);
		}

		//Enable|disable certain dependent settings
		switch(e.id) {
			case ja_settings.guess.elementId:
				Object.getOwnPropertyNames(ja_settings).forEach(function (a) {
					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.style.color =
							e.checked ? "black" : "lightgrey";
					}
				});
				break;
			case ja_settings.roundaboutOverlayDisplay.elementId:
				Object.getOwnPropertyNames(ja_settings).forEach(function (a) {
					var setting = ja_settings[a];
					if(setting.group && setting.group === 'roundaboutOverlayDisplay') {
						ja_log(a +": " + e.value, 3);
						document.getElementById(setting.elementId).disabled = e.value === "rOverNever";
						document.getElementById(setting.elementId).parentNode.style.color =
							e.value !== "rOverNever" ? "black" : "lightgrey";
					}
				});
				break;
			default:
				ja_log("Nothing to do for " + e.id, 2);
		}

		ja_log("Apply pending configuration changes? " + applyPending, 2);
		if(applyPending) {
			ja_log("Applying new settings now", 3);
			setTimeout(function(){ja_save();}, 500);

		} else {
			ja_log("No new settings to apply", 3);
		}
	};

	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) {
			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 = parseInt(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) {
				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(null);
						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(null);
						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: (parseInt(ja_getOption("pointSize")) - 1) + "px",
					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"))).toString() : "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.KEEP_LEFT,  "keepInstructionColor"),
				ja_get_style_rule(ja_routing_type.KEEP_RIGHT, "keepInstructionColor"),
				ja_get_style_rule(ja_routing_type.EXIT, "exitInstructionColor"),
				ja_get_style_rule(ja_routing_type.EXIT_LEFT, "exitInstructionColor"),
				ja_get_style_rule(ja_routing_type.EXIT_RIGHT, "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"),
				ja_get_style_rule(ja_routing_type.ROUNDABOUT_EXIT, "exitInstructionColor"),

				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) {
		var tr = I18n.translate('ja.' + key), no_tr = I18n.missingTranslation('ja.' + key);
		return tr !== no_tr ? tr : key;
	}

	function ja_loadTranslations() {
		ja_log("Loading translations",2);

		var set_trans = function(loc, def) {
            /*jshint -W093*/
			return I18n.translations[loc].ja = def;
		};

		//Default language (English)
		set_trans(window.I18n.defaultLocale,
		set_trans('en', {
			name: "Junction Angle Info",
			settingsTitle: "Junction Angle settings",
			apply: "Apply",
			resetToDefault: "Reset to default",
			aAbsolute: "Absolute",
			aDeparture: "Departure",
			angleMode: "Angle mode",
			angleDisplay: "Angle display style",
			angleDisplayArrows: "Direction arrows",
			displayFancy: "Fancy",
			displaySimple: "Simple",
			guess: "Estimate routing instructions",
			noInstructionColor: "Color for best continuation",
			keepInstructionColor: "Color for keep prompt",
			exitInstructionColor: "Color for exit prompt",
			turnInstructionColor: "Color for turn prompt",
			problemColor: "Color for angles to avoid",
			roundaboutColor: "Color for roundabouts (with non-straight exits)",
			roundaboutOverlayColor: "Color for roundabout overlay",
			roundaboutOverlayDisplay: "Show roundabout circle",
			rOverNever: "Never",
			rOverSelected: "When selected",
			rOverAlways: "Always",
			decimals: "Number of decimals",
			pointSize: "Base point size",

			roundaboutnav: "WIKI: Roundabouts",
			ghissues: "JAI issue tracker"
		}));

		//Apply
		switch (I18n.locale) {

			//Swedish (svenska)
			case 'sv':
				set_trans('sv', {
					name: "Korsningsvinklar",
					settingsTitle: "Inställningar för korsningsvinklar",
					apply: "Godkänn",
					resetToDefault: "Återställ",
					aAbsolute: "Absolut",
					aDeparture: "Sväng",
					angleMode: "Vinkelvisning",
					angleDisplay: "Vinkelstil",
					angleDisplayArrows: "Riktningspilar",
					displayFancy: "Grafisk",
					displaySimple: "Simpel",
					guess: "Gissa navigeringsinstruktioner",
					noInstructionColor: "Färg för \"ingen instruktion\"",
					keepInstructionColor: "Färg för\"håll höger/vänster\"-instruktion",
					exitInstructionColor: "Färg för \"ta av\"-instruktion",
					turnInstructionColor: "Färg för \"sväng\"-instruktion",
					problemColor: "Färg för vinklar att undvika",
					roundaboutColor: "Färg för rondell (med icke-räta vinklar)",
					roundaboutOverlayColor: "Färg för rondellcirkel",
					roundaboutOverlayDisplay: "Visa cirkel på rondell",
					rOverNever: "Aldrig",
					rOverSelected: "När vald",
					rOverAlways: "Alltid",
					decimals: "Decimaler",
					pointSize: "Cirkelns basstorlek"
				});
				break;

			//Finnish (Suomen kieli)
			case 'fi':
				set_trans('fi', {
					name: "Risteyskulmat",
					settingsTitle: "Rysteyskulmien asetukset",
					apply: "Aseta",
					resetToDefault: "Palauta",
					aAbsolute: "Absoluuttinen",
					aDeparture: "Käännös",
					angleMode: "Kulmien näyttö",
					angleDisplay: "Näyttötyyli",
					angleDisplayArrows: "Suuntanuolet",
					displayFancy: "Nätti",
					displaySimple: "Yksinkertainen",
					guess: "Arvioi reititysohjeet",
					noInstructionColor: "ohjeeton \"Suora\"-väri",
					keepInstructionColor: "\"Pysy vasemmalla/oikealla\"-ohjeen väri",
					exitInstructionColor: "\"Poistu\"-ohjeen väri",
					turnInstructionColor: "\"Käänny\"-ohjeen väri",
					problemColor: "Vältettävien kulmien väri",
					roundaboutColor: "Liikenneympyrän (jolla ei-suoria kulmia) ohjeen väri",
					roundaboutOverlayColor: "Liikenneympyrän korostusväri",
					roundaboutOverlayDisplay: "Korosta liikenneympyrä",
					rOverNever: "Ei ikinä",
					rOverSelected: "Kun valittu",
					rOverAlways: "Aina",
					decimals: "Desimaalien määrä",
					pointSize: "Ympyrän peruskoko"
				});
				break;

			//Polish (język polski)
			case 'pl':
				set_trans('pl', {
					settingsTitle: "Ustawienia",
					apply: "Zastosuj",
					resetToDefault: "Przywróć domyślne",
					aAbsolute: "Absolutne",
					aDeparture: "Rozjazdy",
					angleMode: "Tryb wyświetlania kątów",
					angleDisplay: "Styl kierunków",
					displayFancy: "Dwuliniowy",
					displaySimple: "Prosty",
					angleDisplayArrows: "Strzałki kierunków",
					guess: "Szacuj komunikaty trasy",
					noInstructionColor: "Kolor najlepszej kontynuacji",
					keepInstructionColor: "Kolor dla \"kieruj się\"",
					exitInstructionColor: "Kolor dla \"zjedź\"",
					turnInstructionColor: "Kolor dla \"skręć\"",
					problemColor: "Kolor niedozwolonych manewrów lub niejednoznacznych kątów",
					roundaboutColor: "Kolor numerowanych zjazdów na rondzie",
					roundaboutOverlayColor: "Kolor markera ronda",
					roundaboutOverlayDisplay: "Pokazuj marker ronda",
					rOverNever: "Nigdy",
					rOverSelected: "Gdy zaznaczone",
					rOverAlways: "Zawsze",
					decimals: "Ilość cyfr po przecinku",
					pointSize: "Rozmiar punktów pomiaru"
				});
				break;

		}
	}


	/*
	 * Bootstrapping and logging
	 */

	function ja_registerLoginEvent() {
		ja_log("Registering onLogin event listener", 1);
		ja_log(window.Waze.loginManager.events, 3);
		//HTML changes after login, even though the page is not reloaded. Need to defer init until then.
		window.Waze.loginManager.events.register("login", null, junctionangle_init);
		ja_log("Registered onLogin event listener", 1);
	}

	function ja_bootstrap(retries) {
		retries = retries || 0;
		//If Waze has not been defined in ~15 seconds, it probably won't work anyway. Might need tuning
		//for really slow devices?
		if (retries >= 30) {
			ja_log("Failed to bootstrap 30 times. Giving up.", 0);
			return;
		}

		try {
			//No current logged in user
			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 &&
				window.Waze.loginManager.user == null
				) {
				ja_registerLoginEvent();
			}
			//User already logged in and WME ready
			else 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 &&
				window.Waze.loginManager.user != null &&
				null !== document.getElementById('user-info') &&
				null !== document.getElementById('user-info').getElementsByClassName('nav-tabs')[0] &&
				null !== document.getElementById('user-info').getElementsByClassName('nav-tabs')[0].getElementsByClassName('tab-content')[0]) {
				setTimeout(function () {
					junctionangle_init();
				}, 500);
			}
			//Some part of the WME was not yet fully loaded. Retry.
			else {
				setTimeout(function () {
					ja_bootstrap(++retries);
				}, 500);
			}
		} catch (err) {
			ja_log(err, 1);
			setTimeout(function () {
				ja_bootstrap(++retries);
			}, 500);
		}
	}

	/**
	 * Debug logging.
	 * @param ja_log_msg
	 * @param ja_log_level
	 */
	function ja_log(ja_log_msg, ja_log_level) {
		//##NO_FF_START##
		//Firefox addons should not use console.(log|error|debug), so these lines
		//are removed by the FF addon packaging script.
		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);