WME Junction Angle Info (development version)

Show the angle between two selected (and connected) segments

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name				WME Junction Angle Info (development version)
// @namespace			https://github.com/milkboy/WME-ja
// @description			Show the angle between two selected (and connected) segments
// @include				/^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor.*$/
// @version				1.15-dev
// @grant				none
// @copyright			2019 Michael Wikberg <[email protected]>
// @license				CC-BY-NC-SA
// @icon				data:image/png;base64,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
// ==/UserScript==

/**
 * Copyright 2019 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" <?>
 *	2016 Sergey Kuznetsov "WazeRus" <[email protected]> (Russian translation)
 *  2016 "MajkiiTelini" <?> Czech translation
 */

/*jshint eqnull:true, nonew:true, nomen:true, curly:true, latedef:false, 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 ja_version = "1.15-devel";

	var ja_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_TURN_ANGLE = 45.04;			//Turn vs. keep angle specified in Wiki.
	var ja_U_TURN_ANGLE = 168.24;		//U-Turn angle based on map experiments.
	var ja_GRAY_ZONE = 0.5;			//Gray zone angle intended to prevent from irregularities observed on map.
	var ja_OVERLAPPING_ANGLE = 0.666;	//Experimentally measured overlapping angle.

	var ja_routing_type = {
		BC: "junction_none",
		KEEP: "junction_keep",
		KEEP_LEFT: "junction_keep_left",
		KEEP_RIGHT: "junction_keep_right",
		TURN: "junction_turn",
		EXIT: "junction_exit",
		EXIT_LEFT: "junction_exit_left",
		EXIT_RIGHT: "junction_exit_right",
		U_TURN: "junction_u_turn",
		PROBLEM: "junction_problem",
		NO_TURN: "junction_no_turn",
		NO_U_TURN: "junction_no_u_turn",
		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 = {
		defaultOn: { elementType: "checkbox", elementId: "_jaCbShowLayer", defaultValue: true },
		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"},
		exitInstructionColor: { elementType: "color", elementId: "_jaTbExitInstructionColor", defaultValue: "#6cb5ff", group: "guess"},
		turnInstructionColor: { elementType: "color", elementId: "_jaTbTurnInstructionColor", defaultValue: "#4cc600", group: "guess"},
		uTurnInstructionColor: { elementType: "color", elementId: "_jaTbUTurnInstructionColor", defaultValue: "#b66cff", group: "guess"},
		noTurnColor: { elementType: "color", elementId: "_jaTbNoTurnColor", defaultValue: "#a0a0a0", group: "guess"},
		problemColor: { elementType: "color", elementId: "_jaTbProblemColor", defaultValue: "#feed40", group: "guess"},
		roundaboutOverlayDisplay: { elementType: "select", elementId: "_jaSelRoundaboutOverlayDisplay", defaultValue: "rOverNever", options: ["rOverNever","rOverSelected","rOverAlways"]},
		roundaboutOverlayColor: { elementType: "color", elementId: "_jaTbRoundaboutOverlayColor", defaultValue: "#aa0000", group: "roundaboutOverlayDisplay"},
		roundaboutColor: { elementType: "color", elementId: "_jaTbRoundaboutColor", defaultValue: "#ff8000", 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: function(at) {
			var arrows = ja_getOption("angleDisplayArrows");
			return arrows[at % arrows.length];
		},
		left: function() { return this.get(0); },
		right: function() { return this.get(1); },
		left_up: function() { return this.get(2); },
		right_up: function() { return this.get(3); }
	};

	function getWaze() {
		if(window.wrappedJSObject) {
			return window.wrappedJSOnject.W;
		}
		return window.W;
	}

	/*
	 * Main logic functions
	 */
	function junctionangle_init() {
		var i, ja_select_option, navTabs, tabContent;
		var ja_settings_dom = document.createElement("div");
		var ja_settings_dom_panel = document.createElement("div");
		var ja_settings_dom_content = document.createElement("div");
		var ja_settings_header = document.createElement('h4');
		var style = document.createElement('style');
		var form = document.createElement('form');
		var section = document.createElement('div');
		var ja_reset_button = document.createElement('button');
		var userTabs = document.getElementById('user-info');
		var ja_info = document.createElement('ul');
		var ja_version_elem = document.createElement('li');
		var jatab = document.createElement('li');

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

		//Temporary workaround. Beta editor changed the event listener logic, but live is still using the old version
		//if-else should be removed once not needed anymore
		if("events" in getWaze().model.segments) {
			//Live
			getWaze().model.segments.events.on({
				"objectschanged": ja_calculate,
				"objectsremoved": ja_calculate
			});
			getWaze().model.nodes.events.on({
				"objectschanged": ja_calculate,
				"objectsremoved": ja_calculate
			});
		} else if("_events" in getWaze().model.segments) {
			//Beta editor
			getWaze().model.segments.on({
				"objectschanged": ja_calculate,
				"objectsremoved": ja_calculate
			});
			getWaze().model.nodes.on({
				"objectschanged": ja_calculate,
				"objectsremoved": ja_calculate
			});
		}

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

		ja_load();
		ja_loadTranslations();

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

		style.appendChild(document.createTextNode(function () {/*
			#jaOptions > *:first-child {
				margin-top: 1em;
			}
			#jaOptions * {
				vertical-align: middle;
			}
			#jaOptions label {
				display: inline;
			}
			#jaOptions input, select {
				display: inline;
				margin-right: 7px;
				box-sizing: border-box;
				border: 1px solid #cccccc;
				border-radius: 5px;
				padding: 3px;
			}
			#jaOptions input[type="number"] {
				width: 4em;
				padding: 6px;
			}
			#jaOptions input[type="color"] {
				width: 15%;
				height: 2em;
				padding: 4px;
			}
			@supports (-webkit-appearance:none) {
				#jaOptions input[type="color"] {
					padding: 0px 2px 0px 2px;
				}
			}
			#jaOptions .disabled {
				position: relative;
			}
			#jaOptions .disabled:after {
				content: " ";
				z-index: 10;
				display: block;
				position: absolute;
				height: 100%;
				top: 0;
				left: 0;
				right: 0;
				background: rgba(255, 255, 255, 0.666);
			}
			*/}.toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1]));

		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(i = 0; i < setting.options.length; i++) {
						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;
				default:
					ja_log("Unknown setting type " + setting.elementType, 2);
			}

			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'));

		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(document.createElement('div'));
		section.appendChild(ja_reset_button);

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

		navTabs = userTabs.getElementsByClassName('nav-tabs')[0];
		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
		ja_info.className = "list-unstyled -side-panel-section";
		ja_info.style.fontSize = "11px";

		ja_version_elem.appendChild(document.createTextNode(ja_getMessage("name") + ": v" + ja_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);

		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 (getWaze().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())
			});

			//Set visibility according to user preference
			ja_mapLayer.setVisibility(ja_getOption("defaultOn"));

			getWaze().map.addLayer(ja_mapLayer);
			ja_log("version " + ja_version + " loaded.", 0);

			ja_log(getWaze().map, 3);
			ja_log(getWaze().model, 3);
			ja_log(getWaze().loginManager, 3);
			ja_log(getWaze().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) {
		var s_n = {}, s_in = null, s_out = {}, street_n = {}, street_in = null, angle;
		var s_in_id = s_in_a;
		var s_out_id = s_out_a;

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

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

		node.attributes.segIDs.forEach(function(element) {
			if (element === s_in_id) {
				s_in = node.model.segments.getObjectById(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.getObjectById(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.getObjectById(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;
		}

		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.NO_TURN;
		}

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

		//Check for U-turn, which is emitted even if there is only one s-out
		if (Math.abs(angle) > ja_U_TURN_ANGLE + ja_GRAY_ZONE) {
			ja_log("Angle is >= 170 - U-Turn", 2);
			return ja_routing_type.U_TURN;
		} else if (Math.abs(angle) > ja_U_TURN_ANGLE - ja_GRAY_ZONE) {
			ja_log("Angle is in gray zone 169-171", 2);
			return ja_routing_type.PROBLEM;
		}

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

		/*
		 *
		 * Here be dragons!
		 *
		 */
		if(Math.abs(angle) < ja_TURN_ANGLE - ja_GRAY_ZONE) {
			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)) < ja_TURN_ANGLE //Any angle above 45.04 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]];

				var name_match = ja_primary_name_match(street_in, tmp_street_out) ||
						ja_alt_name_match(street_in, tmp_street_out) ||
						ja_cross_name_match(street_in, tmp_street_out);

				if(name_match && ja_segment_type_match(s_in, tmp_s_out)) {
					ja_log("BC name and type match", 2);
					bc_collect(a, 3);
				} else if(name_match) {
					ja_log("BC 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);

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

			//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
			//FZ69617: Wiki seems to be wrong here - experiments shows that "more than two" must be read as "at least two"
			//FZ69617: Wiki also does not mention differences between RHT and LHT countries for this consideration,
			// but map experiments seem to prove that we have to use reverse logic for LHT countries.
			if (!s_in.model.isLeftHand) { //RHT
				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;
					}
				}
			} else { //LHT
				//FZ69617: KEEP RIGHT/LEFT logic for right most segment
				//MISSING IN WIKI: When there are at least two segments less than 45.04°, only the right most segment will
				// be KEEP RIGHT, all the rest will be KEEP LEFT
				if (angles[angles.length - 1][1] === s_out_id) { //s-out is right most segment

					//FZ69617: KEEP RIGHT/LEFT overlapping case
					//MISSING IN WIKI: If the right most segment is overlapping another segment, it will also be KEEP LEFT.
					if (!ja_overlapping_angles(angles[angles.length - 1][0], angles[angles.length - 2][0])) {
						ja_log("Right most <45 segment: keep right", 2);
						return ja_routing_type.KEEP_RIGHT;
					}
				}
			}

			//FZ69617: Two overlapping segments logic
			//WAZE WIKI: If the only two segments less than 45.04° overlap each other, neither will get an instruction.
			//...
			//wlodek76: Three overlapping segments logic
			//MISSING IN WIKI: If the ONLY THREE segments less than 45.04° overlap each other, neither will get an instruction.
			//...
			//FZ69617: Two or more overlapping segments logic
			//MISSING IN WIKI: If there are two or more segments less than 45.04° and all these segmentes overlap each other,
			// neither will get an instruction.
			var overlap_i = 1;
			while(overlap_i < angles.length &&
					ja_overlapping_angles(angles[0][0], angles[overlap_i][0])) {
				++overlap_i;
			}
			if(overlap_i > 1 && overlap_i === angles.length) {
				ja_log("Two or more overlapping segments only: no instruction", 2);
				return ja_routing_type.BC;
			}

			//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", 2);
			return s_in.model.isLeftHand ? ja_routing_type.KEEP_LEFT : ja_routing_type.KEEP_RIGHT;
		} else if (Math.abs(angle) < ja_TURN_ANGLE + ja_GRAY_ZONE) {
			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() {
		var ja_start_time = Date.now();
		var ja_nodes = [];
		var restart = false;
		ja_log("Actually calculating now", 2);
		ja_roundabout_points = [];
		ja_log(getWaze().map, 3);
		if (typeof ja_mapLayer === 'undefined') {
			return;
		}
		//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 (!getWaze().selectionManager.hasSelectedFeatures()) { return; }
		ja_log("Checking junctions for " + getWaze().selectionManager.getSelectedFeatures().length + " segments", 2);

		getWaze().selectionManager.getSelectedFeatures().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(getWaze().model.nodes.getObjectById(node), 3);

			var tmp_s = null, tmp_n = null, tmp_junctionID = null;
			if(getWaze().model.nodes.getObjectById(node) == null ||
				typeof getWaze().model.nodes.getObjectById(node).attributes.segIDs === 'undefined') {
				return;
			}
			getWaze().model.nodes.getObjectById(node).attributes.segIDs.forEach(function(segment) {
				ja_log(segment, 3);

				if(getWaze().model.segments.getObjectById(segment).attributes.junctionID) {
					ja_log("Roundabout detected: " + getWaze().model.segments.getObjectById(segment).attributes.junctionID, 3);
					tmp_junctionID = getWaze().model.segments.getObjectById(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].out_s = tmp_s;
				ja_selected_roundabouts[tmp_junctionID].out_n = node;
			} else {
				ja_selected_roundabouts[tmp_junctionID] = {
					'in_s': tmp_s,
					'in_n': tmp_n,
					'out_s': null,
					'out_n': null,
					'p': getWaze().model.junctions.get(tmp_junctionID).geometry
				};
			}
		});

		//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(
					getWaze().model.nodes.getObjectById(ja_selected_roundabouts[tmp_roundabout].in_n).geometry,
					tmp_roundabout_center,
					getWaze().model.nodes.getObjectById(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
						}
					)
				]);
			}
		}


		var ja_label_distance;
		/*
		 * Define a base distance to markers, depending on the zoom level
		 */
		switch (getWaze().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;
			default:
				ja_log("Unsupported zoom level: " + getWaze().map.zoom + "!", 2);
		}

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

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


		/**
		 * Collect double-turn (inc. U-turn) segments info
		 */
		var doubleTurns = {

			data: {}, //Structure: map<s_id, map<s_out_id, list<{s_in_id, angle, turn_type}>>>

			collect: function (s_id, s_in_id, s_out_id, angle, turn_type) {
				ja_log("Collecting double-turn path from " + s_in_id + " to " + s_out_id	 +
					" via " + s_id + " with angle " + angle + " type: " + turn_type, 2);
				var info = this.data[s_id];
				if (info === undefined) {
					info = this.data[s_id] = {};
				}
				var list = info[s_out_id];
				if (list === undefined) {
					list = info[s_out_id] = [];
				}
				list.push({ s_in_id: s_in_id, angle: angle, turn_type: turn_type });
			},

			forEachItem: function (s_id, s_out_id, fn) {
				var info = this.data[s_id];
				if (info !== undefined) {
					var list = info[s_out_id];
					if (list !== undefined) {
						list.forEach(function(item, i) {
							fn(item, i);
						});
					}
				}
			}
		};

		//Loop through all 15m or less long segments and collect double-turn disallowed ones
		if (ja_getOption("angleMode") === "aDeparture" && ja_nodes.length > 1) {
			getWaze().selectionManager.getSelectedFeatures().forEach(function (selectedSegment) {
				var segmentId = selectedSegment.model.attributes.id;
				var segment = getWaze().model.segments.objects[segmentId];
				ja_log("Checking " + segmentId + " for double turns ...", 2);

				var len = ja_segment_length(segment);
				ja_log("Segment " + segmentId + " length: " + len, 2);

				if (Math.round(len) <= 15) {

					var fromNode = getWaze().model.nodes.getObjectById(segment.attributes.fromNodeID);
					var toNode = getWaze().model.nodes.getObjectById(segment.attributes.toNodeID);
					var a_from = ja_getAngle(segment.attributes.fromNodeID, segment);
					var a_to = ja_getAngle(segment.attributes.toNodeID, segment);

					fromNode.attributes.segIDs.forEach(function (fromSegmentId) {
						if (fromSegmentId === segmentId) { return; }
						var fromSegment = getWaze().model.segments.objects[fromSegmentId];
						var from_a = ja_getAngle(segment.attributes.fromNodeID, fromSegment);
						var from_angle = ja_angle_diff(from_a, a_from, false);
						ja_log("Segment from " + fromSegmentId + " angle: " + from_a + ", turn angle: " + from_angle, 2);

						toNode.attributes.segIDs.forEach(function (toSegmentId) {
							if (toSegmentId === segmentId) {return; }
							var toSegment = getWaze().model.segments.objects[toSegmentId];
							var to_a = ja_getAngle(segment.attributes.toNodeID, toSegment);
							var to_angle = ja_angle_diff(to_a, a_to, false);
							ja_log("Segment to " + toSegmentId + " angle: " + to_a + ", turn angle: " + to_angle, 2);

							var angle = Math.abs(to_angle - from_angle);
							ja_log("Angle from " + fromSegmentId + " to " + toSegmentId + " is: " + angle, 2);

							//Determine whether a turn is disallowed
							if (angle >= 175 - ja_GRAY_ZONE && angle <= 185 + ja_GRAY_ZONE) {
								var turn_type = (angle >= 175 + ja_GRAY_ZONE && angle <= 185 - ja_GRAY_ZONE) ?
										ja_routing_type.NO_U_TURN : ja_routing_type.PROBLEM;

								if (ja_is_turn_allowed(fromSegment, fromNode, segment) &&
										ja_is_turn_allowed(segment, toNode, toSegment)) {
									doubleTurns.collect(segmentId, fromSegmentId, toSegmentId, angle, turn_type);
								}
								if (ja_is_turn_allowed(toSegment, toNode, segment) &&
										ja_is_turn_allowed(segment, fromNode, fromSegment)) {
									doubleTurns.collect(segmentId, toSegmentId, fromSegmentId, angle, turn_type);
								}
							}
						});
					});
				}
			});
		}

		ja_log("Collected double-turn segments:", 2);
		ja_log(doubleTurns.data, 2);


		//Start looping through selected nodes
		for (var i = 0; i < ja_nodes.length; i++) {
			var node = getWaze().model.nodes.getObjectById(ja_nodes[i]);
			var angles = [];
			var ja_selected_segments_count = 0;
			var ja_selected_angles = [];
			var a;

			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(getWaze().model, 3);
				ja_log(getWaze().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);

			ja_current_node_segments.forEach(function (nodeSegment, j) {
				var s = getWaze().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);
					}
					restart = true;
				}
				a = ja_getAngle(ja_nodes[i], s);
				ja_log("Segment " + nodeSegment + " angle is " + a, 2);
				angles[j] = [a, nodeSegment, s == null ? false : s.isSelected()];
				if (s == null ? false : s.isSelected()) {
					ja_selected_segments_count++;
				}
			});

			if(restart) { return; }

			//make sure we have the selected angles in correct order
			ja_log(ja_current_node_segments, 3);
			getWaze().selectionManager.getSelectedFeatures().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 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);

				//draw double turn markers
				doubleTurns.forEachItem(ja_selected_angles[0][1], ja_selected_angles[1][1], function(item) {
					ja_draw_marker(point, node, ja_label_distance, item.angle, ha, true, item.turn_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) {
						return !!a[2];
					})[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);

						//draw double turn markers
						doubleTurns.forEachItem(a_in[1], angle[1], function(item) {
							ja_draw_marker(point, node, ja_label_distance, item.angle, ha, true, item.turn_type);
						});

					} 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 * 1.25 * Math.cos((ha * Math.PI) / 180)),
								node.geometry.y + (ja_label_distance * 1.25 * 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)
		//FZ69617: Exctract initial label distance from point
		var ja_tmp_distance = Math.abs(ha) % 180 < 45 || Math.abs(ha) % 180 > 135 ?
				(point.x - node.geometry.x) / (Math.cos((ha * Math.PI) / 180)) :
				(point.y - node.geometry.y) / (Math.sin((ha * Math.PI) / 180));
		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_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 += ja_arrow.right_up();
					break;
				default:
					ja_log("No extra format for junction type: " + ja_junction_type, 2);
			}
		} 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;
				case ja_routing_type.PROBLEM:
					angleString = "?\n" + angleString;
					break;
				default:
					ja_log("No extra format for junction type: " + ja_junction_type, 2);
			}
		}
		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) {
		(junctionId === undefined ? (getWaze().model.junctions.getObjectArray()) : (function (junction) {
			return junction === undefined ? [] : [ junction ];
		})
		(getWaze().model.junctions.get(junctionId))).forEach(function (element) {
			ja_log(element, 3);
			var nodes = {};
			element.segIDs.forEach(function(s) {
				var seg = getWaze().model.segments.getObjectById(s);
				ja_log(seg, 3);
				nodes[seg.attributes.fromNodeID] = getWaze().model.nodes.getObjectById(seg.attributes.fromNodeID);
				nodes[seg.attributes.toNodeID] = getWaze().model.nodes.getObjectById(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);
			//noinspection JSBitwiseOperatorUsage
			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
				//FZ69617: This no longer seems to be needed
				//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
				//FZ69617: This no longer seems to be needed
				//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 =
			getWaze().model.streets.objects[getWaze().model.segments.objects[segmentId].attributes.primaryStreetID];
		var secondary = [];
		getWaze().model.segments.objects[segmentId].attributes.streetIDs.forEach(function (element) {
			secondary.push(getWaze().model.streets.objects[element]);
		});
		ja_log(primary, 3);
		ja_log(secondary, 3);
		return { primary: primary, secondary: secondary };
	}

	/**
	 * Computes segment's length in meters
	 * @param segment Segment to compute the length of
	 * @returns {number}
	 */
	function ja_segment_length(segment) {
		var len = segment.geometry.getGeodesicLength(getWaze().map.projection);
		ja_log("segment: " + segment.attributes.id +
      " computed len: " + len + " attrs len: " + segment.attributes.length, 3);
		return len;
	}


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


	/*
	 * 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_angle_dist(a, s_in_angle) {
		ja_log("Computing out-angle " + a + " distance 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 = getWaze().model.junctions.get(junctionID);
		var nodes = {};
		var numValidExits = 0;
		junction.segIDs.forEach(function (element, index) {
			var s = getWaze().model.segments.getObjectById(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 = getWaze().model.nodes.getObjectById(s.attributes.toNodeID);
				ja_log(currNode, 3);
				currNode.attributes.segIDs.forEach(function (element2) {
					var s_exit = getWaze().model.segments.getObjectById(element2);
					ja_log(s_exit, 3);
					if (s_exit.attributes.junctionID === null) {
						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);
						}
					} else {
						//part of the junction.. Ignoring
						ja_log(s_exit.attributes.id + " is in the roundabout. ignoring", 3);
					}
				});
				if (allowed) {
					numValidExits++;
					nodes[s.attributes.toNodeID] = getWaze().model.nodes.getObjectById(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(
						getWaze().model.nodes.getObjectById(n_in).geometry,
						ja_coordinates_to_point(junction.geometry.coordinates),
						getWaze().model.nodes.getObjectById(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(
									getWaze().model.nodes.getObjectById(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"));
		var valueArray;
		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
		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) {
		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(e, 3);
		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);
		}

		function disable_input(element, disable) {
			element.disabled = disable;
			if (disable) {
				$(element.parentNode).addClass("disabled");
			} else {
				$(element.parentNode).removeClass("disabled");
			}
		}

		//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);
						disable_input(document.getElementById(setting.elementId), !e.checked);
					}
				});
				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);
						disable_input(document.getElementById(setting.elementId), e.value === "rOverNever");
					}
				});
				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) && setting.min <= val && 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;
				default:
					ja_log("Unknown setting type " + setting.elementType, 2);
			}
		});
		ja_apply();
		return false;
	};

	var ja_apply = function applyJAOptions() {
		ja_log("Applying stored (or default) settings", 2);
		if(typeof getWaze().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("WME not ready (no settings tab)", 2);
		} else {
			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;
					default:
						ja_log("Unknown setting type " + setting.elementType, 2);
				}
			});
		}
		getWaze().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_contrast_color(hex_color) {
		ja_log("Parsing YIQ-based contrast color for: " + hex_color + " ...", 2);
		var r = parseInt(hex_color.substr(1, 2), 16);
		var g = parseInt(hex_color.substr(3, 2), 16);
		var b = parseInt(hex_color.substr(5, 2), 16);
		var yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
		return (yiq >= 128) ? 'black' : 'white';
	}

	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 ? 4 * parseInt(ja_getOption("decimals")) : 0),
					fontSize: (parseInt(ja_getOption("pointSize")) - 1) + "px",
					fillColor: ja_getOption(fillColorOption),
					strokeColor: "#183800",
					fontColor: ja_get_contrast_color(ja_getOption(fillColorOption))
				}
			});
	}

	function ja_style() {
		ja_log("Point radius will be: " + (parseInt(ja_getOption("pointSize"), 10)) +
			(parseInt(ja_getOption("decimals") > 0 ? (4 * 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 ? 4 * 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.NO_TURN, "noTurnColor"),
				ja_get_style_rule(ja_routing_type.PROBLEM, "problemColor"),
				ja_get_style_rule(ja_routing_type.ROUNDABOUT, "roundaboutColor"),
				ja_get_style_rule(ja_routing_type.ROUNDABOUT_EXIT, "exitInstructionColor"),
				ja_get_style_rule(ja_routing_type.U_TURN, "uTurnInstructionColor"),
				ja_get_style_rule(ja_routing_type.NO_U_TURN, "problemColor"),

				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 ? 4 * 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 ? key : tr;
	}

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

		ja_log("Loading translations",2);

		//Apply
		switch (I18n.locale) {
			default:
				//Default language (English)
				set_trans({
					name: "Junction Angle Info",
					settingsTitle: "Junction Angle Info settings",
					resetToDefault: "Reset to default",
					defaultOn: "Show layer by 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",
					uTurnInstructionColor: "Color for U-turn prompt",
					noTurnColor: "Color for disallowed turns",
					problemColor: "Color for angles to avoid",
					roundaboutColor: "Color for non-normal roundabouts",
					roundaboutOverlayColor: "Color for roundabout overlay",
					roundaboutOverlayDisplay: "Show roundabout",
					rOverNever: "Never",
					rOverSelected: "When selected",
					rOverAlways: "Always",
					decimals: "Number of decimals",
					pointSize: "Base point size",

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

			//Czech (čeština)
			case 'cs':
				set_trans({
					name: "Junction Angle Info",
					settingsTitle: "Nastavení JAI",
					resetToDefault: "Výchozí nastavení",
					defaultOn: "Vždy aktivní",
					aAbsolute: "Absolutní",
					aDeparture: "Odjezdový",
					angleMode: "Styl zobrazení úhlů",
					angleDisplay: "Styl výpisu úhlů",
					angleDisplayArrows: "Směrové šipky",
					displayFancy: "Zdobný",
					displaySimple: "Jednoduchý",
					guess: "Odhadovat navigační hlášky",
					noInstructionColor: "Bez hlášení",
					keepInstructionColor: "\"Držte se/Zůstaňte\"",
					exitInstructionColor: "\"Sjeďte\"",
					turnInstructionColor: "\"Odbočte\"",
					uTurnInstructionColor: "\"Otočte se\"",
					noTurnColor: "Nepovolené směry",
					problemColor: "Nejasné úhly",
					roundaboutColor: "Rozbité kruháče",
					roundaboutOverlayColor: "Kruháče",
					roundaboutOverlayDisplay: "ukazovat kruháče",
					rOverNever: "Ne-",
					rOverSelected: "Při výběru",
					rOverAlways: "Vždy",
					decimals: "Počet des. míst",
					pointSize: "Velikost písma",

					roundaboutnav: "US WIKI: Kruhové objezdy",
					ghissues: "Hlášení problémů JAI"
				});
				break;

			//Finnish (Suomen kieli)
			case 'fi':
				set_trans({
					name: "Risteyskulmat",
					settingsTitle: "Rysteyskulmien asetukset",
					resetToDefault: "Palauta",
					defaultOn: "Näytä taso oletuksena",
					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",
					uTurnInstructionColor: "\"Käänny ympäri\"-ohjeen väri",
					noTurnColor: "Kielletyn käännöksen 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({
					settingsTitle: "Ustawienia",
					resetToDefault: "Przywróć domyślne",
					defaultOn: "Pokazać warstwę domyślnie",
					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ęć\"",
					uTurnInstructionColor: "Kolor dla \"zawróć\"",
					noTurnColor: "Kolor niedozwolonych manewrów",
					problemColor: "Kolor problematycznych kątów",
					roundaboutColor: "Kolor rond niestandardowych",
					roundaboutOverlayColor: "Kolor znacznika rond",
					roundaboutOverlayDisplay: "Pokazuj ronda",
					rOverNever: "Nigdy",
					rOverSelected: "Gdy zaznaczone",
					rOverAlways: "Zawsze",
					decimals: "Ilość cyfr po przecinku",
					pointSize: "Rozmiar punktów pomiaru"
				});
				break;

			//Russian (русский)
			case 'ru':
				set_trans({
					name: "Углы поворотов",
					settingsTitle: "Настройки Junction Angle Info",
					resetToDefault: "Сбросить настройки",
					defaultOn: "По умолчанию показывать",
					aAbsolute: "Абсолютные",
					aDeparture: "Повороты",
					angleMode: "- режим углов",
					angleDisplay: "- стиль отображения",
					angleDisplayArrows: "- стрелки направлений",
					displayFancy: "Модный",
					displaySimple: "Простой",
					guess: "Ожидаемые подсказки",
					noInstructionColor: "- нет подсказки",
					keepInstructionColor: "- держитесь",
					exitInstructionColor: "- съезд",
					turnInstructionColor: "- поверните",
					uTurnInstructionColor: "- развернитесь",
					noTurnColor: "- запрещённый манёвр",
					problemColor: "- угол следует избегать",
					roundaboutColor: "- некорректное кольцо",
					roundaboutOverlayColor: "- цвет кольца",
					roundaboutOverlayDisplay: "- показ колец",
					rOverNever: "Никогда",
					rOverSelected: "Если выбрано",
					rOverAlways: "Всегда",
					decimals: "- знаков после запятой",
					pointSize: "- размер кружка",

					roundaboutnav: "Вики: круговые перекрестки",
					ghissues: "Сообщить об ошибке"
				});
				break;

			//Swedish (svenska)
			case 'sv':
				set_trans({
					name: "Korsningsvinklar",
					settingsTitle: "Inställningar för korsningsvinklar",
					resetToDefault: "Återställ",
					defaultOn: "Visa skiktet som standard",
					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",
					uTurnInstructionColor: "Färg för \"U-sväng\"-instruktion",
					noTurnColor: "Färg förbjuden sväng",
					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;

			//Ukrainian (український)
		 	case 'uk':
		    		set_trans({
			    		name: "Junction Angle Info",
			       		settingsTitle: "Налаштування Junction Angle Info",
					resetToDefault: "Скинути налаштування",
					defaultOn: "За замовчуванням показувати",
					aAbsolute: "Абсолютні",
					aDeparture: "Повороти",
					angleMode: "- режим кутів",
					angleDisplay: "- стиль відображення",
					angleDisplayArrows: "- стрілки напрямків",
					displayFancy: "Модний",
					displaySimple: "Простий",
					override: "Візуалізувати \"змінені підказки\"",
					overrideAngles: "Показувати кути для \"змінених підказок\"",
					guess: "Очікувані підказки:",
					noInstructionColor: "- немає підказки",
					continueInstructionColor: "- продовжуйте рух прямо",
					keepInstructionColor: "- тримайтеся",
					exitInstructionColor: "- з'їзд",
					turnInstructionColor: "- поверніть",
					uTurnInstructionColor: "- розверніться",
					noTurnColor: "- заборонений маневр",
					problemColor: "- кут слід уникати",
					roundaboutColor: "- некоректне кільце",
					roundaboutOverlayColor: "- колір кільця",
					roundaboutOverlayDisplay: "- показ кілець",
					rOverNever: "Ніколи",
					rOverSelected: "Якщо вибрано",
					rOverAlways: "Завжди",
					decimals: "- знаків після коми",
					pointSize: "- розмір шрифту",

					roundaboutnav: "WIKI: кругові перехрестя(en)",
					ghissues: "JAI - Повідомити про помилку"
				});
				break;
		}
	}


	/*
	 * Bootstrapping and logging
	 */

	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 {
			//User logged in and WME ready
			if (
				ja_is_model_ready() &&
				ja_is_dom_ready() &&
				getWaze().loginManager.isLoggedIn()) {
				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);
		}
	}

	function ja_is_model_ready() {
		if(typeof getWaze() === 'undefined' || typeof getWaze().map === 'undefined') {
			return false;
		} else {
			return 'undefined' !== typeof getWaze().map.events.register &&
				'undefined' !== typeof getWaze().selectionManager.events.register &&
				'undefined' !== typeof getWaze().loginManager.events.register;
		}
	}

	function ja_is_dom_ready() {
		if(null === document.getElementById('user-info')) {
			return false;
		} else {
			return document.getElementById('user-info').getElementsByClassName('nav-tabs').length > 0 &&
				document.getElementById('user-info').getElementsByClassName('tab-content').length > 0;
		}
	}

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