"use strict";
// ==UserScript==
// @name WME LaneTools
// @namespace https://github.com/SkiDooGuy/WME-LaneTools
// @version 2025.06.01.001
// @description Adds highlights and tools to WME to supplement the lanes feature
// @author SkiDooGuy, Click Saver by HBiede, Heuristics by kndcajun, assistance by jm6087
// @match https://www.waze.com/editor*
// @match https://www.waze.com/*/editor*
// @match https://beta.waze.com/editor*
// @match https://beta.waze.com/*/editor*
// @exclude https://www.waze.com/user/editor*
// @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @require https://cdn.jsdelivr.net/npm/@turf/[email protected]/turf.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/proj4.min.js
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @connect greasyfork.org
// @contributionURL https://github.com/WazeDev/Thank-The-Authors
// ==/UserScript==
/* global W */
/* global WazeWrap */
// import type { KeyboardShortcut, Node, Segment, Selection, Turn, UserSession, WmeSDK } from "wme-sdk-typings";
// import type { Position } from "geojson";
// import _ from "underscore";
// import * as turf from "@turf/turf";
// import WazeWrap from "https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js";
// import proj4 from "proj4";
let sdk;
unsafeWindow.SDK_INITIALIZED.then(() => {
if (!unsafeWindow.getWmeSdk) {
throw new Error("SDK is not installed");
}
sdk = unsafeWindow.getWmeSdk({
scriptId: "wme-lane-tools",
scriptName: "WME LaneTools",
});
console.log(`SDK v ${sdk.getSDKVersion()} on ${sdk.getWMEVersion()} initialized`);
sdk.Events.once({ eventName: "wme-ready" }).then(ltInit);
});
function ltInit() {
let Direction;
(function (Direction) {
Direction[Direction["REVERSE"] = -1] = "REVERSE";
Direction[Direction["ANY"] = 0] = "ANY";
Direction[Direction["FORWARD"] = 1] = "FORWARD";
})(Direction || (Direction = {}));
let LT_ROAD_TYPE;
(function (LT_ROAD_TYPE) {
// Streets
LT_ROAD_TYPE[LT_ROAD_TYPE["NARROW_STREET"] = 22] = "NARROW_STREET";
LT_ROAD_TYPE[LT_ROAD_TYPE["STREET"] = 1] = "STREET";
LT_ROAD_TYPE[LT_ROAD_TYPE["PRIMARY_STREET"] = 2] = "PRIMARY_STREET";
// Highways
LT_ROAD_TYPE[LT_ROAD_TYPE["RAMP"] = 4] = "RAMP";
LT_ROAD_TYPE[LT_ROAD_TYPE["FREEWAY"] = 3] = "FREEWAY";
LT_ROAD_TYPE[LT_ROAD_TYPE["MAJOR_HIGHWAY"] = 6] = "MAJOR_HIGHWAY";
LT_ROAD_TYPE[LT_ROAD_TYPE["MINOR_HIGHWAY"] = 7] = "MINOR_HIGHWAY";
// Other drivable
LT_ROAD_TYPE[LT_ROAD_TYPE["DIRT_ROAD"] = 8] = "DIRT_ROAD";
LT_ROAD_TYPE[LT_ROAD_TYPE["FERRY"] = 14] = "FERRY";
LT_ROAD_TYPE[LT_ROAD_TYPE["PRIVATE_ROAD"] = 17] = "PRIVATE_ROAD";
LT_ROAD_TYPE[LT_ROAD_TYPE["PARKING_LOT_ROAD"] = 20] = "PARKING_LOT_ROAD";
// Non-drivable
LT_ROAD_TYPE[LT_ROAD_TYPE["WALKING_TRAIL"] = 5] = "WALKING_TRAIL";
LT_ROAD_TYPE[LT_ROAD_TYPE["PEDESTRIAN_BOARDWALK"] = 10] = "PEDESTRIAN_BOARDWALK";
LT_ROAD_TYPE[LT_ROAD_TYPE["STAIRWAY"] = 16] = "STAIRWAY";
LT_ROAD_TYPE[LT_ROAD_TYPE["RAILROAD"] = 18] = "RAILROAD";
LT_ROAD_TYPE[LT_ROAD_TYPE["RUNWAY"] = 19] = "RUNWAY";
})(LT_ROAD_TYPE || (LT_ROAD_TYPE = {}));
const MIN_DISPLAY_LEVEL = 14;
const MIN_ZOOM_NON_FREEWAY = 17;
// const DisplayLevels = {
// MIN_ZOOM_ALL: 14,
// MIN_ZOOM_NONFREEWAY: 17,
// };
let HeuristicsCandidate;
(function (HeuristicsCandidate) {
HeuristicsCandidate[HeuristicsCandidate["ERROR"] = -2] = "ERROR";
HeuristicsCandidate[HeuristicsCandidate["FAIL"] = -1] = "FAIL";
HeuristicsCandidate[HeuristicsCandidate["NONE"] = 0] = "NONE";
HeuristicsCandidate[HeuristicsCandidate["PASS"] = 1] = "PASS";
})(HeuristicsCandidate || (HeuristicsCandidate = {}));
if (!WazeWrap.Ready) {
setTimeout(() => {
ltInit();
}, 100);
return;
}
const LANETOOLS_VERSION = `${GM_info.script.version}`;
const GF_LINK = "https://greasyfork.org/en/scripts/537219-wme-lanetools";
const DOWNLOAD_URL = "https://greasyfork.org/en/scripts/537219-wme-lanetools";
const FORUM_LINK = "https://www.waze.com/discuss/t/script-wme-lanetools/53136";
const LI_UPDATE_NOTES = `NEW:<br>
- Conversion to WME SDK<br>
- <b>ENABLE LT Layers To See Markings on the Map</b><br>
- Point Updates to GF vs. GITHUB<br><br>
KNOWN ISSUE:<br>
- Some tab UI enhancements may not work as expected.<br>`;
const LANETOOLS_DEBUG_LEVEL = 1;
const configArray = {};
const RBSArray = { failed: false };
const IsBeta = location.href.indexOf("beta.waze.com") !== -1;
const env = IsBeta ? "beta" : "production";
const TAB_TRANSLATIONS = {
// Default english values
default: {
enabled: "Enabled",
disabled: "Disabled",
toggleShortcut: "Toggle Shortcut",
UIEnhance: "Tab UI Enhancements",
autoWidth: "Auto-open road width",
autoOpen: "Auto-open lanes tab",
autoExpand: "Auto-expand lane editor",
autoFocus: "Auto-focus lane input",
reOrient: "Re-orient lane icons",
enClick: "Enable ClickSaver",
clickStraight: "All straight lanes",
clickTurn: "Turn lanes",
mapHighlight: "Map Highlights",
laneLabel: "Lane labels",
nodeHigh: "Node highlights",
LAOHigh: "Lane angle overrides",
CSOHigh: "Continue straight overrides",
heuristics: "Lane heuristics candidates",
posHeur: "Positive heuristics candidate",
negHeur: "Negative heuristics candidate",
highColor: "Highlight Colors",
colTooltip: "Click to toggle color inputs",
selAllTooltip: "Click on turn name to toggle all lane associations",
fwdCol: "Fwd (A>B)",
revCol: "Rev (B>A)",
labelCol: "Labels",
errorCol: "Lane errors",
laneNodeCol: "Nodes with lanes",
nodeTIOCol: "Nodes with TIOs",
LAOCol: "Segs with TIOs",
viewCSCol: "View only CS",
hearCSCol: "View and hear CS",
heurPosCol: "Lane heuristics likely",
heurNegCol: "Lane heuristics - not qualified",
advTools: "Advanced Tools",
quickTog: "Quick toggle all lanes",
showRBS: "Use RBS heuristics",
delFwd: "Delete FWD Lanes",
delRev: "Delete Rev Lanes",
delOpp: "This segment is one-way but has lanes set in the opposite direction. Click here to delete them",
csIcons: "Highlight CS Icons",
highlightOverride: "Only highlight if segment layer active",
addTIO: "Include TIO in lanes tab",
labelTIO: "TIO",
defaultTIO: "Waze Selected",
noneTIO: "None",
tlTIO: "Turn Left",
trTIO: "Turn Right",
klTIO: "Keep Left",
krTIO: "Keep Right",
conTIO: "Continue",
elTIO: "Exit Left",
erTIO: "Exit Right",
uturnTIO: "U-Turn",
enIcons: "Display lane icons on map",
IconsRotate: "Rotate map icons with segment direction",
},
"en-us": {
enabled: "Enabled",
disabled: "Disabled",
toggleShortcut: "Toggle Shortcut",
UIEnhance: "Tab UI Enhancements",
autoWidth: "Auto-open road width",
autoOpen: "Auto-open lanes tab",
autoExpand: "Auto-expand lane editor",
autoFocus: "Auto-focus lane input",
reOrient: "Re-orient lane icons",
enClick: "Enable ClickSaver",
clickStraight: "All straight lanes",
clickTurn: "Turn lanes",
mapHighlight: "Map Highlights",
laneLabel: "Lane labels",
nodeHigh: "Node highlights",
LAOHigh: "Lane angle overrides",
CSOHigh: "Continue straight overrides",
heuristics: "Lane heuristics candidates",
posHeur: "Positive heuristics candidate",
negHeur: "Negative heuristics candidate",
highColor: "Highlight Colors",
colTooltip: "Click to toggle color inputs",
selAllTooltip: "Click on turn name to toggle all lane associations",
fwdCol: "Fwd (A>B)",
revCol: "Rev (B>A)",
labelCol: "Labels",
errorCol: "Lane errors",
laneNodeCol: "Nodes with lanes",
nodeTIOCol: "Nodes with TIOs",
LAOCol: "Segs with TIOs",
viewCSCol: "View only CS",
hearCSCol: "View and hear CS",
heurPosCol: "Lane heuristics likely",
heurNegCol: "Lane heuristics - not qualified",
advTools: "Advanced Tools",
quickTog: "Quick toggle all lanes",
showRBS: "Use RBS heuristics",
delFwd: "Delete FWD Lanes",
delRev: "Delete Rev Lanes",
delOpp: "This segment is one-way but has lanes set in the opposite direction. Click here to delete them",
csIcons: "Highlight CS Icons",
highlightOverride: "Only highlight if segment layer active",
addTIO: "Include TIO in lanes tab",
labelTIO: "TIO",
defaultTIO: "Waze Selected",
noneTIO: "None",
tlTIO: "Turn Left",
trTIO: "Turn Right",
klTIO: "Keep Left",
krTIO: "Keep Right",
conTIO: "Continue",
elTIO: "Exit Left",
erTIO: "Exit Right",
uturnTIO: "U-Turn",
enIcons: "Display lane icons on map",
IconsRotate: "Rotate map icons with segment direction",
},
};
let MAX_LEN_HEUR; // Maximum length of segment where lane heuristics applied (Specified in Wiki).
let MAX_PERP_DIF; // Updated 2020-09, based on experiments
let MAX_PERP_DIF_ALT; // New 2020-09, based on experiments
let MAX_PERP_TO_CONSIDER; // Don't even consider perp angles outside of this tolerance
let MAX_STRAIGHT_TO_CONSIDER; // Don't even consider straight angles inside of this tolerance
let MAX_STRAIGHT_DIF; // IN TESTING; updated 2020-09
let lt_scanArea_recursive = 0;
let LtSettings;
let strings;
// let _turnInfo = [];
let _turnData = {};
let laneCount = 0;
const LTHighlightLayer = { name: "LT Highlights Layer" };
const LTNamesLayer = { name: "LT Names Layer" };
const LTLaneGraphics = { name: "LT Lane Graphics" };
let _pickleColor;
let UpdateObj;
let MultiAction;
let SetTurn;
let shortcutsDisabled = false;
let isRBS = false;
let allowCpyPst = false;
let langLocality = "default";
let LANG;
let seaPickle;
function applyNamesStyle(properties) {
return properties.layerName === LTNamesLayer.name;
}
function applyNodeHightlightStyle(properties) {
return properties.styleName === "nodeStyle" && properties.layerName === LTHighlightLayer.name;
}
function applyVectorHightlightStyle(properties) {
return properties.styleName === "vectorStyle" && properties.layerName === LTHighlightLayer.name;
}
function applyBoxStyle(properties) {
return properties.styleName === "boxStyle" && properties.layerName === LTLaneGraphics.name;
}
function applyIconBoxStyle(properties) {
return properties.styleName === "iconBoxStyle" && properties.layerName === LTLaneGraphics.name;
}
function applyIconStyle(properties) {
return properties.styleName === "iconStyle" && properties.layerName === LTLaneGraphics.name;
}
const styleConfig = {
styleContext: {
nameStyleLabelColor: (context) => {
return LtSettings.LabelColor;
},
nameStyleLaneNum: (context) => {
return context?.feature?.properties?.style?.laneNumLabel;
},
highlightStrokeColor: (context) => {
return context?.feature?.properties?.style?.stroke;
},
hightlightStrokeWidth: (context) => {
return context?.feature?.properties?.style?.strokeWidth;
},
hightlightStrokeOpacity: (context) => {
return context?.feature?.properties?.style?.strokeOpacity;
},
hightlightStrokeDashStyle: (context) => {
return context?.feature?.properties?.style?.strokeDashstyle;
},
highlightFillColor: (context) => {
return context?.feature?.properties?.style?.fillColor;
},
highlightPointRadius: (context) => {
return context?.feature?.properties?.style?.pointRadius;
},
externalGraphic: (context) => {
return context?.feature?.properties?.style?.externalGraphic;
},
graphicHeight: (context) => {
return context?.feature?.properties?.style?.graphicHeight;
},
graphicWidth: (context) => {
return context?.feature?.properties?.style?.graphicWidth;
},
rotation: (context) => {
return context?.feature?.properties?.style?.rotation;
},
backgroundGraphic: (context) => {
return context?.feature?.properties?.style?.backgroundGraphic;
},
backgroundHeight: (context) => {
return context?.feature?.properties?.style?.backgroundHeight;
},
backgroundWidth: (context) => {
return context?.feature?.properties?.style?.backgroundWidth;
},
backgroundXOffset: (context) => {
return context?.feature?.properties?.style?.backgroundXOffset;
},
backgroundYOffset: (context) => {
return context?.feature?.properties?.style?.backgroundYOffset;
},
},
styleRules: [
{
predicate: applyNamesStyle,
style: {
fontFamily: "Open Sans, Alef, helvetica, sans-serif, monospace",
labelColor: "${nameStyleLabelColor}",
labelText: "${nameStyleLaneNum}",
labelOutlineColor: "black",
fontColor: "${nameStyleLabelColor}",
fontSize: "16",
labelXOffset: 15,
labelYOffset: -15,
labelOutlineWidth: "3",
label: "${nameStyleLaneNum}",
angle: "",
labelAlign: "cm",
strokeWidth: 0,
pointRadius: 0,
},
},
{
predicate: applyNodeHightlightStyle,
style: {
fillColor: "${highlightFillColor}",
pointRadius: "${highlightPointRadius}",
fillOpacity: 0.9,
strokeWidth: 0,
},
},
{
predicate: applyVectorHightlightStyle,
style: {
strokeColor: "${highlightStrokeColor}",
stroke: "${highlightStrokeColor}",
strokeWidth: "${hightlightStrokeWidth}",
strokeOpacity: "${hightlightStrokeOpacity}",
strokeDashstyle: "${hightlightStrokeDashStyle}",
},
},
{
predicate: applyBoxStyle,
style: {
strokeColor: "#ffffff",
strokeOpacity: 1,
strokeWidth: 8,
fillColor: "#ffffff",
},
},
{
predicate: applyIconBoxStyle,
style: {
strokeColor: "#000000",
strokeOpacity: 1,
strokeWidth: 1,
fillColor: "#26bae8",
},
},
{
predicate: applyIconStyle,
style: {
externalGraphic: "${externalGraphic}",
graphicHeight: "${graphicHeight}",
graphicWidth: "${graphicWidth}",
fillColor: "#26bae8",
fillOpacity: 1,
backgroundColor: "#26bae8",
strokeColor: "#26bae8",
rotation: "${rotation}",
backgroundGraphic: "${backgroundGraphic}",
backgroundHeight: "${backgroundHeight}",
backgroundWidth: "${backgroundWidth}",
backgroundXOffset: "${backgroundXOffset}",
backgroundYOffset: "${backgroundYOffset}",
},
},
],
};
console.log("LaneTools: initializing...");
function laneToolsBootstrap(tries = 0) {
console.log("Lane Tools: Initializing...");
const locale = sdk.Settings.getLocale();
LANG = locale.localeCode.toLowerCase();
if (!(LANG in TAB_TRANSLATIONS))
langLocality = "en-us";
else
langLocality = LANG;
initLaneTools();
console.log("Lane Tools: Initialization Finished.");
}
function initLaneTools() {
startScriptUpdateMonitor();
seaPickle = sdk.State.getUserInfo();
UpdateObj = require("Waze/Action/UpdateObject");
MultiAction = require("Waze/Action/MultiAction");
SetTurn = require("Waze/Model/Graph/Actions/SetTurn");
const ltCss = [
'.lt-wrapper {position:relative;width:100%;font-size:12px;font-family:"Rubik", "Boing-light", sans-serif;user-select:none;}',
".lt-section-wrapper {display:block;width:100%;padding:4px;}",
".lt-section-wrapper.border {border-bottom:1px solid grey;margin-bottom:5px;}",
".lt-option-container {padding:3px;}",
".lt-option-container.color {text-decoration:none;}",
'input[type="checkbox"].lt-checkbox {position:relative;top:3px;vertical-align:top;margin:0;}',
'input[type="text"].lt-color-input {position:relative;width:70px;padding:3px;border:2px solid black;border-radius:6px;}',
'input[type="text"].lt-color-input:focus {outline-width:0;}',
"label.lt-label {position:relative;max-width:90%;font-weight:normal;padding-left:5px}",
".lt-Toolbar-Container {display:none;position:absolute;background-color:orange;border-radius:6px;border:1.5px solid;box-size:border-box;z-index:1050;}",
".lt-Toolbar-Wrapper {position:relative;padding:3px;}",
".lt-toolbar-button-container {display:inline-block;padding:5px;}",
".lt-toolbar-button {position:relative;display:block;width:60px;height:25px;border-radius:6px;font-size:12px;}",
".lt-add-Width {display:inline-block;width:15px;height:15px;border:1px solid black;border-radius:8px;margin:0 3px 0 3px;line-height: 1.5;text-align:center;font-size:10px;}",
".lt-add-Width:hover {border:1px solid #26bae8;background-color:#26bae8;cursor:pointer;}",
".lt-add-lanes {display:inline-block;width:15px;height:15px;border:1px solid black;border-radius:8px;margin:0 3px 0 3px;line-height: 1.5;text-align:center;font-size:10px;}",
".lt-add-lanes:hover {border:1px solid #26bae8;background-color:#26bae8;cursor:pointer;}",
".lt-chkAll-lns {display:inline-block;width:20px;height:20px;text-decoration:underline;font-weight:bold;font-size:10px;padding-left:3px;cursor:pointer;}",
".lt-tio-select {max-width:80%;color:rgb(32, 33, 36);background-color:rgb(242, 243, 244);border:0px;border-radius:6px;padding:0 16px 0 10px;cursor:pointer;}",
"#lt-color-title {display:block;width:100%;padding:5px 0 5px 0;font-weight:bold;text-decoration:underline;cursor:pointer;}",
].join(" ");
const $ltTab = $("<div>");
$ltTab.html = [
`<div class='lt-wrapper' id='lt-tab-wrapper'>
<div class='lt-section-wrapper' id='lt-tab-body'>
<div class='lt-section-wrapper border' style='border-bottom:2px double grey;'>
<a href='https://www.waze.com/forum/viewtopic.php?f=819&t=301158' style='font-weight:bold;font-size:12px;text-decoration:underline;' target='_blank'>LaneTools - v${LANETOOLS_VERSION}</a>
<div>
<div style='display:inline-block;'><span class='lt-trans-tglshcut'></span>:<span id='lt-EnableShortcut' style='padding-left:10px;'></span></div>
<div class='lt-option-container' style='float:right;'>
<input type=checkbox class='lt-checkbox' id='lt-ScriptEnabled' />
<label class='lt-label' for='lt-ScriptEnabled'><span class='lt-trans-enabled'></span></label>
</div>
</div>
</div>
<div class='lt-section-wrapper' id='lt-LaneTabFeatures'>
<div class='lt-section-wrapper border'>
<span style='font-weight:bold;'><span id='lt-trans-uiEnhance'></span></span>
<div class='lt-option-container' style='float:right;'>
<input type=checkbox class='lt-checkbox' id='lt-UIEnable' />
<label class='lt-label' for='lt-UIEnable'><span class='lt-trans-enabled'></span></label>
</div>
</div>
<div id='lt-UI-wrapper'>
<div class='lt-option-container' style='margin-bottom:5px;'>
<div style='display:inline-block;'><span class='lt-trans-tglshcut'></span>:<span id='lt-UIEnhanceShortcut' style='padding-left:10px;'></span></div>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-AutoOpenWidth' />
<label class='lt-label' for='lt-AutoOpenWidth'><span id='lt-trans-autoWidth'></span></label>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-AutoLanesTab' />
<label class='lt-label' for='lt-AutoLanesTab'><span id='lt-trans-autoTab'></span></label>
</div>
<div class='lt-option-container' style='display:none;'>
<input type=checkbox class='lt-checkbox' id='lt-AutoExpandLanes' />
<label class='lt-label' for='lt-AutoExpandLanes'><span title="Feature disabled as of Aug 27, 2022 to prevent flickering issue" id='lt-trans-autoExpand'></span></label>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-AutoFocusLanes' />
<label class='lt-label' for='lt-AutoFocusLanes'><span id='lt-trans-autoFocus'></span></label>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-highlightCSIcons' />
<label class='lt-label' for='lt-highlightCSIcons'><span id='lt-trans-csIcons'></span></label>
</div>
<div class='lt-option-container' style='display:none;'>
<input type=checkbox class='lt-checkbox' id='lt-ReverseLanesIcon' />
<label class='lt-label' for='lt-ReverseLanesIcon'><span title="Feature disabled as of July 21, 2023 because lanes displayed wrong" id='lt-trans-orient'></span></label>
</div>
<div class='lt-option-container' style='display:none;'>
<input type=checkbox class='lt-checkbox' id='lt-AddTIO' />
<label class='lt-label' for='lt-AddTIO'><span id='lt-trans-AddTIO'></span></label>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-ClickSaveEnable' />
<label class='lt-label' for='lt-ClickSaveEnable'><span id='lt-trans-enClick'></span></label>
</div>
<div class='lt-option-container clk-svr' style='padding-left:10%;'>
<input type=checkbox class='lt-checkbox' id='lt-ClickSaveStraight' />
<label class='lt-label' for='lt-ClickSaveStraight'><span id='lt-trans-straClick'></span></label>
</div>
<div class='lt-option-container clk-svr' style='padding-left:10%;'>
<input type=checkbox class='lt-checkbox' id='lt-ClickSaveTurns' />
<label class='lt-label' for='lt-ClickSaveTurns'><span id='lt-trans-turnClick'></span></label>
</div>
</div>
</div>
<div class='lt-section-wrapper'>
<div class='lt-section-wrapper border'>
<span style='font-weight:bold;'><span id='lt-trans-mapHigh'></span></span>
<div class='lt-option-container' style='float:right;'>
<input type=checkbox class='lt-checkbox' id='lt-HighlightsEnable' />
<label class='lt-label' for='lt-HighlightsEnable'><span class='lt-trans-enabled'></span></label>
</div>
</div>
<div id='lt-highlights-wrapper'>
<div class='lt-option-container' style='margin-bottom:5px;'>
<div style='display:inline-block;'><span class='lt-trans-tglshcut'></span>:<span id='lt-HighlightShortcut' style='padding-left:10px;'></span></div>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-IconsEnable' />
<label class='lt-label' for='lt-IconsEnable'><span id='lt-trans-enIcons'></span></label>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-IconsRotate' />
<label class='lt-label' for='lt-IconsRotate'><span id='lt-trans-IconsRotate'></span></label>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-LabelsEnable' />
<label class='lt-label' for='lt-LabelsEnable'><span id='lt-trans-lnLabel'></span></label>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-NodesEnable' />
<label class='lt-label' for='lt-NodesEnable'><span id='lt-trans-nodeHigh'></span></label>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-LIOEnable' />
<label class='lt-label' for='lt-LIOEnable'><span id='lt-trans-laOver'></span></label>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-CSEnable' />
<label class='lt-label' for='lt-CSEnable'><span id='lt-trans-csOver'></span></label>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-highlightOverride' />
<label class='lt-label' for='lt-highlightOverride'><span id='lt-trans-highOver'></span></label>
</div>
</div>
</div>
<div class='lt-section-wrapper'>
<div class='lt-section-wrapper border'>
<span style='font-weight:bold;'><span id='lt-trans-heurCan'></span></span>
<div class='lt-option-container' style='float:right;'>
<input type=checkbox class='lt-checkbox' id='lt-LaneHeuristicsChecks' />
<label class='lt-label' for='lt-LaneHeuristicsChecks'><span class='lt-trans-enabled'></span></label>
</div>
</div>
<div id='lt-heur-wrapper'>
<div class='lt-option-container' style='margin-bottom:5px;'>
<div style='display:inline-block;'><span class='lt-trans-tglshcut'></span>:<span id='lt-LaneHeurChecksShortcut' style='padding-left:10px;'></span></div>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-LaneHeurPosHighlight' />
<label class='lt-label' for='lt-LaneHeurPosHighlight'><span id='lt-trans-heurPos'></span></label>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-LaneHeurNegHighlight' />
<label class='lt-label' for='lt-LaneHeurNegHighlight'><span id='lt-trans-heurNeg'></span></label>
</div>
</div>
</div>
<div class='lt-section-wrapper'>
<div class='lt-section-wrapper'>
<span id='lt-color-title' data-original-title='${TAB_TRANSLATIONS[langLocality].colTooltip}'><span id='lt-trans-highCol'></span>:</span>
<div id='lt-color-inputs' style='display:none;'>
<div class='lt-option-container color'>
<input type=color class='lt-color-input' id='lt-ABColor' />
<label class='lt-label' for='lt-ABColor' id='lt-ABColorLabel'><span id='lt-trans-fwdCol'></span></label>
</div>
<div class='lt-option-container color'>
<input type=color class='lt-color-input' id='lt-BAColor' />
<label class='lt-label' for='lt-BAColor' id='lt-BAColorLabel'><span id='lt-trans-revCol'></span></label>
</div>
<div class='lt-option-container color'>
<input type=color class='lt-color-input' id='lt-LabelColor' />
<label class='lt-label' for='lt-LabelColor' id='lt-LabelColorLabel'><span id='lt-trans-labelCol'></span></label>
</div>
<div class='lt-option-container color'>
<input type=color class='lt-color-input' id='lt-ErrorColor' />
<label class='lt-label' for='lt-ErrorColor' id='lt-ErrorColorLabel'><span id='lt-trans-errorCol'></span></label>
</div>
<div class='lt-option-container color'>
<input type=color class='lt-color-input' id='lt-NodeColor' />
<label class='lt-label' for='lt-NodeColor' id='lt-NodeColorLabel'><span id='lt-trans-nodeCol'></span></label>
</div>
<div class='lt-option-container color'>
<input type=color class='lt-color-input' id='lt-TIOColor' />
<label class='lt-label' for='lt-TIOColor' id='lt-TIOColorLabel'><span id='lt-trans-tioCol'></span></label>
</div>
<div class='lt-option-container color'>
<input type=color class='lt-color-input' id='lt-LIOColor' />
<label class='lt-label' for='lt-TIOColor' id='lt-LIOColorLabel'><span id='lt-trans-laoCol'></span></label>
</div>
<div class='lt-option-container color'>
<input type=color class='lt-color-input' id='lt-CS1Color' />
<label class='lt-label' for='lt-CS1Color' id='lt-CS1ColorLabel'><span id='lt-trans-viewCol'></span></label>
</div>
<div class='lt-option-container color'>
<input type=color class='lt-color-input' id='lt-CS2Color' />
<label class='lt-label' for='lt-CS2Color' id='lt-CS2ColorLabel'><span id='lt-trans-hearCol'></span></label>
</div>
<div class='lt-option-container color'>
<input type=color class='lt-color-input' id='lt-HeurColor' />
<label class='lt-label' for='lt-HeurColor' id='lt-HeurColorLabel'><span id='lt-trans-posCol'></span></label>
</div>
<div class='lt-option-container color'>
<input type=color class='lt-color-input' id='lt-HeurFailColor' />
<label class='lt-label' for='lt-HeurFailColor' id='lt-HeurFailColorLabel'><span id='lt-trans-negCol'></span></label>
</div>
</div>
</div>
</div>
<div class='lt-section-wrapper' id='lt-adv-tools' style='display:none;'>
<div class='lt-section-wrapper border'>
<span style='font-weight:bold;'><span id='lt-trans-advTools'>></span></span>
</div>
<div class='lt-option-container'>
<input type=checkbox class='lt-checkbox' id='lt-SelAllEnable' />
<label class='lt-label' for='lt-SelAllEnable' ><span id='lt-trans-quickTog'></span></label>
</div>
<div class='lt-option-container' id='lt-serverSelectContainer' style='display:none;'>
<input type=checkbox class='lt-checkbox' id='lt-serverSelect' />
<label class='lt-label' for='lt-serverSelect'><span id='lt-trans-heurRBS'></span></label>
</div>
<div class='lt-option-container' id='lt-cpy-pst' style='display:none;'>
<input type=checkbox class='lt-checkbox' id='lt-CopyEnable' />
<label class='lt-label' for='lt-CopyEnable'>Copy/Paste Lanes</label>
<span style='font-weight: bold;'>(**Caution** - double check results, feature still in Dev)</span>
</div>
<div id='lt-sheet-link' style='display:none;'>
<a href='https://docs.google.com/spreadsheets/d/1_3sF09sMOid_us37j5CQqJZlBGGr1vI_3Rrmp5K-KCQ/edit?usp=sharing' target='_blank'>LT Config Sheet</a>
</div>
</div>
</div>
</div>`,
].join(" ");
const $ltButtons = $("<div>");
$ltButtons.html([
`<div class='lt-Toolbar-Container' id="lt-toolbar-container">
<div class='lt-Toolbar-Wrapper'>
<div class='lt-toolbar-button-container'>
<button type='button' class='lt-toolbar-button' id='copyA-button'>Copy A</button>
</div>
<div class='lt-toolbar-button-container'>
<button type='button' class='lt-toolbar-button' id='copyB-button'>Copy B</button>
</div>
<div class='lt-toolbar-button-container'>
<button type='button' class='lt-toolbar-button' id='pasteA-button'>Paste A</button>
</div>
<div class='lt-toolbar-button-container'>
<button type='button' class='lt-toolbar-button' id='pasteB-button'>Paste B</button>
</div>
</div>
</div>`,
].join(" "));
_pickleColor = seaPickle?.rank;
const proceedReady = _pickleColor && _pickleColor >= 0;
if (proceedReady) {
// WazeWrap.Interface.Tab("LT", $ltTab.html, setupOptions, "LT");
sdk.Sidebar.registerScriptTab().then((r) => {
r.tabLabel.innerHTML = "LT";
r.tabPane.innerHTML = $ltTab.html;
setupOptions().then(() => {
scanArea();
lanesTabSetup();
displayLaneGraphics();
});
});
$(`<style type="text/css">${ltCss}</style>`).appendTo("head");
$("#map").append($ltButtons.html());
WazeWrap.Interface.ShowScriptUpdate(GM_info.script.name, GM_info.script.version, LI_UPDATE_NOTES, GF_LINK, FORUM_LINK);
console.log("LaneTools: loaded");
}
else {
console.error("LaneTools: loading error....");
}
}
function startScriptUpdateMonitor() {
try {
const updateMonitor = new WazeWrap.Alerts.ScriptUpdateMonitor(GM_info.script.name, GM_info.script.version, DOWNLOAD_URL, GM_xmlhttpRequest, DOWNLOAD_URL);
updateMonitor.start();
}
catch (ex) {
// Report, but don't stop if ScriptUpdateMonitor fails.
console.error("WME LaneTools:", ex);
}
}
async function setupOptions() {
function setOptionStatus() {
// Set check boxes based on last use
setChecked("lt-ScriptEnabled", LtSettings.ScriptEnabled);
setChecked("lt-UIEnable", LtSettings.UIEnable);
setChecked("lt-AutoOpenWidth", LtSettings.AutoOpenWidth);
setChecked("lt-AutoExpandLanes", LtSettings.AutoExpandLanes);
setChecked("lt-AutoLanesTab", LtSettings.AutoLanesTab);
setChecked("lt-HighlightsEnable", LtSettings.HighlightsEnable);
setChecked("lt-LabelsEnable", LtSettings.LabelsEnable);
setChecked("lt-NodesEnable", LtSettings.NodesEnable);
setChecked("lt-LIOEnable", LtSettings.LIOEnable);
setChecked("lt-CSEnable", LtSettings.CSEnable);
setChecked("lt-highlightOverride", LtSettings.highlightOverride);
setChecked("lt-CopyEnable", LtSettings.CopyEnable);
setChecked("lt-SelAllEnable", LtSettings.SelAllEnable);
setChecked("lt-serverSelect", LtSettings.serverSelect);
setChecked("lt-AutoFocusLanes", LtSettings.AutoFocusLanes);
setChecked("lt-ReverseLanesIcon", LtSettings.ReverseLanesIcon);
setChecked("lt-ClickSaveEnable", LtSettings.ClickSaveEnable);
setChecked("lt-ClickSaveStraight", LtSettings.ClickSaveStraight);
setChecked("lt-ClickSaveTurns", LtSettings.ClickSaveTurns);
setChecked("lt-LaneHeurPosHighlight", LtSettings.LaneHeurPosHighlight);
setChecked("lt-LaneHeurNegHighlight", LtSettings.LaneHeurNegHighlight);
setChecked("lt-LaneHeuristicsChecks", LtSettings.LaneHeuristicsChecks);
setChecked("lt-highlightCSIcons", LtSettings.highlightCSIcons);
setChecked("lt-AddTIO", LtSettings.AddTIO);
setChecked("lt-IconsEnable", LtSettings.IconsEnable);
setChecked("lt-IconsRotate", LtSettings.IconsRotate);
setValue("lt-ABColor", LtSettings.ABColor);
setValue("lt-BAColor", LtSettings.BAColor);
setValue("lt-LabelColor", LtSettings.LabelColor);
setValue("lt-ErrorColor", LtSettings.ErrorColor);
setValue("lt-NodeColor", LtSettings.NodeColor);
setValue("lt-TIOColor", LtSettings.TIOColor);
setValue("lt-LIOColor", LtSettings.LIOColor);
setValue("lt-CS1Color", LtSettings.CS1Color);
setValue("lt-CS2Color", LtSettings.CS2Color);
setValue("lt-HeurColor", LtSettings.HeurColor);
setValue("lt-HeurFailColor", LtSettings.HeurFailColor);
if (!getId("lt-ClickSaveEnable")?.checked) {
$(".lt-option-container.clk-svr").hide();
}
if (!getId("lt-UIEnable")?.checked) {
$("#lt-UI-wrapper").hide();
}
if (!getId("lt-HighlightsEnable")?.checked) {
$("#lt-highlights-wrapper").hide();
}
if (!getId("lt-LaneHeuristicsChecks")?.checked) {
$("#lt-heur-wrapper").hide();
}
function setChecked(checkboxId, checked) {
$(`#${checkboxId}`).prop("checked", checked);
}
function setValue(inputId, color) {
const inputElem = $(`#${inputId}`);
inputElem.attr("value", color);
inputElem.css("border", `2px solid ${color}`);
}
}
await loadSettings();
await loadSpreadsheet();
initLaneGuidanceClickSaver();
// Layer for highlights
sdk.Map.addLayer({
layerName: LTHighlightLayer.name,
styleRules: styleConfig.styleRules,
zIndexing: true,
styleContext: styleConfig.styleContext,
});
sdk.LayerSwitcher.addLayerCheckbox(LTHighlightLayer);
sdk.Map.setLayerVisibility({
layerName: LTHighlightLayer.name,
visibility: LtSettings.highlightsVisible && LtSettings.HighlightsEnable,
});
// LTHighlightLayer = new OpenLayers.Layer.Vector("LTHighlightLayer", { uniqueName: "_LTHighlightLayer" });
// W.map.addLayer(LTHighlightLayer);
// LTHighlightLayer.setVisibility(true);
// Layer for future use of lane association icons...
// LTLaneGraphics = new OpenLayers.Layer.Vector("LTLaneGraphics", { uniqueName: "LTLaneGraphics" });
// W.map.addLayer(LTLaneGraphics);
// LTLaneGraphics.setVisibility(true);
sdk.Map.addLayer({
layerName: LTLaneGraphics.name,
styleRules: styleConfig.styleRules,
zIndexing: true,
styleContext: styleConfig.styleContext,
});
sdk.LayerSwitcher.addLayerCheckbox(LTLaneGraphics);
sdk.Map.setLayerVisibility({
layerName: LTLaneGraphics.name,
visibility: LtSettings.ltGraphicsVisible,
});
sdk.Events.on({
eventName: "wme-layer-checkbox-toggled",
eventHandler: (payload) => {
sdk.Map.setLayerVisibility({
layerName: payload.name,
visibility: payload.checked,
});
if (payload.name === LTLaneGraphics.name) {
LtSettings.ltGraphicsVisible = payload.checked;
}
else if (payload.name === LTHighlightLayer.name) {
LtSettings.highlightsVisible = payload.checked;
}
else if (payload.name === LTNamesLayer.name) {
LtSettings.ltNamesVisible = payload.checked;
}
saveSettings();
if (payload.checked)
scanArea();
},
});
sdk.Events.on({
eventName: "wme-save-finished",
eventHandler: (payload) => {
if (payload.success) {
scanArea();
lanesTabSetup();
displayLaneGraphics();
}
},
});
sdk.Map.addLayer({
layerName: LTNamesLayer.name,
styleRules: styleConfig.styleRules,
zIndexing: true,
styleContext: styleConfig.styleContext,
});
// Layer for lane text
sdk.LayerSwitcher.addLayerCheckbox(LTNamesLayer);
sdk.Map.setLayerVisibility({
layerName: LTNamesLayer.name,
visibility: LtSettings.ltNamesVisible,
});
sdk.LayerSwitcher.setLayerCheckboxChecked({
name: LTLaneGraphics.name,
isChecked: LtSettings.ltGraphicsVisible,
});
sdk.LayerSwitcher.setLayerCheckboxChecked({
name: LTHighlightLayer.name,
isChecked: LtSettings.highlightsVisible,
});
sdk.LayerSwitcher.setLayerCheckboxChecked({ name: LTNamesLayer.name, isChecked: LtSettings.ltNamesVisible });
sdk.Events.on({
eventName: "wme-map-move-end",
eventHandler: () => {
scanArea();
displayLaneGraphics();
},
});
sdk.Events.on({
eventName: "wme-map-zoom-changed",
eventHandler: () => {
scanArea();
displayLaneGraphics();
},
});
sdk.Events.on({
eventName: "wme-selection-changed",
eventHandler: () => {
scanArea();
lanesTabSetup();
displayLaneGraphics();
},
});
// Add keyboard shortcuts
try {
const enableHighlightsShortcut = {
shortcutId: "enableHighlights",
description: "Toggle lane highlights",
callback: toggleHighlights,
shortcutKeys: "",
};
sdk.Shortcuts.createShortcut(enableHighlightsShortcut);
// new WazeWrap.Interface.Shortcut(
// "enableHighlights",
// "Toggle lane highlights",
// "wmelt",
// "Lane Tools",
// LtSettings.enableHighlights,
// toggleHighlights,
// null
// ).add();
const enableUIEnhancementsShortcut = {
callback: toggleUIEnhancements,
shortcutId: "enableUIEnhancements",
description: "Toggle UI Enhancements",
shortcutKeys: "",
};
sdk.Shortcuts.createShortcut(enableUIEnhancementsShortcut);
// new WazeWrap.Interface.Shortcut(
// "enableUIEnhancements",
// "Toggle UI enhancements",
// "wmelt",
// "Lane Tools",
// LtSettings.enableUIEnhancements,
// toggleUIEnhancements,
// null
// ).add();
const enableHeuristicsShortcut = {
callback: toggleLaneHeuristicsChecks,
shortcutId: "enableHeuristics",
description: "Toggle heuristic highlights",
shortcutKeys: "",
};
sdk.Shortcuts.createShortcut(enableHeuristicsShortcut);
// new WazeWrap.Interface.Shortcut(
// "enableHeuristics",
// "Toggle heuristic highlights",
// "wmelt",
// "Lane Tools",
// LtSettings.enableHeuristics,
// toggleLaneHeuristicsChecks,
// null
// ).add();
const enableScriptShortcut = {
shortcutId: "enableScript",
description: "Toggle script",
callback: toggleScript,
shortcutKeys: "",
};
sdk.Shortcuts.createShortcut(enableScriptShortcut);
// new WazeWrap.Interface.Shortcut(
// "enableScript",
// "Toggle script",
// "wmelt",
// "Lane Tools",
// LtSettings.enableScript,
// toggleScript,
// null
// ).add();
}
catch (e) {
console.log("LT: Error creating shortcuts. This feature will be disabled.");
$("#lt-EnableShortcut").text(`${TAB_TRANSLATIONS[langLocality].disabled}`);
$("#lt-HighlightShortcut").text(`${TAB_TRANSLATIONS[langLocality].disabled}`);
$("#lt-UIEnhanceShortcut").text(`${TAB_TRANSLATIONS[langLocality].disabled}`);
$("#lt-LaneHeurChecksShortcut").text(`${TAB_TRANSLATIONS[langLocality].disabled}`);
shortcutsDisabled = true;
}
// Setup user options now that the settings are loaded
const highlights = $("#lt-HighlightsEnable");
const colorTitle = $("#lt-color-title");
const heurChecks = $("#lt-LaneHeuristicsChecks");
setOptionStatus();
setTimeout(() => {
updateShortcutLabels();
}, 50);
setHeuristics();
setLocalisation();
if (_pickleColor && _pickleColor > 0) {
let featureList = "LaneTools: The following special access features are enabled: ";
$("#lt-adv-tools").css("display", "block");
const quickTog = $("#lt-trans-quickTog");
quickTog.attr("data-original-title", `${strings.selAllTooltip}`);
quickTog.tooltip();
_.each(RBSArray, (u) => {
if (seaPickle !== null && u[0] === seaPickle.userName) {
if (u[1] === "1") {
isRBS = true;
}
if (u[2] === "1") {
allowCpyPst = true;
}
}
});
if (isRBS) {
$("#lt-serverSelectContainer").css("display", "block");
featureList += "RBS Heuristics";
}
if (allowCpyPst) {
$("#lt-sheet-link").css({
display: "block",
margin: "2px",
});
const ltSheetLinkAnchor = $("#lt-sheet-link > a");
ltSheetLinkAnchor.css({
padding: "2px",
border: "2px solid black",
"border-radius": "6px",
"text-decoration": "none",
});
ltSheetLinkAnchor
.on("mouseenter", function () {
$(this).css("background-color", "orange");
})
.on("mouseleave", function () {
$(this).css("background-color", "#eeeeee");
});
// $('#lt-cpy-pst').css('display', 'block');
// $('.lt-Toolbar-Container').draggable({ containment: 'parent', zIndex: '100' });
// WazeWrap.Events.register('selectionchanged', null, displayToolbar);
$(".lt-toolbar-button").on("click", function () {
if ($(this)[0].id === "copyA-button") {
copyLaneInfo("A");
}
if ($(this)[0].id === "copyB-button") {
copyLaneInfo("B");
}
if ($(this)[0].id === "pasteA-button") {
pasteLaneInfo("A");
}
if ($(this)[0].id === "pasteB-button") {
pasteLaneInfo("B");
}
});
featureList = isRBS ? `${featureList}, Copy/Paste` : `${featureList}Copy/Paste`;
}
if (isRBS || allowCpyPst) {
console.log(featureList);
}
}
else {
$("#lt-LaneTabFeatures").css("display", "none");
}
$(".lt-checkbox").on("click", function () {
const settingName = $(this)[0].id.substring(3);
LtSettings[settingName] = this.checked;
saveSettings();
});
$(".lt-color-input").on("change", function () {
const settingName = $(this)[0].id.substring(3);
LtSettings[settingName] = this.value;
saveSettings();
$(`#lt-${settingName}`).css("border", `2px solid ${this.value}`);
removeHighlights();
scanArea();
});
$("#lt-ScriptEnabled").on("click", () => {
if (getId("lt-ScriptEnabled")?.checked) {
scanArea();
}
else {
removeHighlights();
removeLaneGraphics();
}
});
highlights.on("click", () => {
if (getId("lt-HighlightsEnable")?.checked) {
scanArea();
}
else {
removeHighlights();
}
scanArea();
});
$("#lt-LabelsEnable").on("click", () => {
if (getId("lt-LabelsEnable")?.checked) {
scanArea();
}
else {
removeHighlights();
scanArea();
}
});
$("#lt-NodesEnable").on("click", () => {
if (getId("lt-NodesEnable")?.checked) {
scanArea();
}
else {
removeHighlights();
scanArea();
}
});
$("#lt-LIOEnable").on("click", () => {
if (getId("lt-LIOEnable")?.checked) {
scanArea();
}
else {
removeHighlights();
scanArea();
}
});
$("#lt-IconsEnable").on("click", () => {
if (getId("lt-IconsEnable")?.checked) {
displayLaneGraphics();
}
else {
removeLaneGraphics();
}
});
$("#lt-highlightOverride").on("click", () => {
if (getId("lt-highlightOverride")?.checked) {
scanArea();
}
else {
removeHighlights();
scanArea();
}
});
colorTitle.on("click", () => {
$("#lt-color-inputs").toggle();
});
$("#lt-ClickSaveEnable").on("click", () => {
$(".lt-option-container.clk-svr").toggle();
});
$("#lt-UIEnable").on("click", () => {
$("#lt-UI-wrapper").toggle();
removeLaneGraphics();
});
highlights.on("click", () => {
$("#lt-highlights-wrapper").toggle();
});
heurChecks.on("click", () => {
$("#lt-heur-wrapper").toggle();
});
heurChecks.on("click", () => {
if (getId("lt-LaneHeuristicsChecks")?.checked) {
scanArea();
}
else {
removeHighlights();
scanArea();
}
});
$("#lt-LaneHeurPosHighlight").on("click", () => {
if (getId("lt-LaneHeurPosHighlight")?.checked) {
scanArea();
}
else {
removeHighlights();
scanArea();
}
});
$("#lt-LaneHeurNegHighlight").on("click", () => {
if (getId("lt-LaneHeurNegHighlight")?.checked) {
scanArea();
}
else {
removeHighlights();
scanArea();
}
});
$("#lt-serverSelect").on("click", () => {
setHeuristics();
removeHighlights();
scanArea();
});
// Watches for the shortcut dialog to close and updates UI
const el = $.fn.hide;
$.fn.hide = function () {
this.trigger("hide");
return el.apply(this, arguments);
};
$("#keyboard-dialog").on("hide", () => {
checkShortcutsChanged();
});
colorTitle.tooltip();
}
async function loadSettings() {
const localSettings = JSON.parse(localStorage.getItem("LT_Settings"));
const serverSettings = await WazeWrap.Remote.RetrieveSettings("LT_Settings");
if (!serverSettings) {
console.error("LaneTools: Error communicating with WW settings server");
}
const defaultSettings = {
lastSaveAction: 0,
ScriptEnabled: true,
UIEnable: true,
AutoOpenWidth: false,
AutoExpandLanes: false,
AutoLanesTab: false,
HighlightsEnable: true,
LabelsEnable: true,
NodesEnable: true,
ABColor: "#990033",
BAColor: "#0033cc",
LabelColor: "#FFAD08",
ErrorColor: "#F50E0E",
NodeColor: "#66ccff",
TIOColor: "#ff9900",
LIOColor: "#ff9900",
CS1Color: "#04E6F6",
CS2Color: "#8F47FA",
HeurColor: "#00aa00",
HeurFailColor: "#E804F6",
CopyEnable: false,
SelAllEnable: false,
serverSelect: false,
LIOEnable: true,
CSEnable: true,
AutoFocusLanes: true,
ReverseLanesIcon: false,
ClickSaveEnable: true,
ClickSaveStraight: false,
ClickSaveTurns: true,
enableScript: "",
enableHighlights: "",
enableUIEnhancements: "",
enableHeuristics: "",
LaneHeurNegHighlight: false,
LaneHeurPosHighlight: false,
LaneHeuristicsChecks: false,
highlightCSIcons: false,
highlightOverride: true,
AddTIO: false,
IconsEnable: true,
IconsRotate: true,
highlightsVisible: false,
ltGraphicsVisible: false,
ltNamesVisible: false,
};
LtSettings = $.extend({}, defaultSettings, localSettings);
if (serverSettings && serverSettings.lastSaveAction > LtSettings.lastSaveAction) {
$.extend(LtSettings, serverSettings);
// console.log('LaneTools: server settings used');
}
else {
// console.log('LaneTools: local settings used');
}
}
async function saveSettings() {
const { ScriptEnabled, HighlightsEnable, LabelsEnable, NodesEnable, UIEnable, AutoLanesTab, AutoOpenWidth, AutoExpandLanes, ABColor, BAColor, LabelColor, ErrorColor, NodeColor, TIOColor, LIOColor, CS1Color, CS2Color, CopyEnable, SelAllEnable, serverSelect, LIOEnable, CSEnable, AutoFocusLanes, ReverseLanesIcon, ClickSaveEnable, ClickSaveStraight, ClickSaveTurns, enableScript, enableHighlights, enableUIEnhancements, enableHeuristics, HeurColor, HeurFailColor, LaneHeurPosHighlight, LaneHeurNegHighlight, LaneHeuristicsChecks, highlightCSIcons, highlightOverride, AddTIO, IconsEnable, IconsRotate, highlightsVisible, ltGraphicsVisible, ltNamesVisible, } = LtSettings;
const localSettings = {
lastSaveAction: Date.now(),
ScriptEnabled,
HighlightsEnable,
LabelsEnable,
NodesEnable,
UIEnable,
AutoOpenWidth,
AutoLanesTab,
AutoExpandLanes,
ABColor,
BAColor,
LabelColor,
ErrorColor,
NodeColor,
TIOColor,
LIOColor,
CS1Color,
CS2Color,
CopyEnable,
SelAllEnable,
serverSelect,
LIOEnable,
CSEnable,
AutoFocusLanes,
ReverseLanesIcon,
ClickSaveEnable,
ClickSaveStraight,
ClickSaveTurns,
enableScript,
enableHighlights,
enableUIEnhancements,
enableHeuristics,
HeurColor,
HeurFailColor,
LaneHeurPosHighlight,
LaneHeurNegHighlight,
LaneHeuristicsChecks,
highlightCSIcons,
highlightOverride,
AddTIO,
IconsEnable,
IconsRotate,
highlightsVisible,
ltGraphicsVisible,
ltNamesVisible,
};
// Grab keyboard shortcuts and store them for saving
// for (const name in W.accelerators.Actions) {
// const { shortcut, group } = W.accelerators.Actions[name];
// if (group === "wmelt") {
for (const shortcut of sdk.Shortcuts.getAllShortcuts()) {
localSettings[shortcut.shortcutId] = shortcut.shortcutKeys;
}
// Required for the instant update of changes to the keyboard shortcuts on the UI
LtSettings = localSettings;
if (localStorage) {
localStorage.setItem("LT_Settings", JSON.stringify(localSettings));
}
const serverSave = await WazeWrap.Remote.SaveSettings("LT_Settings", localSettings);
if (serverSave === null) {
console.warn("LaneTools: User PIN not set in WazeWrap tab");
}
else {
if (serverSave === false) {
console.error("LaneTools: Unable to save settings to server");
}
}
}
async function loadSpreadsheet() {
let connected = false;
const apiKey = "AIzaSyDZjmkSx5xWc-86hsAIzedgDgRgy8vB7BQ";
const settingsFailFunc = (jqXHR, _textStatus, errorThrown) => {
console.error("LaneTools: Error loading settings:", errorThrown);
};
const rbsFailFunc = (_jqXHR, _textStatus, errorThrown) => {
console.error("LaneTools: Error loading RBS:", errorThrown);
if (!RBSArray.failed) {
WazeWrap.Alerts.error(GM_info.script.name, "Unable to load heuristics data for LG. This feature will not be available");
RBSArray.failed = true;
}
};
const translationsFailFunc = (jqXHR, textStatus, errorThrown) => {
console.error("LaneTools: Error loading trans:", errorThrown);
};
try {
await $.getJSON(`https://sheets.googleapis.com/v4/spreadsheets/1_3sF09sMOid_us37j5CQqJZlBGGr1vI_3Rrmp5K-KCQ/values/Translations!A2:C?key=${apiKey}`)
.done(async (transArray) => {
if (transArray.values.length > 0) {
_.each(transArray.values, (t) => {
if (!TAB_TRANSLATIONS[t[1]] && Number.parseInt(t[2], 10) === 1) {
TAB_TRANSLATIONS[t[1]] = JSON.parse(t[0]);
}
});
}
else {
translationsFailFunc(null, null, "Failed to get any translations");
}
})
.fail(translationsFailFunc);
}
catch (e) {
translationsFailFunc(null, null, e);
}
try {
await $.getJSON(`https://sheets.googleapis.com/v4/spreadsheets/1_3sF09sMOid_us37j5CQqJZlBGGr1vI_3Rrmp5K-KCQ/values/Angles!A2:B?key=${apiKey}`)
.done((serverSettings) => {
if (serverSettings.values.length > 0) {
_.each(serverSettings.values, (v) => {
if (!configArray[v[1]]) {
configArray[v[1]] = JSON.parse(v[0]);
}
});
connected = true;
}
else {
settingsFailFunc();
}
})
.fail(settingsFailFunc);
}
catch (e) {
settingsFailFunc(null, null, e);
}
try {
await $.getJSON(`https://sheets.googleapis.com/v4/spreadsheets/1_3sF09sMOid_us37j5CQqJZlBGGr1vI_3Rrmp5K-KCQ/values/RBS_Access!A2:C?key=${apiKey}`)
.done((allowedRBS) => {
if (allowedRBS.values.length > 0) {
for (let i = 0; i < allowedRBS.values.length; i++) {
RBSArray[i] = allowedRBS.values[i];
}
RBSArray.failed = false;
}
else {
rbsFailFunc();
}
})
.fail(rbsFailFunc);
}
catch (e) {
rbsFailFunc(null, null, e);
}
if (connected) {
_.each(configArray, (serverKey) => {
for (const k in serverKey) {
if (serverKey.hasOwnProperty(k)) {
const keyValue = serverKey[k];
serverKey[k] = Number.parseFloat(keyValue);
}
}
});
}
}
function setLocalisation() {
// langLocality = I18n.currentLocale().toLowerCase();
if (!(langLocality in TAB_TRANSLATIONS)) {
langLocality = "en";
}
if (TAB_TRANSLATIONS[langLocality]) {
strings = TAB_TRANSLATIONS[langLocality];
}
else if (langLocality.includes("-") && TAB_TRANSLATIONS[langLocality.split("-")[0]]) {
strings = TAB_TRANSLATIONS[langLocality.split("-")[0]];
}
// If there is no value set in any of the translated strings then use the defaults
for (const transString of Object.keys(strings)) {
if (strings[transString] === "") {
strings[transString] = TAB_TRANSLATIONS.default[transString];
}
}
$(".lt-trans-enabled").text(strings.enabled);
$(".lt-trans-tglshcut").text(strings.toggleShortcut);
$("#lt-trans-uiEnhance").text(strings.UIEnhance);
$("#lt-trans-autoTab").text(strings.autoOpen);
$("#lt-trans-autoWidth").text(strings.autoWidth);
$("#lt-trans-autoExpand").text(strings.autoExpand);
$("#lt-trans-autoFocus").text(strings.autoFocus);
$("#lt-trans-orient").text(strings.reOrient);
$("#lt-trans-enClick").text(strings.enClick);
$("#lt-trans-straClick").text(strings.clickStraight);
$("#lt-trans-turnClick").text(strings.clickTurn);
$("#lt-trans-mapHigh").text(strings.mapHighlight);
$("#lt-trans-lnLabel").text(strings.laneLabel);
$("#lt-trans-nodeHigh").text(strings.nodeHigh);
$("#lt-trans-laOver").text(strings.LAOHigh);
$("#lt-trans-csOver").text(strings.CSOHigh);
$("#lt-trans-heurCan").text(strings.heuristics);
$("#lt-trans-heurPos").text(strings.posHeur);
$("#lt-trans-heurNeg").text(strings.negHeur);
$("#lt-trans-highCol").text(strings.highColor);
$("#lt-trans-fwdCol").text(strings.fwdCol);
$("#lt-trans-revCol").text(strings.revCol);
$("#lt-trans-labelCol").text(strings.labelCol);
$("#lt-trans-errorCol").text(strings.errorCol);
$("#lt-trans-nodeCol").text(strings.laneNodeCol);
$("#lt-trans-tioCol").text(strings.nodeTIOCol);
$("#lt-trans-laoCol").text(strings.LAOCol);
$("#lt-trans-viewCol").text(strings.viewCSCol);
$("#lt-trans-hearCol").text(strings.hearCSCol);
$("#lt-trans-posCol").text(strings.heurPosCol);
$("#lt-trans-negCol").text(strings.heurNegCol);
$("#lt-trans-advTools").text(strings.advTools);
$("#lt-trans-quickTog").text(strings.quickTog);
$("#lt-trans-heurRBS").text(strings.showRBS);
$("#lt-trans-csIcons").text(strings.csIcons);
$("#lt-trans-highOver").text(strings.highlightOverride);
$("#lt-trans-AddTIO").text(strings.addTIO);
$("#lt-trans-enIcons").text(strings.enIcons);
$("#lt-trans-IconsRotate").text(strings.IconsRotate);
$("#lt-color-title").attr("data-original-title", strings.colTooltip);
if (shortcutsDisabled) {
$("#lt-EnableShortcut").text(`${strings.disabled}`);
$("#lt-HighlightShortcut").text(`${strings.disabled}`);
$("#lt-UIEnhanceShortcut").text(`${strings.disabled}`);
$("#lt-LaneHeurChecksShortcut").text(`${strings.disabled}`);
}
}
function setHeuristics() {
if (RBSArray.failed) {
return;
}
const angles = isRBS && getId("lt-serverSelect").checked ? configArray.RBS : configArray.RPS;
MAX_LEN_HEUR = angles.MAX_LEN_HEUR;
MAX_PERP_DIF = angles.MAX_PERP_DIF;
MAX_PERP_DIF_ALT = angles.MAX_PERP_DIF_ALT;
MAX_PERP_TO_CONSIDER = angles.MAX_PERP_TO_CONSIDER;
MAX_STRAIGHT_TO_CONSIDER = angles.MAX_STRAIGHT_TO_CONSIDER;
MAX_STRAIGHT_DIF = angles.MAX_STRAIGHT_DIF;
}
// Checks the WME value of a shortcut (from the shortcut menu) against the scripts value and saves if they are different
function checkShortcutsChanged() {
let triggerSave = false;
for (const name in W.accelerators.Actions) {
const { shortcut, group } = W.accelerators.Actions[name];
if (group === "wmelt") {
let TempKeys = "";
if (shortcut) {
if (shortcut.altKey === true) {
TempKeys += "A";
}
if (shortcut.shiftKey === true) {
TempKeys += "S";
}
if (shortcut.ctrlKey === true) {
TempKeys += "C";
}
if (TempKeys !== "") {
TempKeys += "+";
}
if (shortcut.keyCode) {
TempKeys += shortcut.keyCode;
}
}
else {
TempKeys = "-1";
}
if (LtSettings[name] !== TempKeys) {
triggerSave = true;
console.log(`LaneTools: Stored shortcut ${name}: ${LtSettings[name]} changed to ${TempKeys}`);
break;
}
}
}
if (triggerSave) {
saveSettings();
setTimeout(() => {
updateShortcutLabels();
}, 200);
}
}
// Pulls the keyboard shortcuts from the script and returns a machine value
function getKeyboardShortcut(shortcut) {
if (shortcut === null)
return null;
const keys = LtSettings[shortcut];
if (typeof keys !== "string")
return null;
let val = "";
if (keys.indexOf("+") > -1) {
const specialKeys = keys.split("+")[0];
for (let i = 0; i < specialKeys.length; i++) {
if (val.length > 0) {
val += "+";
}
if (specialKeys[i] === "C") {
val += "Ctrl";
}
if (specialKeys[i] === "S") {
val += "Shift";
}
if (specialKeys[i] === "A") {
val += "Alt";
}
}
if (val.length > 0) {
val += "+";
}
let num = Number.parseInt(keys.split("+")[1]);
if (num >= 96 && num <= 105) {
// Numpad keys
num -= 48;
val += "[num pad]";
}
val += String.fromCharCode(num);
}
else {
let num = Number.parseInt(keys, 10);
if (num >= 96 && num <= 105) {
// Numpad keys
num -= 48;
val += "[num pad]";
}
val += String.fromCharCode(num);
}
return val;
}
// Updates the UI tab with the shortcut values
function updateShortcutLabels() {
if (!shortcutsDisabled) {
$("#lt-EnableShortcut").text(getKeyboardShortcut("enableScript") || "");
$("#lt-HighlightShortcut").text(getKeyboardShortcut("enableHighlights") || "");
$("#lt-UIEnhanceShortcut").text(getKeyboardShortcut("enableUIEnhancements") || "");
$("#lt-LaneHeurChecksShortcut").text(getKeyboardShortcut("enableHeuristics") || "");
}
}
function getLegacySegObj(id) {
return W.model.segments.getObjectById(id);
}
function getSegObj(id) {
if (!id)
return null;
return sdk.DataModel.Segments.getById({ segmentId: id });
}
function getNodeObj(id) {
if (id === null)
return null;
return sdk.DataModel.Nodes.getById({ nodeId: id });
}
function getLegacyNodeObj(id) {
return W.model.nodes.getObjectById(id);
}
function lanesTabSetup() {
// hook into edit panel on the left
if (getId("edit-panel")?.getElementsByTagName("wz-tabs").length === 0) {
setTimeout(lanesTabSetup, 500);
//console.log('Edit panel not yet loaded.');
return;
}
// const selSeg = W.selectionManager.getSelectedWMEFeatures();
const selection = sdk.Editing.getSelection();
let fwdDone = false;
let revDone = false;
let isRotated = false;
let expandEditTriggered = false;
// Highlights junction node when hovering over left panel
function hoverNodeTo() {
// W.model.nodes.getObjectById(W.selectionManager.getSegmentSelection().segments[0].attributes.toNodeID)
// .attributes.geometry;
// W.model.nodes.get(W.selectionManager.getSegmentSelection().segments[0].attributes.toNodeID).attributes.geometry
const selSeg = isSegmentSelected(selection)
? sdk.DataModel.Segments.getById({ segmentId: selection?.ids[0] })
: null;
const nodeB = selSeg?.toNodeId ? sdk.DataModel.Nodes.getById({ nodeId: selSeg.toNodeId }) : null;
nodeB && document.getElementById(nodeB?.id.toString());
// document.getElementById(
// W.model.nodes.getObjectById(W.selectionManager.getSegmentSelection().segments[0].attributes.toNodeID)
// .attributes.geometry.id
// );
// document.getElementById(W.model.nodes.get(W.selectionManager.getSegmentSelection().segments[0].attributes.toNodeID).attributes.geometry.id)
console.log("hovering to B");
}
function hoverNodeFrom() {
// W.model.nodes.getObjectById(W.selectionManager.getSegmentSelection().segments[0].attributes.fromNodeID)
// .attributes.geometry;
const selSeg = isSegmentSelected(selection)
? sdk.DataModel.Segments.getById({ segmentId: selection?.ids[0] })
: null;
const nodeA = selSeg?.fromNodeId ? sdk.DataModel.Nodes.getById({ nodeId: selSeg.fromNodeId }) : null;
nodeA && document.getElementById(nodeA?.id.toString());
// W.model.nodes.get(W.selectionManager.getSegmentSelection().segments[0].attributes.fromNodeID).attributes.geometry
// document.getElementById(
// W.model.nodes.getObjectById(W.selectionManager.getSegmentSelection().segments[0].attributes.fromNodeID)
// .attributes.geometry.id
// );
// document.getElementById(W.model.nodes.get(W.selectionManager.getSegmentSelection().segments[0].attributes.fromNodeID).attributes.geometry.id)
console.log("hovering to A");
}
function showAddLaneGuidance(laneDir) {
insertSelAll(laneDir);
addLnsBtns(laneDir);
adjustSpace();
focusEle();
applyButtonListeners();
// if (getId("lt-AddTIO")?.checked) addTIOUI(laneDir);
}
function updateUI() {
// if (eventInfo !== null) {
// eventInfo.stopPropagation();
// }
// if (getId('lt-ReverseLanesIcon').checked && !isRotated) {
// rotateArrows();
// }
if (getId("lt-highlightCSIcons")?.checked) {
colorCSDir();
}
// Add delete buttons and preselected lane number buttons to UI
if (_pickleColor && _pickleColor >= 1) {
const selSeg = isSegmentSelected(selection)
? sdk.DataModel.Segments.getById({ segmentId: selection?.ids[0] })
: null;
if (getId("li-del-opp-btn"))
$("#li-del-opp-btn").remove();
if (getId("li-del-fwd-btn"))
$("#li-del-fwd-btn").remove();
if (getId("li-del-rev-btn"))
$("#li-del-rev-btn").remove();
const $fwdButton = $(`<button type="button" id="li-del-fwd-btn" style="height:20px;background-color:white;border:1px solid grey;border-radius:8px;">${strings.delFwd}</button>`);
const $revButton = $(`<button type="button" id="li-del-rev-btn" style="height:20px;background-color:white;border:1px solid grey;border-radius:8px;">${strings.delRev}</button>`);
const $oppButton = $(`<button type="button" id="li-del-opp-btn" style="height:auto;background-color:orange;border:1px solid grey;border-radius:8px; margin-bottom:5px;">${strings.delOpp}</button>`);
const $btnCont1 = $('<div style="display:inline-block;position:relative;" />');
const $btnCont2 = $('<div style="display:inline-block;position:relative;" />');
const $btnCont3 = $('<div style="display:inline-block;position:relative;" />');
$fwdButton.appendTo($btnCont1);
$revButton.appendTo($btnCont2);
$oppButton.appendTo($btnCont3);
const delFwd = $("#li-del-fwd-btn");
const delRev = $("#li-del-rev-btn");
const delOpp = $("#li-del-opp-btn");
delFwd.off();
delRev.off();
delOpp.off();
if (!getId("li-del-rev-btn") && !revDone && selSeg?.toNodeLanesCount && selSeg.toNodeLanesCount > 0) {
if ($(".rev-lanes > div.lane-instruction.lane-instruction-from > div.instruction").length > 0) {
$btnCont2.prependTo(".rev-lanes > div.lane-instruction.lane-instruction-from > div.instruction");
$(".rev-lanes > div.lane-instruction.lane-instruction-from > div.instruction").css("border-bottom", `4px dashed ${LtSettings.BAColor}`);
}
else if (selSeg.isAtoB) {
//jm6087
$oppButton.prop("title", "rev");
$oppButton.prependTo("#edit-panel > div > div > div > div.segment-edit-section > wz-tabs > wz-tab.lanes-tab");
}
}
else {
$(".rev-lanes > div.lane-instruction.lane-instruction-from > div.instruction").css("border-bottom", `4px dashed ${LtSettings.BAColor}`);
}
if (!getId("li-del-fwd-btn") &&
!fwdDone &&
selSeg?.fromNodeLanesCount &&
selSeg.fromNodeLanesCount > 0) {
if ($(".fwd-lanes > div.lane-instruction.lane-instruction-from > div.instruction").length > 0) {
$btnCont1.prependTo(".fwd-lanes > div.lane-instruction.lane-instruction-from > div.instruction");
$(".fwd-lanes > div.lane-instruction.lane-instruction-from > div.instruction").css("border-bottom", `4px dashed ${LtSettings.ABColor}`);
}
else if (selSeg.isBtoA) {
//jm6087
$oppButton.prop("title", "fwd");
$oppButton.prependTo("#edit-panel > div > div > div > div.segment-edit-section > wz-tabs > wz-tab.lanes-tab");
}
}
else {
$(".fwd-lanes > div.lane-instruction.lane-instruction-from > div.instruction").css("border-bottom", `4px dashed ${LtSettings.ABColor}`);
}
$("#li-del-fwd-btn").on("click", () => {
delLanes("fwd");
fwdDone = true;
setTimeout(() => {
updateUI();
}, 200);
});
$("#li-del-rev-btn").on("click", () => {
delLanes("rev");
revDone = true;
setTimeout(() => {
updateUI();
}, 200);
});
$("#li-del-opp-btn").on("click", function () {
const dir = $(this).prop("title");
delLanes(dir);
if (dir === "rev") {
revDone = true;
}
else {
fwdDone = true;
}
updateUI();
});
}
waitForElementLoaded(".fwd-lanes > div.lane-instruction.lane-instruction-to > div.instruction > div.lane-edit > .edit-lane-guidance").then((elem) => {
$(elem).off();
$(elem).on("click", () => {
showAddLaneGuidance("fwd");
});
});
waitForElementLoaded(".rev-lanes > div.lane-instruction.lane-instruction-to > div.instruction > div.lane-edit > .edit-lane-guidance").then((elem) => {
$(elem).off();
$(elem).on("click", () => {
showAddLaneGuidance("rev");
});
});
if (!fwdDone && !revDone && !expandEditTriggered) {
expandEdit();
}
adjustSpace();
}
function applyButtonListeners() {
$(".apply-button.waze-btn.waze-btn-blue").off();
$(".cancel-button").off();
const fwdLanes = $(".fwd-lanes");
const revLanes = $(".rev-lanes");
fwdLanes.find(".apply-button.waze-btn.waze-btn-blue").on("click", () => {
fwdDone = true;
updateUI();
});
revLanes.find(".apply-button.waze-btn.waze-btn-blue").on("click", () => {
revDone = true;
updateUI();
});
fwdLanes.find(".cancel-button").on("click", () => {
fwdDone = true;
updateUI();
});
revLanes.find(".cancel-button").on("click", () => {
revDone = true;
updateUI();
});
}
function expandEdit() {
expandEditTriggered = true;
if (getId("lt-AutoExpandLanes")?.checked) {
if (!fwdDone) {
}
if (!revDone) {
}
}
if (getId("lt-AutoOpenWidth")?.checked) {
if (!fwdDone) {
$(".fwd-lanes").find(".set-road-width > wz-button").trigger("click"); // ADDED
}
if (!revDone) {
$(".rev-lanes").find(".set-road-width > wz-button").trigger("click"); // ADDED
}
}
}
function adjustSpace() {
$(".fwd-lanes > div > .direction-lanes").css({ padding: "5px 5px 10px", "margin-bottom": "10px" });
$(".rev-lanes > div > .direction-lanes").css({ padding: "5px 5px 10px", margin: "0px" });
$(".fwd-lanes > div > .lane-instruction.lane-instruction-to > .instruction > .lane-edit > .edit-region > div > .controls.direction-lanes-edit").css("padding-top", "10px");
$(".rev-lanes > div > .lane-instruction.lane-instruction-to > .instruction > .lane-edit > .edit-region > div > .controls.direction-lanes-edit").css("padding-top", "10px");
$(".fwd-lanes > div.lane-instruction.lane-instruction-to > div.instruction > div.edit-region > div > div > div:nth-child(1)").css("margin-bottom", "4px");
$(".rev-lanes > div.lane-instruction.lane-instruction-to > div.instruction > div.edit-region > div > div > div:nth-child(1)").css("margin-bottom", "4px");
}
function getLaneItems(count, class_names_list) {
const itemsList = [];
const classString = class_names_list.join(" ");
const idStringBase = class_names_list.join("-");
for (let i = 1; i <= count; ++i) {
const idString = `${idStringBase}-${i.toString()}`;
const selectorString = `<div class="${classString}" id="${idString}">${i.toString()}</div>`;
const newItem = $(selectorString).css({
padding: "1px 1px 1px 1px",
margin: "0 3px 0 3px",
border: "1px solid black",
"border-radius": "8px",
"border-color": "black",
height: "15px",
width: "15px",
"text-align": "center",
"line-height": "1.5",
"font-size": "10px",
display: "inline-block",
});
$(selectorString).on("hover", function () {
$(this).css({
border: "1px solid #26bae8",
"background-color": "#26bae8",
cursor: "pointer",
});
});
itemsList.push(newItem);
}
return itemsList;
}
function setupLaneCountControls(parentSelector, classNamesList) {
const jqueryClassSelector = `.${classNamesList.join(".")}`;
$(jqueryClassSelector).on("click", function () {
$(jqueryClassSelector).css({ "background-color": "transparent", color: "black" });
$(this).css({ "background-color": "navy", color: "white" });
});
}
function addLnsBtns(laneDir) {
// Add predetermined lane values
if (laneDir !== "fwd" && laneDir !== "rev") {
throw new Error(`Direction ${laneDir} is not supported`);
}
const dirLanesClass = `.${laneDir}-lanes`;
const addLanesTag = `lt-${laneDir}-add-lanes`;
const addWidthTag = `lt-${laneDir}-add-Width`;
const lanes = $(dirLanesClass);
if (lanes.find(".lane-instruction-to").children().length > 0x0 && !getId(addLanesTag)) {
const addLanesItem = $(`<div style="display:inline-flex;flex-direction:row;justify-content:space-around;margin-top:4px;position:relative;" id="${addLanesTag}" />`);
const classNamesList = ["lt-add-lanes", laneDir];
const laneCountsToAppend = getLaneItems(10, classNamesList);
for (let idx = 0; idx < laneCountsToAppend.length; ++idx) {
addLanesItem.append(laneCountsToAppend[idx]);
}
const prependSelector = `${dirLanesClass} > div.lane-instruction.lane-instruction-to > div.instruction > div.edit-region > div`;
// let prependSelector = dirLanesClass + "> div > div > div.lane-instruction.lane-instruction-to > div.instruction > div.edit-region > div.controls.direction-lanes-edit > div.form-group > div.controls-container";
waitForElementLoaded(prependSelector).then((elm) => {
const prependElement = $(prependSelector);
prependElement.prepend(addLanesItem);
setupLaneCountControls(lanes, classNamesList);
$(".lt-add-lanes").on("click", function () {
const numAddStr = $(this).text();
const numAdd = Number.parseInt(numAddStr, 10);
if ($(this).hasClass(`lt-add-lanes ${laneDir}`)) {
// As of React >=15.6. Triggering change or input events on the input form cannot be
// done via jquery selectors. Which means that they have to be triggered via
// React native calls.
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
const inputForm = document.querySelector(`wz-card${dirLanesClass} input[name=laneCount]`);
nativeInputValueSetter.call(inputForm, numAdd);
const inputEvent = new Event("input", { bubbles: true });
inputForm?.dispatchEvent(inputEvent);
const changeEvent = new Event("change", { bubbles: true });
inputForm?.dispatchEvent(changeEvent);
}
});
});
}
// if (revLanes.find(".direction-lanes").children().length > 0x0 && !getId("lt-rev-add-lanes")) {
// let revLanesItem = $(
// '<div style="display:inline-flex;flex-direction:row;justify-content:space-around;margin-top:4px;" id="lt-rev-add-lanes" />'),
// classNamesList = [ "lt-add-lanes", "rev" ], laneCountsToAppend = getLaneItems(10, classNamesList);
// for (let idx = 0; idx < laneCountsToAppend.length; ++idx) {
// revLanesItem.append(laneCountsToAppend[idx]);
// }
// let prependSelector = '.rev-lanes > div > div > div.lane-instruction.lane-instruction-to > div.instruction > div.edit-region > div.controls.direction-lanes-edit > div.form-group > div.controls-container';
// waitForElementLoaded(prependSelector).then((elm) => {
// let revPrependTo = $(prependSelector);
// revPrependTo.prepend(revLanesItem);
// // revLanesItem.appendTo('.rev-lanes > div > div > div.lane-instruction.lane-instruction-to > div.instruction > div.edit-region > div > div > div:nth-child(1)');
// setupLaneCountControls(revLanes, classNamesList);
// $('.lt-add-lanes').on("click",function () {
// let numAdd = $(this).text();
// numAdd = Number.parseInt(numAdd, 10);
// if ($(this).hasClass('lt-add-lanes rev')) {
// // As of React >=15.6. Triggering change or input events on the input form cannot be
// // done via jquery selectors. Which means that they have to be triggered via
// // React native calls.
// let nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
// let inputForm = document.querySelector("div.rev-lanes input[name=laneCount]");
// nativeInputValueSetter.call(inputForm, numAdd);
// let inputEvent = new Event('input', {bubbles: true});
// inputForm.dispatchEvent(inputEvent);
// let changeEvent = new Event('change', {bubbles: true});
// inputForm.dispatchEvent(changeEvent);
// }
// });
//
// })
// }
//if (lanes.find(".direction-lanes").children().length > 0 && !getId(addWidthTag)) {
// let addFwdLanes =
// $('<div style="display:inline-flex;flex-direction:row;width:100%;" id="'+addWidthTag+'" />'),
// classNamesList = ["lt-add-Width", laneDir], laneCountsToAppend = getLaneItems(8, classNamesList);
// for (let idx = 0; idx < laneCountsToAppend.length; ++idx) {
// addFwdLanes.append(laneCountsToAppend[idx]);
// }
// let lnSelector = $(dirLanesClass + " > div > .lane-instruction.lane-instruction-from > .instruction > .road-width-edit > div > div > div > .lane-width-card")
// addFwdLanes.prependTo(lnSelector);
// setupLaneCountControls(lnSelector, classNamesList);
//}
// if (revLanes.find(".direction-lanes").children().length > 0 && !getId("lt-rev-add-Width")) {
// let appendRevLanes =
// $('<div style="display:inline-flex;flex-direction:row;width:100%;" id="lt-rev-add-Width" />'),
// classNamesList = [ "lt-add-Width", "rev" ], laneCountsToAppend = getLaneItems(8, classNamesList);
// for (let idx = 0; idx < laneCountsToAppend.length; ++idx) {
// appendRevLanes.append(laneCountsToAppend[idx]);
// }
// let lnSelector = $(".rev-lanes > div > div > .lane-instruction.lane-instruction-from > .instruction > .road-width-edit > div > div > div > .lane-width-card");
// appendRevLanes.prependTo(lnSelector);
// setupLaneCountControls(lnSelector, classNamesList);
// }
$(".lt-add-Width").on("click", function () {
const numAddStr = $(this).text();
const numAdd = Number.parseInt(numAddStr, 10);
if ($(this).hasClass(`lt-add-Width ${laneDir}`)) {
const lanes = $(dirLanesClass);
lanes.find("#number-of-lanes").val(numAdd);
lanes.find("#number-of-lanes").trigger("change");
lanes.find("#number-of-lanes").trigger("focus");
}
// if ($(this).hasClass('lt-add-Width rev')) {
// const revLanes = $('.rev-lanes');
// revLanes.find('#number-of-lanes').val(numAdd);
// revLanes.find('#number-of-lanes').trigger("change");
// revLanes.find('#number-of-lanes').trigger("focus");
// }
});
}
function focusEle() {
// Places the focus on the relevant lanes # input if the direction exists
const autoFocusLanes = getId("lt-AutoFocusLanes");
if (autoFocusLanes?.checked) {
const fwdLanes = $(".fwd-lanes");
const revLanes = $(".rev-lanes");
if (fwdLanes.find(".edit-region").children().length > 0 && !fwdDone) {
fwdLanes.find(".form-control").trigger("focus");
}
else if (revLanes.find(".edit-region").children().length > 0 && !revDone) {
revLanes.find(".form-control").trigger("focus");
}
}
}
function insertSelAll(dir) {
const setAllEnable = getId("lt-SelAllEnable");
if (setAllEnable?.checked) {
$(".street-name").css("user-select", "none");
const inputDirection = dir === "fwd" ? $(".fwd-lanes").find(".form-control")[0] : $(".rev-lanes").find(".form-control")[0];
const startVal = $(inputDirection).val();
// Toggles all checkboxes in turns row
$(inputDirection).on("change", function () {
let boxDirection;
if ($(this).parents(".fwd-lanes").length) {
boxDirection = $(".fwd-lanes").find(".controls-container.turns-region");
}
else if ($(this).parents(".rev-lanes").length) {
boxDirection = $(".rev-lanes").find(".controls-container.turns-region");
}
boxDirection = $(".street-name", boxDirection);
for (let p = 0; p < boxDirection.length; p++) {
$(boxDirection[p]).off();
$(boxDirection[p]).on("click", function () {
const secParent = $(this).get(0);
const contParent = secParent.parentElement;
const chkBxs = $(".checkbox-large.checkbox-white", contParent);
const firstCheckInv = !getId(chkBxs[0].id)?.checked;
for (let i = 0; i < chkBxs.length; i++) {
const checkBox = $(`#${chkBxs[i].id}`);
checkBox.prop("checked", firstCheckInv);
checkBox.change();
}
});
}
});
if (startVal > 0) {
$(inputDirection).trigger("change");
}
}
}
function colorCSDir() {
const selSeg = isSegmentSelected(selection) && selection?.objectType === "segment"
? sdk.DataModel.Segments.getById({ segmentId: selection.ids[0] })
: null;
if (!selSeg)
return;
const fwdNode = getNodeObj(selSeg?.toNodeId);
const revNode = getNodeObj(selSeg?.fromNodeId);
const fwdConfig = checkLanesConfiguration(selSeg, fwdNode, fwdNode ? fwdNode.connectedSegmentIds : [], selSeg?.toNodeLanesCount);
const revConfig = checkLanesConfiguration(selSeg, revNode, revNode ? revNode.connectedSegmentIds : [], selSeg?.fromNodeLanesCount);
if (fwdConfig.csMode > 0) {
const csColor = fwdConfig.csMode === 1 ? LtSettings.CS1Color : LtSettings.CS2Color;
const arrowDiv = $("#segment-edit-lanes > div > div > div.fwd-lanes > div.lane-instruction.lane-instruction-to > div.instruction > div.lane-arrows > div").children();
for (let i = 0; i < arrowDiv.length; i++) {
if (arrowDiv[i].title === fwdConfig.csStreet) {
$(arrowDiv[i]).css("background-color", csColor);
}
}
}
if (revConfig.csMode > 0) {
const csColor = revConfig.csMode === 1 ? LtSettings.CS1Color : LtSettings.CS2Color;
const arrowDiv = $("#segment-edit-lanes > div > div > div.rev-lanes > div.lane-instruction.lane-instruction-to > div.instruction > div.lane-arrows > div").children();
for (let i = 0; i < arrowDiv.length; i++) {
if (arrowDiv[i].title === revConfig.csStreet) {
$(arrowDiv[i]).css("background-color", csColor);
}
}
}
}
// Rotates lane display arrows in lane tab for South directions
// Function written by Dude495 and modified by SkiDooGuy to fit into LaneTools better
function rotateArrows() {
const direction = document.getElementsByClassName("heading");
const boxDiv = $(".lane-arrows > div").get();
for (let i = 0; i < direction.length; i++) {
if (direction[i]?.textContent?.includes("south")) {
const arrows = $(boxDiv[i]).children();
$(arrows).css("transform", "rotate(180deg)");
$(boxDiv[i]).append(arrows.get().reverse());
}
}
isRotated = true;
}
// Begin lanes tab enhancements
if (getId("lt-UIEnable")?.checked && getId("lt-ScriptEnabled")?.checked) {
if (isSegmentSelected(selection)) {
// Check to ensure that there is only one segment object selected, then setup click event
waitForElementLoaded(".lanes-tab").then((elm) => {
formatLanesTab(getId("lt-AutoLanesTab")?.checked || elm.isActive);
});
//$('.lanes-tab').on("click",(event) => {
// fwdDone = false;
// revDone = false;
// updateUI(event);
//});
}
else if (selection && selection.ids.length === 2) {
// We have exactly TWO features selected. Check heuristics and highlight
scanHeuristicsCandidates(selection);
}
}
function formatLanesTab(clickTab = false, tries = 0) {
if ($(".tabs-labels > div:nth-child(3)", $(".segment-edit-section > wz-tabs")[0].shadowRoot).length > 0) {
fwdDone = false;
revDone = false;
$(".tabs-labels > div:nth-child(3)", $(".segment-edit-section > wz-tabs")[0].shadowRoot).on("click", () => {
fwdDone = false;
revDone = false;
updateUI();
});
if (clickTab) {
// If the auto open lanes option is enabled, initiate a click event on the Lanes tab element
waitForElementLoaded(".lanes-tab").then((elm) => {
$(".tabs-labels > div:nth-child(3)", $(".segment-edit-section > wz-tabs")[0].shadowRoot).trigger("click");
});
}
}
else if (tries < 500) {
setTimeout(() => {
formatLanesTab(clickTab, tries + 1);
}, 200);
}
else {
console.error("LaneTools: Failed to click lanes tab");
}
}
}
// Toggles parts of script when the keyboard shortcut is used
function toggleScript() {
$("#lt-ScriptEnabled").trigger("click");
}
function toggleHighlights() {
$("#lt-HighlightsEnable").trigger("click");
}
function toggleUIEnhancements() {
$("#lt-UIEnable").trigger("click");
}
function toggleLaneHeuristicsChecks() {
$("#lt-LaneHeuristicsChecks").trigger("click");
}
function displayToolbar() {
const objSelected = sdk.Editing.getSelection();
const scriptEnabled = getId("lt-ScriptEnabled");
const copyEnable = getId("lt-CopyEnable");
if (scriptEnabled?.checked && copyEnable && copyEnable.checked && objSelected && objSelected.ids.length === 1) {
if (objSelected.objectType === "segment") {
const map = sdk.Map.getMapViewportElement();
$("#lt-toolbar-container").css({
display: "block",
left: map.width() * 0.1,
top: map.height() * 0.1,
});
}
}
else {
$("#lt-toolbar-container").css("display", "none");
}
}
function getId(ele) {
return document.getElementById(ele);
}
function isSegment(obj) {
return obj && "roadType" in obj;
}
function isSegmentSelected(selection) {
return (selection && selection.objectType === "segment") || false;
}
// returns true if object is within window bounds and above zoom threshold
function onScreen(obj, curZoomLevel) {
if (!obj || !obj.geometry) {
return false;
}
// Either FREEWAY or Zoom >=4
if (curZoomLevel >= MIN_ZOOM_NON_FREEWAY || (isSegment(obj) && obj.roadType === LT_ROAD_TYPE.FREEWAY)) {
// var ext = W.map.getOLExtent();
var ext = sdk.Map.getMapExtent();
return true;
}
return false;
}
// borrowed from JAI
function getCardinalAngle(nodeId, segment) {
if (nodeId == null || segment == null) {
return null;
}
let ja_dx;
let ja_dy;
if (segment.fromNodeId === nodeId) {
const sp = lt_get_second_point(segment);
const fp = lt_get_first_point(segment);
if (!sp || !fp)
return null;
ja_dx = sp[0] - fp[0];
ja_dy = sp[1] - fp[1];
}
else {
const next_to_last = lt_get_next_to_last_point(segment);
const last_point = lt_get_last_point(segment);
if (!next_to_last || !last_point)
return null;
ja_dx = next_to_last[0] - last_point[0];
ja_dy = next_to_last[1] - last_point[1];
}
const angle_rad = Math.atan2(ja_dy, ja_dx);
let angle_deg = ((angle_rad * 180) / Math.PI) % 360;
if (angle_deg < 0)
angle_deg = angle_deg + 360;
// console.log('Cardinal: ' + Math.round(angle_deg));
return Math.round(angle_deg);
}
// borrowed from JAI
function lt_get_first_point(segment) {
return segment?.geometry.coordinates[0];
// return segment.geometry.components[0];
}
// borrowed from JAI
function lt_get_last_point(segment) {
return segment?.geometry.coordinates.at(-1);
// return segment.geometry.components[segment.geometry.components.length - 1];
}
// borrowed from JAI
function lt_get_second_point(segment) {
return segment?.geometry.coordinates[1];
// return segment.geometry.components[1];
}
// borrowed from JAI
function lt_get_next_to_last_point(segment) {
return segment?.geometry.coordinates.at(-2);
// return segment.geometry.components[segment.geometry.components.length - 2];
}
function delLanes(dir) {
const selObjs = W.selectionManager.getSelectedWMEFeatures();
const selSeg = selObjs[0]._wmeObject;
const turnGraph = W.model.getTurnGraph();
const mAction = new MultiAction();
let node;
let conSegs;
let updates = {};
// mAction.setModel(W.model);
if (dir === "fwd") {
updates.fwdLaneCount = 0;
node = getLegacyNodeObj(selSeg.attributes.toNodeID);
conSegs = node.getSegmentIds();
// const fwdLanes = $('.fwd-lanes');
// fwdLanes.find('.form-control').val(0);
// fwdLanes.find('.form-control').trigger("change");
}
if (dir === "rev") {
updates.revLaneCount = 0;
node = getLegacyNodeObj(selSeg.attributes.fromNodeID);
conSegs = node.getSegmentIds();
// const revLanes = $('.rev-lanes');
// revLanes.find('.form-control').val(0);
// revLanes.find('.form-control').trigger("change");
}
mAction.doSubAction(W.model, new UpdateObj(selSeg, updates));
for (let i = 0; i < conSegs.length; i++) {
let turnStatus = turnGraph.getTurnThroughNode(node, selSeg, getLegacySegObj(conSegs[i]));
let turnData = turnStatus.getTurnData();
if (turnData.hasLanes()) {
turnData = turnData.withLanes();
turnStatus = turnStatus.withTurnData(turnData);
mAction.doSubAction(W.model, new SetTurn(turnGraph, turnStatus));
}
}
mAction._description = "Deleted lanes and turn associations";
W.model.actionManager.add(mAction);
}
function removeHighlights() {
sdk.Map.removeAllFeaturesFromLayer({ layerName: LTHighlightLayer.name });
sdk.Map.removeAllFeaturesFromLayer({ layerName: LTNamesLayer.name });
}
function removeLaneGraphics() {
sdk.Map.removeAllFeaturesFromLayer({ layerName: LTLaneGraphics.name });
}
function applyName(geo, fwdLnsCount, revLnsCount) {
// if (!fwdLnsCount) fwdLnsCount = 0;
// if (!revLnsCount) revLnsCount = 0;
const laneNum = `${fwdLnsCount} / ${revLnsCount}`;
const lnLabel = turf.point(geo, { styleName: "nameStyle", layerName: LTNamesLayer.name, style: { laneNumLabel: laneNum } }, { id: `point_${geo.toString()}` });
sdk.Map.addFeatureToLayer({ feature: lnLabel, layerName: LTNamesLayer.name });
}
function highlightSegment(objGeo, direction, applyDash, applyLabels, fwdLnsCount, revLnsCount, applyLioHighlight, csMode, isBad, heur, heurOverHighlight) {
const VectorStyle = {
DASH_THIN: 1,
DASH_THICK: 2,
HIGHLIGHT: 10,
OVER_HIGHLIGHT: 20,
};
// fwdLnsCount = !fwdLnsCount ? 0 : fwdLnsCount;
// revLnsCount = !revLnsCount ? 0 : revLnsCount;
// const geo = objGeo.clone();
const applyCSHighlight = getId("lt-CSEnable")?.checked;
// Need to rework this to account for segment length, cause of geo adjustment and such
if (objGeo.length > 2) {
const geoLength = objGeo.length;
const geoMiddle = geoLength / 2;
const fwdPoint = geoLength % 2 ? Math.ceil(geoMiddle) - 1 : Math.ceil(geoMiddle);
const revPoint = geoLength % 2 ? Math.floor(geoMiddle) + 1 : Math.floor(geoMiddle);
if (direction === Direction.FORWARD) {
const newString = buildGeoComponentString(objGeo, fwdPoint, geoLength);
if (applyDash) {
createVector(newString, LtSettings.ABColor, VectorStyle.DASH_THIN);
} // draw dashed line
drawHighlight(newString, applyLioHighlight, isBad, heur, heurOverHighlight); // draw highlight
}
else if (direction === Direction.REVERSE) {
const newString = buildGeoComponentString(objGeo, 0, revPoint);
if (applyDash) {
createVector(newString, LtSettings.BAColor, VectorStyle.DASH_THIN);
}
drawHighlight(newString, applyLioHighlight, isBad, heur, heurOverHighlight);
}
// Add the label only on the forward pass, or reverse if there are no forward lanes
if (applyLabels && (direction === Direction.FORWARD || fwdLnsCount === 0)) {
if (geoLength % 2) {
applyName(objGeo[fwdPoint], fwdLnsCount, revLnsCount);
}
else {
const p0 = objGeo[revPoint - 1];
const p1 = objGeo[fwdPoint];
const newPoint = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];
// let newPoint = new OpenLayers.getOLGeometry().Point((p0.x + p1.x) / 2, (p0.y + p1.y) / 2);
// let newPoint = new OpenLayers.Geometry.Point((p0.x + p1.x) / 2, (p0.y + p1.y) / 2);
// var newPoint = {
// id: "pointNode_" + (p0[0] + p1[0]) / 2 + " " + (p0[1] + p1[1]) / 2,
// geometry: {
// coordinates: [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2],
// type: "Point",
// },
// type: "Feature",
// properties: { styleName: "styleNode", layerName: LTHighlightLayer.name },
// };
applyName(newPoint, fwdLnsCount, revLnsCount);
}
}
}
else {
const p0 = objGeo[0];
const p1 = objGeo[1];
// let point1 = new OpenLayers.getOLGeometry().Point((p0.x + p1.x) / 2, (p0.y + p1.y) / 2);
// let point1 = new OpenLayers.Geometry.Point((p0.x + p1.x) / 2, (p0.y + p1.y) / 2);
const p1C = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];
const pVector = [p1C];
// var point1 = {
// id: "pointNode_" + (p0[0] + p1[0]) / 2 + " " + (p0[1] + p1[1]) / 2,
// geometry: {
// coordinates: p1C,
// type: "Point",
// },
// type: "Feature",
// properties: { styleName: "vectorStyle", layerName: LTHighlightLayer.name },
// };
if (direction === Direction.FORWARD) {
const p2C = [objGeo[1][0], objGeo[1][1]];
pVector.push(p2C);
const newString = turf.lineString(pVector, { styleName: "vectorStyle", layerName: LTHighlightLayer.name }, { id: `line_${pVector.toString()}` });
if (applyDash) {
createVector(newString, LtSettings.ABColor, VectorStyle.DASH_THIN);
}
drawHighlight(newString, applyLioHighlight, isBad, heur, heurOverHighlight);
}
else if (direction === Direction.REVERSE) {
const p2C = [objGeo[0][0], objGeo[0][1]];
pVector.push(p2C);
const newString = turf.lineString(pVector, { styleName: "vectorStyle", layerName: LTHighlightLayer.name }, { id: `line_${pVector.toString()}` });
if (applyDash) {
createVector(newString, LtSettings.BAColor, VectorStyle.DASH_THIN);
}
drawHighlight(newString, applyLioHighlight, isBad, heur, heurOverHighlight);
}
// Add the label only on the forward pass, or reverse if there are no forward lanes
if (applyLabels && (direction === Direction.FORWARD || fwdLnsCount === 0)) {
applyName(p1C, fwdLnsCount, revLnsCount);
}
}
function buildGeoComponentString(geometry, from, to) {
const components = [];
let cIdx = 0;
for (let i = from; i < to; i++) {
components[cIdx++] = geometry[i];
}
return turf.lineString(components, { styleName: "vectorStyle", layerName: LTHighlightLayer.name }, { id: `line_${components.toString()}` });
}
function drawHighlight(newString, lio, bad, heurNom, heurOverHighlight = false) {
if (bad) {
createVector(newString, LtSettings.ErrorColor, VectorStyle.OVER_HIGHLIGHT);
return;
}
if (lio) {
createVector(newString, LtSettings.LIOColor, VectorStyle.HIGHLIGHT);
}
if (csMode === 1 && applyCSHighlight) {
createVector(newString, LtSettings.CS1Color, VectorStyle.HIGHLIGHT);
}
if (csMode === 2 && applyCSHighlight) {
createVector(newString, LtSettings.CS2Color, VectorStyle.HIGHLIGHT);
}
if (heurNom === HeuristicsCandidate.PASS) {
createVector(newString, LtSettings.HeurColor, heurOverHighlight ? VectorStyle.OVER_HIGHLIGHT : VectorStyle.HIGHLIGHT);
}
else if (heurNom === HeuristicsCandidate.FAIL) {
createVector(newString, LtSettings.HeurFailColor, heurOverHighlight ? VectorStyle.OVER_HIGHLIGHT : VectorStyle.HIGHLIGHT);
}
}
function createVector(geoCom, lineColor, style) {
// let newVector = new OpenLayers.Feature.Vector(geoCom, {}, {});
// LTHighlightLayer.addFeatures([newVector]);
const stroke = lineColor;
let strokeOpacity = 1;
let strokeWidth = 15;
let strokeDashArray = [];
switch (style) {
case VectorStyle.DASH_THICK:
strokeWidth = 8;
strokeDashArray = [8, 10];
break;
case VectorStyle.DASH_THIN:
strokeWidth = 4;
strokeDashArray = [10, 10];
break;
case VectorStyle.HIGHLIGHT:
strokeWidth = 15;
strokeOpacity = 0.6;
break;
case VectorStyle.OVER_HIGHLIGHT:
strokeWidth = 18;
strokeOpacity = 0.85;
break;
default:
break;
}
geoCom.properties = geoCom.properties ? geoCom.properties : {};
geoCom.properties.style = {
strokeColor: stroke,
stroke: stroke,
strokeWidth: strokeWidth,
strokeOpacity: strokeOpacity,
strokeDashstyle: strokeDashArray.join(" "),
};
sdk.Map.addFeatureToLayer({ feature: geoCom, layerName: LTHighlightLayer.name });
}
// LTHighlightLayer.setZIndex(2880);
}
function highlightNode(objGeo, color, overSized = false) {
// const geo = objGeo.clone();
// const highlight = new OpenLayers.Feature.Vector(geo, {});
if (!objGeo)
return;
const newString = {
id: `Node_${objGeo.toString()}`,
geometry: {
type: "Point",
coordinates: objGeo,
},
type: "Feature",
properties: {
styleName: "nodeStyle",
layerName: LTHighlightLayer.name,
style: {
fillColor: color,
pointRadius: overSized ? 18 : 10,
},
},
};
// let nodeStyle = {
// fillColor: color,
// pointRadius: overSized ? 18 : 10,
// fillOpacity: 0.9,
// strokeWidth: 0,
// };
// Object.assign(styleRules.nodeHighlightStyle.style, nodeStyle);
// LTHighlightLayer.addFeatures([highlight]);
sdk.Map.addFeatureToLayer({ feature: newString, layerName: LTHighlightLayer.name });
// const node = document.getElementById(geo.id);
// if (node) {
// node.setAttribute("fill", color);
// node.setAttribute("r", overSized ? "18" : "10");
// node.setAttribute("fill-opacity", "0.9");
// node.setAttribute("stroke-width", "0");
// }
}
const lt_scanArea_timer = {
timeoutID: -1,
start: function () {
this.cancel();
this.timeoutID = window.setTimeout(() => {
this.calculate();
}, 500);
},
calculate: function () {
scanArea_real();
this.timeoutID = -1;
},
cancel: function () {
if (typeof this.timeoutID === "number") {
window.clearTimeout(this.timeoutID);
this.timeoutID = -1;
lt_scanArea_recursive = 0;
}
},
};
function scanArea() {
// Use a delay timer to ensure the DOM is settled
lt_scanArea_recursive = 3;
scanArea_real();
}
function scanArea_real() {
const isEnabled = getId("lt-ScriptEnabled")?.checked;
const mapHighlights = getId("lt-HighlightsEnable")?.checked;
const heurChecks = getId("lt-LaneHeuristicsChecks")?.checked;
// const zoomLevel = W.map.getZoom() != null ? W.map.getZoom() : 16;
const zoomLevel = sdk.Map.getZoomLevel();
const highOverride = getId("lt-highlightOverride")?.checked; // jm6087
const layerCheck = W.layerSwitcherController.getTogglerState("ITEM_ROAD") ||
W.layerSwitcherController.getTogglerState("ITEM_ROAD_V2"); //jm6087
removeHighlights();
// console.log(zoomLevel);
if (zoomLevel < MIN_DISPLAY_LEVEL) {
return;
}
// If segment layer is checked (true) or (segment layer is not checked (false) and highlight override is set to show only when segment layer on - not checked (false)
if (layerCheck || (!layerCheck && !highOverride)) {
//jm6087
if (isEnabled && (mapHighlights || heurChecks)) {
scanSegments(sdk.DataModel.Segments.getAll(), false);
}
if (isEnabled && heurChecks) {
// const selFeat = W.selectionManager.getSelectedWMEFeatures();
const selectedFeat = sdk.Editing.getSelection();
if (selectedFeat?.objectType === "segment")
scanHeuristicsCandidates(selectedFeat);
}
} //jm6087
}
// Given two features, checks if they are segments, and their path qualifies for heuristics; then highlight
function scanHeuristicsCandidates(selection) {
const segs = [];
let count = 0;
for (let idx = 0; selection && idx < selection.ids.length; ++idx) {
if (typeof selection.ids[idx] === "string") {
lt_log(`Segment ID: ${selection.ids[idx]} reported as Segment ID incorrectly`, 1);
}
const seg = sdk.DataModel.Segments.getById({ segmentId: selection.ids[idx] });
if (!seg)
continue;
count = segs.push(seg);
}
// _.each(features, (f) => {
// if (f && f._wmeObject && f._wmeObject.type === "segment") {
// count = segs.push(f._wmeObject);
// }
// });
scanSegments(segs, true);
return count;
}
// Check all given segments for heuristics qualification
function scanSegments(segments, selectedSegsOverride = false) {
const heurChecks = getId("lt-LaneHeuristicsChecks")?.checked ?? false;
const heurScan_PosHighlight = heurChecks && (getId("lt-LaneHeurPosHighlight")?.checked ?? false);
const heurScan_NegHighlight = heurChecks && (getId("lt-LaneHeurNegHighlight")?.checked ?? false);
const mapHighlights = getId("lt-HighlightsEnable")?.checked ?? false;
const applyLioHighlight = mapHighlights && (getId("lt-LIOEnable")?.checked ?? false);
const applyLabels = mapHighlights && (getId("lt-LabelsEnable")?.checked ?? false);
const zoomLevel = sdk.Map.getZoomLevel();
// const turnGraph = W.model.getTurnGraph();
// console.log(zoomLevel);
_.each(segments, (s) => {
if (onScreen(s, zoomLevel)) {
// const sAtts = s.getAttributes();
const tryRedo = false;
const segLength = lt_segment_length(s);
// FORWARD
tryRedo || scanSegment_Inner(s, Direction.FORWARD, segLength, tryRedo);
// If errors encountered, scan again. (Usually this is an issue with first loading of DOM after zoom or long pan)
if (tryRedo && lt_scanArea_recursive > 0) {
lt_log("LT errors found, scanning again", 2);
removeHighlights();
lt_scanArea_recursive--;
lt_scanArea_timer.start();
return;
}
tryRedo || scanSegment_Inner(s, Direction.REVERSE, segLength, tryRedo);
// If errors encountered, scan again. (Usually this is an issue with first loading of DOM after zoom or long pan)
if (tryRedo && lt_scanArea_recursive > 0) {
lt_log("LT errors found, scanning again", 2);
removeHighlights();
lt_scanArea_recursive--;
lt_scanArea_timer.start();
}
}
});
function scanSegment_Inner(seg, direction, segLength, tryRedo) {
const fwdLaneCount = seg.fromNodeLanesCount;
const revLaneCount = seg.toNodeLanesCount;
if (fwdLaneCount + revLaneCount === 0)
return;
let node = getNodeObj(seg.toNodeId);
let oppNode = getNodeObj(seg.fromNodeId);
let laneCount = fwdLaneCount;
let oppLaneCount = revLaneCount;
if (direction !== Direction.FORWARD) {
node = getNodeObj(seg.fromNodeId);
oppNode = getNodeObj(seg.toNodeId);
laneCount = revLaneCount;
oppLaneCount = fwdLaneCount;
}
let tlns = false;
let tio = false;
let badLn = false;
let lio = false;
let csMode = 0;
let heurCand = HeuristicsCandidate.NONE;
let entrySeg = null;
const entrySegRef = {
seg: 0,
direction: Direction.ANY,
};
// CHECK LANES & HEURISTICS
if (node !== null && onScreen(node, zoomLevel)) {
const nodeSegs = node.connectedSegmentIds;
if (laneCount && laneCount > 0) {
const config = checkLanesConfiguration(seg, node, nodeSegs, laneCount);
tlns = config.tlns;
tio = config.tio;
lio = config.lio;
badLn = config.badLn;
csMode = config.csMode;
tryRedo = badLn || tryRedo;
}
// 1/1/21: Only check for heuristics on segments <50m. IMPORTANT because now we're checking segments regardless of lanes
if (heurChecks && segLength <= MAX_LEN_HEUR) {
// Check Heuristics regardless of heurChecks, because we want to report Errors even if Heur highlights are off
heurCand = isHeuristicsCandidate(seg, node, nodeSegs, oppNode, laneCount, segLength, entrySegRef);
if (heurCand === HeuristicsCandidate.ERROR) {
// fwdHeurCand = HeuristicsCandidate.NONE;
badLn = true;
}
if (!heurChecks) {
heurCand = HeuristicsCandidate.NONE;
}
else if (heurCand !== HeuristicsCandidate.NONE) {
entrySeg = { ...entrySegRef };
}
}
}
// HIGHLIGHTS
if (!selectedSegsOverride) {
// Full scan highlights
let heur = HeuristicsCandidate.NONE;
if ((heurScan_PosHighlight && heurCand === HeuristicsCandidate.PASS) ||
(heurScan_NegHighlight && heurCand === HeuristicsCandidate.FAIL)) {
heur = heurCand;
}
if (laneCount && (laneCount > 0 || heur !== null || badLn)) {
highlightSegment(seg.geometry.coordinates, direction, mapHighlights, applyLabels, fwdLaneCount, revLaneCount, lio && applyLioHighlight, csMode, badLn, heur, false);
}
// Nodes highlights
if (mapHighlights && getId("lt-NodesEnable")?.checked) {
if (tlns) {
highlightNode(node?.geometry.coordinates, LtSettings.NodeColor);
// highlightNode(node.geometry, `${LtSettings.NodeColor}`);
}
if (tio) {
highlightNode(node?.geometry.coordinates, LtSettings.TIOColor);
// highlightNode(node.geometry, `${LtSettings.TIOColor}`);
}
}
}
else {
// Selected segment highlights
lt_log(`candidate(f):${heurCand}`);
if (heurCand !== HeuristicsCandidate.NONE) {
if (entrySeg && segments.findIndex((element) => element.id === entrySeg.seg) > -1) {
const nodeColor = heurCand === HeuristicsCandidate.PASS ? LtSettings.NodeColor : LtSettings.HeurFailColor;
highlightSegment(seg.geometry.coordinates, direction, false, false, 0, 0, false, csMode, badLn, heurCand, true);
const eSeg = sdk.DataModel.Segments.getById({ segmentId: entrySeg.seg });
if (eSeg) {
highlightSegment(eSeg?.geometry.coordinates, entrySeg.direction, false, false, 0, 0, false, 0, false, heurCand, true);
}
highlightNode(node?.geometry.coordinates, nodeColor, true);
highlightNode(oppNode?.geometry.coordinates, nodeColor, true);
}
}
}
return tryRedo;
}
}
function checkLanesConfiguration(s, node, segs, numLanes) {
const laneConfig = {
tlns: false,
tio: false,
badLn: false,
lio: false,
csMode: 0,
csStreet: null,
};
const turnLanes = [];
// const turnGraph = W.model.getTurnGraph();
// const pturns = turnGraph.getAllPathTurns();
const fromTurns = sdk.DataModel.Turns.getTurnsFromSegment({ segmentId: s.id });
const toTurns = sdk.DataModel.Turns.getTurnsToSegment({ segmentId: s.id });
const pturns = fromTurns.filter((t) => t.isPathTurn);
pturns.push(...toTurns.filter((t) => t.isPathTurn));
const jpturns = fromTurns.filter((t) => t.isJunctionBoxTurn);
jpturns.push(...toTurns.filter((t) => t.isJunctionBoxTurn));
const zoomLevel = sdk.Map.getZoomLevel();
function addTurns(fromLns, toLns) {
if (toLns === undefined || fromLns === undefined)
return;
for (let k = fromLns; k < toLns + 1; k++) {
let newValue = true;
for (let j = 0; j < turnLanes.length; j++) {
if (turnLanes[j] === k) {
newValue = false;
}
}
if (newValue) {
turnLanes.push(k);
}
}
}
for (let i = 0; i < segs.length; i++) {
if (segs[i] === s.id)
continue;
const seg2 = getSegObj(segs[i]);
const turnsThrough = !node ? [] : sdk.DataModel.Turns.getTurnsThroughNode({ nodeId: node?.id });
for (let idx = 0; idx < turnsThrough.length; ++idx) {
const t = turnsThrough[idx];
if (t.isUTurn || (t.fromSegmentId !== s.id && t.toSegmentId !== segs[i]))
continue;
// const turnData = turnGraph.getTurnThroughNode(node, s, seg2).getTurnData();
if (t.isAllowed) {
// Check for turn instruction override
if (t.instructionOpCode !== null) {
laneConfig.tio = true;
}
// Check for lanes
if (t.lanes !== null) {
laneConfig.tlns = true;
// Check for lane angle override
if (t.lanes.angleOverride !== null) {
laneConfig.lio = true;
}
// Check for Continue Straight override
// 1 is for view only, 2 is for view and hear
const primaryStreetId = seg2?.primaryStreetId;
if (primaryStreetId && primaryStreetId !== null && s.primaryStreetId === primaryStreetId) {
if (t.lanes.guidanceMode === "display") {
laneConfig.csMode = 1;
laneConfig.csStreet = sdk.DataModel.Streets.getById({
streetId: primaryStreetId,
})?.name;
}
else if (t.lanes.guidanceMode === "display-and-voice") {
laneConfig.csMode = 2;
laneConfig.csStreet = sdk.DataModel.Streets.getById({
streetId: primaryStreetId,
})?.name;
}
}
const fromLns = t.lanes.fromLaneIndex;
const toLns = t.lanes.toLaneIndex;
addTurns(fromLns, toLns);
}
}
}
}
// check paths
for (let i = 0; i < pturns.length; i++) {
if (pturns[i].lanes !== null) {
const fromLns = pturns[i].lanes?.fromLaneIndex;
const toLns = pturns[i].lanes?.toLaneIndex;
addTurns(fromLns, toLns);
}
}
// check turns in JBs
// const jb = W.model.bigJunctions.getObjectArray();
for (let t = 0; t < jpturns.length; t++) {
const tdat = jpturns[t].lanes;
if (tdat) {
addTurns(tdat.fromLaneIndex, tdat.toLaneIndex);
}
}
turnLanes.sort();
for (let z = 0; z < turnLanes.length; z++) {
if (turnLanes[z] !== z) {
laneConfig.badLn = true;
}
}
if (numLanes && turnLanes.length < numLanes && onScreen(node, zoomLevel)) {
laneConfig.badLn = true;
}
return laneConfig;
}
function setTurns(direction) {
const clickSaveEnabled = getId("lt-ClickSaveEnable");
if (!clickSaveEnabled?.checked) {
return;
}
const lanesPane = document.getElementsByClassName(direction)[0];
if (!lanesPane)
return;
const left = lanesPane.getElementsByClassName("angle--135").length > 0
? "angle--135"
: lanesPane.getElementsByClassName("angle--90").length > 0
? "angle--90"
: "angle--45";
const right = lanesPane.getElementsByClassName("angle-135").length > 0
? "angle-135"
: lanesPane.getElementsByClassName("angle-90").length > 0
? "angle-90"
: "angle-45";
const turnSections = lanesPane.getElementsByClassName("turn-lane-edit-container");
let setLeft = false;
let setRight = false;
const alreadySet = [].slice
.call(turnSections)
.reduce((acc, turn) => acc +
[].slice
.call(turn.getElementsByTagName("input"))
.reduce((acc, input) => (input.checked === true ? acc + 1 : acc), 0), 0);
if (alreadySet === 0) {
for (let i = 0; i < turnSections.length; i++) {
const turnSection = turnSections[i];
// Check if the lanes are already set. If already set, don't change anything.
const laneCheckboxes = turnSection.getElementsByTagName("wz-checkbox");
if (laneCheckboxes && laneCheckboxes.length > 0) {
if (getId("lt-ClickSaveTurns")?.checked) {
if (turnSection.getElementsByClassName(left).length > 0 &&
laneCheckboxes[0].checked !== undefined &&
laneCheckboxes[0].checked === false) {
setLeft = true;
laneCheckboxes[0].click();
}
else if (turnSection.getElementsByClassName(right).length > 0 &&
laneCheckboxes[laneCheckboxes.length - 1].checked !== undefined &&
laneCheckboxes[laneCheckboxes.length - 1].checked === false) {
setRight = true;
laneCheckboxes[laneCheckboxes.length - 1].click();
}
}
}
}
for (let i = 0; i < turnSections.length; i++) {
const turnSection = turnSections[i];
const laneCheckboxes = turnSection.getElementsByTagName("wz-checkbox");
if (setRight) {
// Clear All Lanes Except the Right most for right turn
if (turnSection.getElementsByClassName(right).length > 0) {
for (let j = 0; j < laneCheckboxes.length - 1; ++j) {
waitForElementLoaded("input[type='checkbox']", laneCheckboxes[j].shadowRoot);
if (laneCheckboxes[j].checked)
laneCheckboxes[j].click();
}
}
}
if (setLeft) {
// Clear all Lanes except left most for left turn
if (turnSection.getElementsByClassName(left).length > 0) {
for (let j = 1; j < laneCheckboxes.length; ++j) {
waitForElementLoaded("input[type='checkbox']", laneCheckboxes[j].shadowRoot);
if (laneCheckboxes[j].checked)
laneCheckboxes[j].click();
}
}
}
if (turnSection.getElementsByClassName("angle-0").length > 0) {
// Set all lanes for straight turns
for (let j = 0; j < laneCheckboxes.length; j++) {
waitForElementLoaded("input[type='checkbox']", laneCheckboxes[j].shadowRoot);
if (laneCheckboxes[j].checked === false) {
if (j === 0 && (LtSettings.ClickSaveStraight || setLeft === false)) {
laneCheckboxes[j].click();
}
else if (j === laneCheckboxes.length - 1 &&
(LtSettings.ClickSaveStraight || setRight === false)) {
laneCheckboxes[j].click();
}
else if (j !== 0 && j !== laneCheckboxes.length - 1) {
laneCheckboxes[j].click();
}
}
}
}
}
}
}
function waitForElementLoaded(selector, root = undefined) {
return new Promise((resolve) => {
if (!root) {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver((mutations) => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
else {
if (root.querySelector(selector)) {
return resolve(root.querySelector(selector));
}
const observer = new MutationObserver((mutations) => {
if (root.querySelector(selector)) {
observer.disconnect();
resolve(root.querySelector(selector));
}
});
observer.observe(root, {
childList: true,
subtree: true,
});
}
});
}
function processLaneNumberChange() {
const parent = $(this).parents().eq(8);
const elem = parent[0];
const className = elem.className;
const numLanes = Number.parseInt($(this).val(), 10);
waitForElementLoaded(".turn-lane-checkbox").then((elem) => {
setTurns(className, numLanes);
});
const laneCountNums = $(this).parents().find(".lt-add-lanes");
if (laneCountNums.length > 0) {
const counterClassName = laneCountNums[0].className;
const selectorClassName = `.${counterClassName.replace(" ", ".")}`;
const counterClassToSelectName = `#${counterClassName.replace(" ", "-")}-${numLanes.toString()}`;
$(selectorClassName).css({ "background-color": "transparent", color: "black" });
$(counterClassToSelectName).css({ "background-color": "navy", color: "white" });
}
}
function initLaneGuidanceClickSaver() {
const laneObserver = new MutationObserver((mutations) => {
// if (
// W.selectionManager.getSelectedWMEFeatures()[0] &&
// W.selectionManager.getSelectedWMEFeatures()[0].featureType === "segment" &&
// getId("lt-ScriptEnabled").checked
// )
const selection = sdk.Editing.getSelection();
if (selection?.objectType === "segment" && getId("lt-ScriptEnabled")?.checked) {
const laneCountElement = document.getElementsByName("laneCount");
for (let idx = 0; idx < laneCountElement.length; idx++) {
laneCountElement[idx].addEventListener("keyup", processLaneNumberChange, false);
laneCountElement[idx].addEventListener("change", processLaneNumberChange, false);
}
}
});
laneObserver.observe(document.getElementById("edit-panel"), {
childList: true,
subtree: true,
});
// console.log('LaneTools: Click Saver Module loaded');
}
function isHeuristicsCandidate(segCandidate, curNodeExit, nodeExitSegIds, curNodeEntry, laneCount, segLength, inSegRef) {
// CRITERIA FOR HEURISTICS, as described on the wiki: https://wazeopedia.waze.com/wiki/USA/User:Nzahn1/Lanes#Mapping_lanes_on_divided_roadways
// 1. Both left and right turns are possible at the intersection;
// 2. The two portions of the divided roadway are essentially parallel to each other;
// 3. The two intersecting roads are more or less perpendicular to each other;
// 4. The median segment in question is 50 m or shorter; and
// 5. The number of lanes entering the intersection is equal to the total number of lanes exiting the intersection
// (total number of lanes exiting intersection = number of lanes on the median segment +
// number of right-turn only lanes on the entry segment -- other words, no new lanes are added in the median).
// MY OBSERVATIONS
// 11. We must have an incoming segment supplemenatary to outgoing segment 1. (alt-incoming)
// 12. That alt-incoming segment must be within perpendicular tolerance to BOTH the median segment and the incoming segment.
if (nodeExitSegIds == null || curNodeEntry == null || laneCount == null || inSegRef == null) {
lt_log("isHeuristicsCandidate received bad argument (null)", 1);
return HeuristicsCandidate.NONE;
}
let outSeg2 = null;
let outTurnAngle2 = null;
let outSeg2IsHeurFail = 0;
let inSeg = null;
let inAzm = null;
let inTurnAngle = null;
let inSegIsHeurFail = 0;
let altIncomingSeg = null;
let altInAzm = null;
let altInIsHeurFail = 0;
let inNumLanesThrough = 0;
// #4 first: Check the length (and get outta' here if not)
// 1/1/21: This is now redundant with outer loop. But leaving it in just in case...
if (segLength > MAX_LEN_HEUR) {
return HeuristicsCandidate.NONE;
}
// Get current segment heading at the node
const segId = segCandidate.id;
const segEndAzm = lt_getMathAzimuth_to_node(curNodeExit.id, segCandidate);
const segBeginAzm = lt_getMathAzimuth_from_node(curNodeEntry.id, segCandidate);
let out1TargetAngle = -90.0; // For right-hand side of the road countries (right-turn)
let out2TargetAngle = 90.0; // (left-turn)
if (segCandidate.primaryStreetId === null) {
lt_log(`Unable to process Heuristics on Segment: ${segCandidate.id} as it has no Primary Street Set`, 1);
return HeuristicsCandidate.NONE;
}
const street = sdk.DataModel.Streets.getById({ streetId: segCandidate.primaryStreetId });
if (!street) {
lt_log(`Unable to Process Heuristics on Street: ${segCandidate.primaryStreetId} as street with this id doesn't exist`, 1);
return HeuristicsCandidate.NONE;
}
const city = street?.cityId ? sdk.DataModel.Cities.getById({ cityId: street.cityId }) : null;
if (!city) {
lt_log(`Unable to Process Heuristics on City ${street?.cityId} as it doesn't exist`, 1);
return HeuristicsCandidate.NONE;
}
const segmentCountry = city.countryId ? sdk.DataModel.Countries.getById({ countryId: city?.countryId }) : null;
if (!segmentCountry) {
lt_log(`Unable to Process Heuristics on Country ${city.countryId}`, 1);
}
if (segmentCountry?.isLeftHandTraffic) {
out1TargetAngle = 90.0; // left turn
out2TargetAngle = -90.0; // right turn
}
lt_log("==================================================================================", 2);
lt_log(`Checking heuristics candidate: seg ${segId} node ${curNodeExit.id} azm ${segEndAzm} nodeExitSegIds:${nodeExitSegIds.length}`, 2);
// Find the incoming segment, and validate angle to cursegment
const nodeEntrySegIds = curNodeEntry.connectedSegmentIds;
for (let ii = 0; ii < nodeEntrySegIds.length; ii++) {
let thisTimeFail = 0;
if (nodeEntrySegIds[ii] === segId) {
continue;
} // ignore same segment as our original
const is = getSegObj(nodeEntrySegIds[ii]);
// Check turn from this seg to candidate seg
if (is !== null && !lt_is_turn_allowed(is, curNodeEntry, segCandidate)) {
continue;
}
const ia = lt_getMathAzimuth_to_node(curNodeEntry.id, is); // absolute math azimuth
const ita = lt_turn_angle(ia, segBeginAzm); // turn angle
lt_log(`Turn angle from inseg ${nodeEntrySegIds[ii]}: ${ita}(${ia},${segBeginAzm})`, 3);
if (ita !== null && Math.abs(ita) > MAX_STRAIGHT_DIF) {
// tolerance met?
if (Math.abs(ita) > MAX_STRAIGHT_TO_CONSIDER) {
continue;
}
lt_log(` Not eligible as inseg: ${ita}`, 2);
thisTimeFail = HeuristicsCandidate.FAIL;
}
// const turnsThrough = turnGraph.getTurnThroughNode(curNodeEntry, is, segCandidate);
// const turnData = turnsThrough.getTurnData();
// const turnsThrough = sdk.DataModel.Turns.getTurnsThroughNode({ nodeId: curNodeEntry.id });
// let turnData = turnsThrough[tidx];
function getMatchingTurn(node, from, to) {
const turns = sdk.DataModel.Turns.getTurnsThroughNode({ nodeId: node.id });
if (from !== null) {
for (let idx = 0; idx < turns.length; ++idx) {
if (turns[idx].fromSegmentId === from.id && turns[idx].toSegmentId === to.id)
return turns[idx];
}
}
return null;
}
const turnData = getMatchingTurn(curNodeEntry, is, segCandidate);
if (turnData === null || !turnData.lanes) {
lt_log(`Straight turn has no lanes:${nodeEntrySegIds[ii]} to ${segId}`, 3);
continue; // No lanes? Don't even think about it. (Not a candidate)
}
// #5 Ensure this (straight) turn motion has lanes, and lane count matches; otherwise ERROR
// 1/1/21: One exception. If laneCount is 0, and there is exactly 1 straight incoming lane, then treat it as equal. (Conversation with @jm6087)
const nl = turnData.lanes.toLaneIndex - turnData.lanes.fromLaneIndex + 1;
if (nl !== laneCount && !(laneCount === 0 && nl === 1)) {
lt_log("Straight turn lane count does not match", 2);
thisTimeFail = HeuristicsCandidate.ERROR; // Failed lane match should give us an ERROR
}
// Only one segment allowed // TBD ??? For now, don't allow more than one.
if (inSeg !== null && thisTimeFail >= inSegIsHeurFail) {
if (inSegIsHeurFail === 0 && thisTimeFail === 0) {
lt_log(`Error: >1 qualifying entry segment for ${segCandidate.id}: ${inSeg.id},${is?.id}`, 2);
lt_log("==================================================================================", 2);
return 0; // just stop here
}
}
inSeg = is;
inAzm = ia;
inTurnAngle = ita;
inNumLanesThrough = nl;
inSegIsHeurFail = thisTimeFail;
if (!inSegRef) {
const newSegRef = {
seg: 0,
direction: Direction.ANY,
};
inSegRef = newSegRef;
}
if (inSeg)
inSegRef.seg = inSeg.id;
inSegRef.direction = inSeg?.toNodeId === curNodeEntry.id ? Direction.FORWARD : Direction.REVERSE;
}
if (inSeg === null) {
lt_log("== No inseg found ==================================================================", 2);
return 0; // otherwise wait for later
}
lt_log(`Found inseg candidate: ${inSeg.id} ${inSegIsHeurFail === 0 ? "" : "(failed)"}`, 2);
// #3(a) Determine the outgoing segment 2 (the 2nd turn) and validate turn angle
for (let ii = 0; ii < nodeExitSegIds.length; ii++) {
let thisTimeFail = 0;
if (nodeExitSegIds[ii] === segId) {
continue;
} // ignore same segment as our original
const os = getSegObj(nodeExitSegIds[ii]);
// Check turn from candidate seg to this seg
if (!lt_is_turn_allowed(segCandidate, curNodeExit, os)) {
continue;
}
const oa = lt_getMathAzimuth_from_node(curNodeExit.id, os); // absolute math azimuth
const ota = lt_turn_angle(segEndAzm, oa); // turn angle
lt_log(`Turn angle to outseg2 ${nodeExitSegIds[ii]}: ${ota}(${segEndAzm},${oa})`, 2);
// Just to be sure, we can't do Heuristics if there's a chance to turn right (RH)
if (ota !== null && Math.abs(out1TargetAngle - ota) < MAX_PERP_TO_CONSIDER) {
// tolerance met?
return HeuristicsCandidate.NONE;
}
// Ok now check our turn angle
if (ota !== null && Math.abs(out2TargetAngle - ota) > MAX_PERP_DIF) {
// tolerance met?
if (Math.abs(out2TargetAngle - ota) > MAX_PERP_TO_CONSIDER) {
continue;
} // too far out of tolerance to care (don't consider it a candidate at all)
lt_log(` Not eligible as outseg2: ${ota}`, 2);
thisTimeFail = HeuristicsCandidate.FAIL;
}
// Only one segment allowed // TBD ??? For now, don't allow more than one.
if (outSeg2 !== null && thisTimeFail >= outSeg2IsHeurFail) {
if (outSeg2IsHeurFail === 0 && thisTimeFail === 0) {
lt_log(`Error: >1 qualifying exit2 segment for ${segCandidate.id}: ${outSeg2.id},${os?.id}`, 2);
lt_log("==================================================================================", 2);
return 0; // just stop here
}
}
outSeg2 = os;
outTurnAngle2 = ota;
outSeg2IsHeurFail = thisTimeFail;
}
if (outSeg2 == null) {
lt_log("== No Outseg2 found ==================================================================", 2);
return 0;
}
lt_log(`Found outseg2 candidate: ${outSeg2.id} ${outSeg2IsHeurFail === 0 ? "" : "(failed)"}`, 2);
// #11 & 12: The Segment 1 that matters is the incoming (parallel to outgoing seg2)
for (let ii = 0; ii < nodeEntrySegIds.length; ii++) {
if (nodeEntrySegIds[ii] === segId || nodeEntrySegIds[ii] === inSeg.id) {
// ignore same segment as our original
continue;
}
const ai1 = getSegObj(nodeEntrySegIds[ii]);
let thisTimeFail = 0;
// Ensure the segment is one-way TOWARD the node (incoming direction)
if ((ai1?.isAtoB && ai1.toNodeId !== curNodeEntry.id) ||
(ai1?.isBtoA && ai1.fromNodeId !== curNodeEntry.id)) {
continue;
}
// Check turn from this seg to our segment
const ia = lt_getMathAzimuth_to_node(curNodeEntry.id, ai1); // absolute math azimuth
// 12. Check angle from inseg to this seg (se)
// Since we already have azm of this seg TOWARD the node, just check the supplementary turn angle. Must also be within tolerance. (See Geometry proof :)
const tta = lt_turn_angle(inAzm, ia);
lt_log(`Turn angle from inseg (supplementary) ${nodeEntrySegIds[ii]}: ${tta}(${inAzm},${ia})`, 3);
if (tta !== null && Math.abs(out1TargetAngle - tta) > MAX_PERP_DIF_ALT) {
// tolerance met?
if (Math.abs(out1TargetAngle - tta) > MAX_PERP_TO_CONSIDER) {
// too far out of tolerance to care (don't consider it a candidate at all)
continue;
}
lt_log(` Not eligible as altIn1: ${tta}`, 3);
thisTimeFail = HeuristicsCandidate.FAIL;
}
// Only one segment allowed // TBD ??? For now, don't allow more than one.
if (altIncomingSeg !== null) {
// If the new candidate is worse than what we already have, just move on
if (thisTimeFail < altInIsHeurFail) {
continue;
}
// If they both are good, then error
if (altInIsHeurFail === 0 && thisTimeFail === 0) {
lt_log(`Error: >1 qualifying segment for ${segCandidate.id}: ${altIncomingSeg.id},${ai1?.id}`, 2);
lt_log("==================================================================================", 2);
return HeuristicsCandidate.FAIL;
}
} // If the new candidate is better than the old, then assign our candidate to the new one (below)
altIncomingSeg = ai1;
altInAzm = ia;
altInIsHeurFail = thisTimeFail;
}
if (altIncomingSeg == null) {
lt_log("== No alt incoming-1 segment found ==================================================================", 2);
return 0;
}
lt_log(`Alt incoming-1 segment found: ${altIncomingSeg.id} ${altInIsHeurFail === 0 ? "" : "(failed)"}`, 2);
// Have we found a failure candidate?
if (inSegIsHeurFail < 0 || altInIsHeurFail < 0 || outSeg2IsHeurFail < 0) {
lt_log(`Found a failed candidate for ${segId} ( ${Math.min(inSegIsHeurFail, altInIsHeurFail, outSeg2IsHeurFail)})`, 2);
// NOTE: IF any seg is a FAIL, then return FAIL (not Error)
if (inSegIsHeurFail === HeuristicsCandidate.FAIL ||
altInIsHeurFail === HeuristicsCandidate.FAIL ||
outSeg2IsHeurFail === HeuristicsCandidate.FAIL) {
return HeuristicsCandidate.FAIL;
}
return HeuristicsCandidate.ERROR;
}
// We have a winner!!!
lt_log(`Found a heuristics candidate! ${segId} to ${outSeg2.id} at ${outTurnAngle2}`, 2);
return 1;
////////////////////////////////////////////// end of func /////////////////////////////////////////////////////////
// get the absolute angle for a segment at an end point - borrowed from JAI
function lt_getMathAzimuth_from_node(nodeId, segment) {
if (nodeId === null || segment === null) {
return null;
}
let ja_dx;
let ja_dy;
if (segment.fromNodeId === nodeId) {
const secondPoint = lt_get_second_point(segment);
const firstPoint = lt_get_first_point(segment);
if (!secondPoint || !firstPoint) {
throw new Error("Missing Start and end Point of the Segment");
}
ja_dx = secondPoint[0] - firstPoint[0];
ja_dy = secondPoint[1] - firstPoint[1];
}
else {
const nextToLastPoint = lt_get_next_to_last_point(segment);
const lastPoint = lt_get_last_point(segment);
if (!nextToLastPoint || !lastPoint) {
throw new Error("Missing Points at the End of the Segment");
}
ja_dx = nextToLastPoint[0] - lastPoint[0];
ja_dy = nextToLastPoint[1] - lastPoint[1];
}
const angle_rad = Math.atan2(ja_dy, ja_dx);
const angle_deg = ((angle_rad * 180) / Math.PI) % 360;
lt_log(`Azm from node ${nodeId} / ${segment.id}: ${angle_deg}`, 3);
return angle_deg;
}
function lt_getMathAzimuth_to_node(nodeId, segment) {
if (!nodeId || !segment)
return null;
const fromAzm = lt_getMathAzimuth_from_node(nodeId, segment);
if (fromAzm === null)
return null;
let toAzm = fromAzm + 180.0;
if (toAzm >= 180.0) {
toAzm -= 360.0;
}
lt_log(`Azm to node ${nodeId} / ${segment.id}: ${toAzm}`, 3);
return toAzm;
}
/** Get absolute angle between 2 inputs.
* @param aIn absolute s_in angle (to node)
* @param aOut absolute s_out angle (from node)
* @returns {number}
*/
function lt_turn_angle(aIn, aOut) {
if (aIn === null || aOut === null)
return null;
let angleInAdjusted = aIn;
let angleOutAdjusted = aOut;
while (aOut > 180.0) {
angleOutAdjusted -= 360.0;
}
while (aOut < -180.0) {
angleOutAdjusted += 360.0;
}
while (aIn > 180.0) {
angleInAdjusted -= 360.0;
}
while (aIn < -180.0) {
angleInAdjusted += 360.0;
}
let a = angleOutAdjusted - angleInAdjusted;
a += a > 180 ? -360 : a < -180 ? 360 : 0;
lt_log(`Turn ${angleInAdjusted},${angleOutAdjusted}: ${a}`, 3);
return a;
}
function lt_is_turn_allowed(s_from, via_node, s_to) {
const turnsThrough = sdk.DataModel.Turns.getTurnsThroughNode({ nodeId: via_node.id });
function isTurnAllowedBySegDirections(from, to) {
const result = {
allowedBySegDirections: false,
allowed: false,
};
if (from !== null && to !== null) {
for (let tidx = 0; tidx < turnsThrough.length; ++tidx) {
if (turnsThrough[tidx].fromSegmentId === from.id && turnsThrough[tidx].toSegmentId === to.id) {
result.allowed = turnsThrough[tidx].isAllowed;
result.allowedBySegDirections = true;
break;
}
}
}
return result;
}
const permissions = isTurnAllowedBySegDirections(s_from, s_to);
lt_log(`Allow from ${s_from.id} to ${s_to !== null ? s_to.id : 0} via ${via_node.id}? ${permissions.allowedBySegDirections} | ${permissions.allowed}`, 3);
// Is there a driving direction restriction?
if (!permissions.allowedBySegDirections) {
lt_log("Driving direction restriction applies", 3);
return false;
}
// Is turn allowed by other means (e.g. turn restrictions)?
if (!permissions.allowed) {
lt_log("Other restriction applies", 3);
return false;
}
// TBD: Do we need to consider restrictions?
/*if(s_to.attributes.fromNodeID === via_node.attributes.id) {
lt_log("FWD direction",3);
return ja_is_car_allowed_by_restrictions(s_to.attributes.fwdRestrictions);
} else {
lt_log("REV direction",3);
return ja_is_car_allowed_by_restrictions(s_to.attributes.revRestrictions);
} */
return true;
}
}
// Segment Length - borrowed from JAI
function lt_segment_length(segment) {
// let len = segment.geometry.getGeodesicLength(W.map.olMap.projection);
// let len = olSphere.getLength(segment.geometry);
const len = 0;
// let len = segment.geometry.getGeodesicLength(W.map.olMap.projection);
lt_log(`segment: ${segment.id} computed len: ${len} `, 3);
return len;
}
function lt_log(lt_log_msg, lt_log_level = 1) {
// ##NO_FF_START##
// Firefox addons should not use console.(log|error|debug), so these lines
// are removed by the FF addon packaging script.
if (lt_log_level <= LANETOOLS_DEBUG_LEVEL) {
console.log("LaneTools Dev Msg: ", lt_log_msg);
}
// ##NO_FF_END##
}
function copyLaneInfo(side) {
_turnInfo = [];
const selFeatures = sdk.Editing.getSelection();
const seg = selFeatures[0]._wmeObject;
const segAtt = seg.getFeatureAttributes();
const segGeo = seg.geometry.components;
const nodeID = side === "A" ? segAtt.fromNodeID : segAtt.toNodeID;
laneCount = side === "A" ? segAtt.revLaneCount : segAtt.fwdLaneCount;
console.log(laneCount);
const node = getNodeObj(nodeID);
const conSegs = node.getSegmentIds();
// const turnGraph = W.model.getTurnGraph();
let geoPoint1;
if (side === "A") {
geoPoint1 = segGeo[1];
}
else {
geoPoint1 = segGeo[segGeo.length - 2];
}
let ja_dx = geoPoint1.x - node.geometry.x;
let ja_dy = geoPoint1.y - node.geometry.y;
let angleRad = Math.atan2(ja_dy, ja_dx);
const angleDeg = ((angleRad * 180) / Math.PI) % 360;
for (let i = 0; i < conSegs.length; i++) {
const seg2 = getSegObj(conSegs[i]);
const seg2Att = seg2?.getFeatureAttributes();
const seg2Geo = seg2?.geometry.components;
let geoPoint2;
let seg2Dir;
const turnInfo = turnGraph.getTurnThroughNode(node, seg, seg2).getTurnData();
if (turnInfo.state === 1 && turnInfo.lanes) {
if (seg2Att.fromNodeID === nodeID) {
seg2Dir = "A";
}
else {
seg2Dir = "B";
}
if (seg2Dir === "A") {
geoPoint2 = seg2Geo[1];
}
else {
geoPoint2 = seg2Geo[seg2Geo.length - 2];
}
ja_dx = geoPoint2.x - node.geometry.x;
ja_dy = geoPoint2.y - node.geometry.y;
angleRad = Math.atan2(ja_dy, ja_dx);
let tempAngle = ((angleRad * 180) / Math.PI) % 360;
if (angleDeg < 0)
tempAngle = angleDeg - tempAngle;
_turnData = {};
let laneData = turnInfo.getLaneData();
_turnData.id = seg2.attributes.id;
_turnData.order = tempAngle;
_turnData.lanes = laneData;
_turnInfo.push(_turnData);
}
_turnInfo.sort((a, b) => (a.order > b.order ? 1 : -1));
}
console.log(_turnInfo);
}
function pasteLaneInfo(side) {
const mAction = new MultiAction();
// mAction.setModel(W.model);
const selFeatures = W.selectionManager.getSelectedWMEFeatures();
const seg = selFeatures[0]._wmeObject;
const segGeo = seg.geometry.components;
const segAtt = seg.getFeatureAttributes();
const nodeID = side === "A" ? segAtt.fromNodeID : segAtt.toNodeID;
// let sortA = _cpyDir == side ? 1 : -1;
// let sortB = _cpyDir == side ? -1 : 1;
let geoPoint1;
const node = getNodeObj(nodeID);
const conSegs = node.getSegmentIds();
// const turnGraph = W.model.getTurnGraph();
let pasteData = {};
const pasteInfo = [];
if (side === "A") {
geoPoint1 = segGeo[1];
}
else {
geoPoint1 = segGeo[segGeo.length - 2];
}
let ja_dx = geoPoint1.x - node.geometry.x;
let ja_dy = geoPoint1.y - node.geometry.y;
let angleRad = Math.atan2(ja_dy, ja_dx);
const angleDeg = ((angleRad * 180) / Math.PI) % 360;
for (let i = 0; i < conSegs.length; i++) {
const seg2 = getSegObj(conSegs[i]);
const seg2Att = seg2.attributes;
const seg2Geo = seg2.geometry.components;
let geoPoint2 = {};
let seg2Dir;
const turnInfo = turnGraph.getTurnThroughNode(node, seg, seg2).getTurnData();
if (seg2Att.fromNodeID === nodeID) {
seg2Dir = "A";
}
else {
seg2Dir = "B";
}
if (seg2Dir === "A") {
geoPoint2 = seg2Geo[1];
}
else {
geoPoint2 = seg2Geo[seg2Geo.length - 2];
}
if (turnInfo.state === 1) {
pasteData = {};
ja_dx = geoPoint2.x - node.geometry.x;
ja_dy = geoPoint2.y - node.geometry.y;
angleRad = Math.atan2(ja_dy, ja_dx);
let tempAngle = ((angleRad * 180) / Math.PI) % 360;
if (angleDeg < 0)
tempAngle = angleDeg - tempAngle;
pasteData.id = seg2Att.id;
pasteData.order = tempAngle;
pasteInfo.push(pasteData);
}
pasteInfo.sort((a, b) => (a.order > b.order ? 1 : -1));
}
console.log(pasteInfo);
if (_turnInfo.length === pasteInfo.length) {
if (side === "A") {
mAction.doSubAction(W.model, new UpdateObj(seg, { revLaneCount: laneCount }));
}
else {
mAction.doSubAction(W.model, new UpdateObj(seg, { fwdLaneCount: laneCount }));
}
for (let k = 0; k < pasteInfo.length; k++) {
const pasteTurn = {};
// Copy turn data into temp object
for (let q = 0; q < _turnInfo.length; q++) {
pasteTurn[q] = _turnInfo[q];
}
// If pasting in the opposite direction, reverse the lane associations
/* if (_cpyDir != side) {
for (let z=0;z < pasteTurn.length; z++) {
pasteTurn[z].lanes.arrowAngle = pasteTurn[z].lanes.arrowAngle * -1;
}
} */
const toSeg = getSegObj(pasteInfo[k].id);
let turnStatus = turnGraph.getTurnThroughNode(node, seg, toSeg);
let turnData = turnStatus.getTurnData();
turnData = turnData.withLanes(pasteTurn[k].lanes);
turnStatus = turnStatus.withTurnData(turnData);
mAction.doSubAction(W.model, new SetTurn(turnGraph, turnStatus));
}
mAction._description = "Pasted some lane stuff";
W.model.actionManager.add(mAction);
lanesTabSetup.formatLanesTab(true);
}
else {
WazeWrap.Alerts.warning(GM_info.script.name, "There are a different number of enabled turns on this segment/node");
}
}
function getIcons(dir) {
const tempEle = [];
let svgcount = 0;
for (let i = 0; i < dir.length; i++) {
//if (dir[i].id !== "") {
const temp = {
uturn: false,
miniuturn: false,
svg: [],
};
const uTurnDisplay = $(dir[i])
.find(".uturn")
.css("display");
const miniUturnDisplay = $(dir[i])
.find(".small-uturn")
.css("display");
temp.uturn = uTurnDisplay && uTurnDisplay !== "none";
temp.miniuturn = miniUturnDisplay && miniUturnDisplay !== "none";
temp.svg = $(dir[i])
.find("svg")
.map(function () {
return this;
})
.get();
if (temp.svg.length > 0) {
svgcount++;
}
tempEle[i] = temp;
}
return svgcount > 0 ? tempEle : false;
}
function convertToBase64(svgs) {
const serial = new XMLSerializer();
_.each(svgs, (obj) => {
try {
const svg = obj.svg[0];
const tmp = serial.serializeToString(svg);
obj.svg = `data:image/svg+xml;base64,${window.btoa(tmp)}`;
}
catch (e) {
// console.log(e);
}
});
return svgs;
}
function getStartPoints(node, featDis, numIcons, sign) {
const start = !featDis || !featDis.start ? 0 : featDis.start;
const boxheight = !featDis || !featDis.boxheight ? 0 : featDis.boxheight;
const boxincwidth = !featDis || !featDis.boxincwidth ? 0 : featDis.boxincwidth;
const nodePos = proj4("EPSG:4326", "EPSG:3857", node.geometry.coordinates);
switch (sign) {
case 0:
return proj4("EPSG:3857", "EPSG:4326", [nodePos[0] + start * 2, nodePos[1] + boxheight]);
// x: node.geometry.x + (featDis.start * 2),
// y: node.geometry.y + (featDis.boxheight)
case 1:
return proj4("EPSG:3857", "EPSG:4326", [nodePos[0] + boxheight, nodePos[1] + boxincwidth * numIcons]);
// x: node.geometry.x + featDis.boxheight,
// y: node.geometry.y + (featDis.boxincwidth * numIcons/1.8)
case 2:
return proj4("EPSG:3857", "EPSG:4326", [
nodePos[0] - (start + boxincwidth + numIcons),
nodePos[1] + boxheight,
]);
// x: node.geometry.x - (featDis.start + (featDis.boxincwidth * numIcons)),
// y: node.geometry.y + (featDis.start + featDis.boxheight)
case 3:
return proj4("EPSG:3857", "EPSG:4326", [
nodePos[0] + start + boxincwidth,
nodePos[1] - (start + boxheight),
]);
// x: node.geometry.x + (featDis.start + featDis.boxincwidth),
// y: node.geometry.y - (featDis.start + featDis.boxheight)
case 4:
return proj4("EPSG:3857", "EPSG:4326", [
nodePos[0] - (start + boxheight * 3),
nodePos[1] + (boxincwidth + numIcons * 0.5),
]);
// x: node.geometry.x - (featDis.start + (featDis.boxheight * 1.5)),
// y: node.geometry.y - (featDis.start + (featDis.boxincwidth * numIcons * 1.5))
case 5:
return proj4("EPSG:3857", "EPSG:4326", [nodePos[0] + (start + boxincwidth), nodePos[1] + start]);
// x: node.geometry.x + (featDis.start + featDis.boxincwidth/2),
// y: node.geometry.y + (featDis.start/2)
case 6:
return proj4("EPSG:3857", "EPSG:4326", [
nodePos[0] - start,
nodePos[1] - start * ((boxincwidth * numIcons) / 2),
]);
// x: node.geometry.x - (featDis.start),
// y: node.geometry.y - (featDis.start * (featDis.boxincwidth * numIcons/2))
case 7:
return proj4("EPSG:3857", "EPSG:4326", [
nodePos[0] - start * boxincwidth * numIcons,
nodePos[1] + start,
]);
// x: node.geometry.x - (featDis.start * (featDis.boxincwidth * numIcons/2)),
// y: node.geometry.y - (featDis.start)
default:
break;
}
return [];
}
function getFeatDistance() {
const label_distance = {
start: undefined,
boxheight: undefined,
boxincwidth: undefined,
iconbordermargin: undefined,
iconborderheight: undefined,
iconborderwidth: undefined,
graphicHeight: undefined,
graphicWidth: undefined,
};
switch (sdk.Map.getZoomLevel()) {
case 22:
label_distance.start = 2;
label_distance.boxheight = 1.7;
label_distance.boxincwidth = 1.1;
label_distance.iconbordermargin = 0.1;
label_distance.iconborderheight = 1.6;
label_distance.iconborderwidth = 1;
label_distance.graphicHeight = 42;
label_distance.graphicWidth = 25;
break;
case 21:
label_distance.start = 2;
label_distance.boxheight = 3.2;
label_distance.boxincwidth = 2.2;
label_distance.iconbordermargin = 0.2;
label_distance.iconborderheight = 3;
label_distance.iconborderwidth = 2;
label_distance.graphicHeight = 42;
label_distance.graphicWidth = 25;
break;
case 20:
label_distance.start = 2;
label_distance.boxheight = 5.2;
label_distance.boxincwidth = 3.8;
label_distance.iconbordermargin = 0.3;
label_distance.iconborderheight = 4.9;
label_distance.iconborderwidth = 3.5;
label_distance.graphicHeight = 42;
label_distance.graphicWidth = 25;
break;
case 19:
label_distance.start = 3;
label_distance.boxheight = 10.0;
label_distance.boxincwidth = 7.2;
label_distance.iconbordermargin = 0.4;
label_distance.iconborderheight = 9.6;
label_distance.iconborderwidth = 6.8;
label_distance.graphicHeight = 42;
label_distance.graphicWidth = 25;
break;
case 18:
label_distance.start = 3;
label_distance.boxheight = 20.0;
label_distance.boxincwidth = 14.0;
label_distance.iconbordermargin = 0.5;
label_distance.iconborderheight = 19.5;
label_distance.iconborderwidth = 13.5;
label_distance.graphicHeight = 42;
label_distance.graphicWidth = 25;
break;
case 17:
label_distance.start = 10;
label_distance.boxheight = 39.0;
label_distance.boxincwidth = 28.0;
label_distance.iconbordermargin = 1.0;
label_distance.iconborderheight = 38.0;
label_distance.iconborderwidth = 27.0;
label_distance.graphicHeight = 42;
label_distance.graphicWidth = 25;
break;
case 16:
label_distance.start = 15;
label_distance.boxheight = 80.0;
label_distance.boxincwidth = 55;
label_distance.iconbordermargin = 2.0;
label_distance.iconborderheight = 78.0;
label_distance.iconborderwidth = 53;
label_distance.graphicHeight = 42;
label_distance.graphicWidth = 25;
break;
case 15:
label_distance.start = 2;
label_distance.boxheight = 120.0;
label_distance.boxincwidth = 90;
label_distance.iconbordermargin = 3.0;
label_distance.iconborderheight = 117.0;
label_distance.iconborderwidth = 87;
label_distance.graphicHeight = 42;
label_distance.graphicWidth = 25;
break;
case 14:
label_distance.start = 2;
label_distance.boxheight = 5.2;
label_distance.boxincwidth = 3.8;
label_distance.iconbordermargin = 0.3;
label_distance.iconborderheight = 4.9;
label_distance.iconborderwidth = 3.5;
label_distance.graphicHeight = 42;
label_distance.graphicWidth = 25;
break;
// case 13:
// label_distance.start = 2;
// label_distance.boxheight = 5.2;
// label_distance.boxincwidth = 3.8;
// label_distance.iconbordermargin = .3;
// label_distance.iconborderheight = 4.9;
// label_distance.iconborderwidth = 3.5;
// label_distance.graphicHeight = 42;
// label_distance.graphicWidth = 25;
// break;
}
return label_distance;
}
function drawIcons(seg, node, imgs) {
if (!seg || !node)
return;
const featDis = getFeatDistance();
let deg = getCardinalAngle(node.id, seg);
if (!deg)
return;
const points = [];
let operatorSign = 0;
const numIcons = imgs.length;
// Orient all icons straight up if the rotate option isn't enabled
if (!getId("lt-IconsRotate")?.checked)
deg = -90;
// Rotate in the style is clockwise, the rotate() func is counterclockwise
if (deg === 0) {
deg += 180;
operatorSign = 1;
}
else if (deg > 0 && deg <= 30) {
deg += 2 * (90 - deg);
// console.log('Math stuff2: ' + deg);
operatorSign = 1;
}
else if (deg >= 330 && deg <= 360) {
deg -= 180 - 2 * (360 - deg);
// console.log('Math stuff2: ' + deg);
operatorSign = 1;
}
else if (deg > 30 && deg < 60) {
deg -= 90 - 2 * (360 - deg);
// console.log('Math stuff3: ' + deg);
operatorSign = 2;
}
else if (deg >= 60 && deg <= 120) {
deg -= 90 - 2 * (360 - deg);
// console.log('Math stuff4: ' + deg);
operatorSign = 2;
}
else if (deg > 120 && deg < 150) {
deg -= 90 - 2 * (360 - deg);
// console.log('Math stuff5: ' + deg);
operatorSign = 7;
}
else if (deg >= 150 && deg <= 210) {
deg = 180 - deg;
// console.log('Math stuff6: ' + deg);
operatorSign = 4;
}
else if (deg > 210 && deg < 240) {
deg -= 90 - 2 * (360 - deg);
// console.log('Math stuff7: ' + deg);
operatorSign = 6;
}
else if (deg >= 240 && deg <= 300) {
deg -= 180 - 2 * (360 - deg);
// console.log('Math stuff8: ' + deg);
operatorSign = 3;
}
else if (deg > 300 && deg < 330) {
deg -= 180 - 2 * (360 - deg);
// console.log('Math stuff9: ' + deg);
operatorSign = 5;
}
else {
console.log("LT: icon angle is out of bounds");
}
const iconRotate = deg > 315 ? deg : deg + 90;
const boxRotate = 360 - iconRotate;
// console.log(deg);
// console.log(operatorSign);
// Determine start point respective to node based on segment angle
// let boxRotate = deg * -1;
const startPoint = getStartPoints(node, featDis, numIcons, operatorSign);
if (!startPoint[0] || !startPoint[1])
return;
// Box coords
// var boxPoint1 = new OpenLayers.Geometry.Point(startPoint.x, startPoint.y + featDis.boxheight);
// var boxPoint2 = new OpenLayers.Geometry.Point(
// startPoint.x + featDis.boxincwidth * numIcons,
// startPoint.y + featDis.boxheight
// );
// var boxPoint3 = new OpenLayers.Geometry.Point(startPoint.x + featDis.boxincwidth * numIcons, startPoint.y);
// var boxPoint4 = new OpenLayers.Geometry.Point(startPoint.x, startPoint.y);
let boxPoint1 = proj4("EPSG:4326", "EPSG:3857", startPoint);
boxPoint1[1] += !featDis || !featDis.boxheight ? 0 : featDis.boxheight;
boxPoint1 = proj4("EPSG:3857", "EPSG:4326", boxPoint1);
let boxPoint2 = proj4("EPSG:4326", "EPSG:3857", startPoint);
boxPoint2[0] += !featDis || !featDis.boxincwidth ? 0 : featDis.boxincwidth * numIcons;
boxPoint2[1] += !featDis || !featDis.boxheight ? 0 : featDis.boxheight;
boxPoint2 = proj4("EPSG:3857", "EPSG:4326", boxPoint2);
let boxPoint3 = proj4("EPSG:4326", "EPSG:3857", startPoint);
boxPoint3[0] += !featDis || !featDis.boxincwidth ? 0 : featDis.boxincwidth * numIcons;
boxPoint3 = proj4("EPSG:3857", "EPSG:4326", boxPoint3);
const boxPoint4 = startPoint;
points.push(boxPoint1, boxPoint2, boxPoint3, boxPoint4, boxPoint1);
// Object.assign(styleRules.boxStyle.style, {
// strokeColor: "#ffffff",
// strokeOpacity: 1,
// strokeWidth: 8,
// fillColor: "#ffffff",
// // rotate: boxRotate
// });
// let boxRing = new OpenLayers.Geometry.LinearRing(points);
// centerPoint = boxRing.getCentroid();
// boxRing.rotate(boxRotate, centerPoint);
// let boxVector = new OpenLayers.Feature.Vector(boxRing, null, boxStyle);
let turfBoxRing = turf.polygon([points]);
turfBoxRing = turf.transformRotate(turfBoxRing, -1 * boxRotate);
const centerPoint = turf.centroid(turfBoxRing);
const boxRing = turf.polygon(turfBoxRing.geometry.coordinates, { styleName: "boxStyle", layerName: LTLaneGraphics.name }, { id: `polygon_${points.toString()}` });
// LTLaneGraphics.addFeatures([boxVector]);
sdk.Map.addFeatureToLayer({ feature: boxRing, layerName: LTLaneGraphics.name });
let num = 0;
_.each(imgs, (img) => {
const iconPoints = [];
// Icon Background
// var iconPoint1 = new OpenLayers.Geometry.Point(
// startPoint.x + featDis.boxincwidth * num + featDis.iconbordermargin,
// startPoint.y + featDis.iconborderheight
// );
let iconPoint1 = proj4("EPSG:4326", "EPSG:3857", startPoint);
iconPoint1[0] += !featDis
? 0
: (!featDis.boxincwidth ? 0 : featDis.boxincwidth) * num +
(!featDis.iconbordermargin ? 0 : featDis.iconbordermargin);
iconPoint1[1] += !featDis || !featDis.iconborderheight ? 0 : featDis.iconborderheight;
iconPoint1 = proj4("EPSG:3857", "EPSG:4326", iconPoint1);
// var iconPoint2 = new OpenLayers.Geometry.Point(
// startPoint.x + featDis.boxincwidth * num + featDis.iconborderwidth,
// startPoint.y + featDis.iconborderheight
// );
let iconPoint2 = proj4("EPSG:4326", "EPSG:3857", startPoint);
iconPoint2[0] += !featDis
? 0
: (!featDis.boxincwidth ? 0 : featDis.boxincwidth) * num +
(!featDis.iconborderwidth ? 0 : featDis.iconborderwidth);
iconPoint2[1] += !featDis || !featDis.iconborderheight ? 0 : featDis.iconborderheight;
iconPoint2 = proj4("EPSG:3857", "EPSG:4326", iconPoint2);
// var iconPoint3 = new OpenLayers.Geometry.Point(
// startPoint.x + featDis.boxincwidth * num + featDis.iconborderwidth,
// startPoint.y + featDis.iconbordermargin
// );
let iconPoint3 = proj4("EPSG:4326", "EPSG:3857", startPoint);
iconPoint3[0] += !featDis
? 0
: (!featDis.boxincwidth ? 0 : featDis.boxincwidth) * num +
(!featDis.iconborderwidth ? 0 : featDis.iconborderwidth);
iconPoint3[1] += !featDis || !featDis.iconbordermargin ? 0 : featDis.iconbordermargin;
iconPoint3 = proj4("EPSG:3857", "EPSG:4326", iconPoint3);
// var iconPoint4 = new OpenLayers.Geometry.Point(
// startPoint.x + featDis.boxincwidth * num + featDis.iconbordermargin,
// startPoint.y + featDis.iconbordermargin
// );
let iconPoint4 = proj4("EPSG:4326", "EPSG:3857", startPoint);
iconPoint4[0] += !featDis
? 0
: (!featDis.boxincwidth ? 0 : featDis.boxincwidth) * num +
(!featDis.iconbordermargin ? 0 : featDis.iconbordermargin);
iconPoint4[1] += !featDis || !featDis.iconbordermargin ? 0 : featDis.iconbordermargin;
iconPoint4 = proj4("EPSG:3857", "EPSG:4326", iconPoint4);
iconPoints.push(iconPoint1, iconPoint2, iconPoint3, iconPoint4, iconPoint1);
// Object.assign(styleRules.iconBoxStyle.style, {
// strokeColor: "#000000",
// strokeOpacity: 1,
// strokeWidth: 1,
// fillColor: "#26bae8",
// // rotate: boxRotate
// });
// let iconBoxRing = new OpenLayers.Geometry.LinearRing(iconPoints);
let turfIconBoxRing = turf.polygon([iconPoints]);
turfIconBoxRing = turf.transformRotate(turfIconBoxRing, -1 * boxRotate, { pivot: centerPoint.geometry });
const iconBoxRing = turf.polygon(turfIconBoxRing.geometry.coordinates, { styleName: "iconBoxStyle", layerName: LTLaneGraphics.name }, { id: `polygon_${iconPoints.toString()}` });
// iconBoxRing.rotate(boxRotate, centerPoint);
// let iconBoxVector = new OpenLayers.Feature.Vector(iconBoxRing, null, iconBoxStyle);
// LTLaneGraphics.addFeatures([iconBoxVector]);
sdk.Map.addFeatureToLayer({ feature: iconBoxRing, layerName: LTLaneGraphics.name });
// Icon coords
const arrowOrigin = turf.centroid(turfIconBoxRing);
// let iconStart = new OpenLayers.Geometry.Point(arrowOrigin.x, arrowOrigin.y);
let ulabel = "";
const usize = {
x: undefined,
y: undefined,
};
const uoffset = {
x: undefined,
y: undefined,
};
if (img.uturn === true) {
ulabel = `https://web-assets.waze.com/webapps/wme/${sdk.getWMEVersion()}-${env}/font/989fe58ac11ed7d3/u-turn-small.svg`;
usize.x = 0.6;
usize.y = 0.6;
uoffset.x = -7;
uoffset.y = -12;
}
if (img.miniuturn === true) {
ulabel = `https://web-assets.waze.com/webapps/wme/${sdk.getWMEVersion()}-${env}/font/989fe58ac11ed7d3/u-turn-small.svg`;
usize.x = 0.3;
usize.y = 0.25;
uoffset.x = -8;
uoffset.y = 4;
}
const iconStart = turf.point(arrowOrigin.geometry.coordinates, {
styleName: "iconStyle",
layerName: LTLaneGraphics.name,
style: {
externalGraphic: img.svg,
graphicHeight: featDis.graphicHeight,
graphicWidth: featDis.graphicWidth,
fillColor: "#26bae8",
fillOpacity: 1,
backgroundColor: "#26bae8",
strokeColor: "#26bae8",
rotation: iconRotate,
backgroundGraphic: ulabel,
backgroundHeight: !featDis || !featDis.graphicHeight || !usize.y
? undefined
: featDis.graphicHeight * usize.y,
backgroundWidth: !featDis || !featDis.graphicWidth || !usize.x ? undefined : featDis.graphicWidth * usize.x,
backgroundXOffset: uoffset.x,
backgroundYOffset: uoffset.y,
},
}, { id: `point_${iconPoints.toString()}` });
sdk.Map.addFeatureToLayer({ layerName: LTLaneGraphics.name, feature: iconStart });
num++;
});
// LTLaneGraphics.setZIndex(2890);
}
function displayLaneGraphics() {
removeLaneGraphics();
const selection = sdk.Editing.getSelection();
if (!getId("lt-ScriptEnabled")?.checked ||
!getId("lt-IconsEnable")?.checked ||
selection == null ||
selection?.objectType !== "segment" ||
(selection.ids && selection.ids.length !== 1))
return;
const seg = sdk.DataModel.Segments.getById({ segmentId: selection.ids[0] });
if (!seg)
return;
const zoomLevel = sdk.Map.getZoomLevel();
if (zoomLevel < 15 ||
(seg.roadType !== (LT_ROAD_TYPE.FREEWAY || LT_ROAD_TYPE.MAJOR_HIGHWAY || LT_ROAD_TYPE.MINOR_HIGHWAY) &&
zoomLevel < 16))
return;
waitForElementLoaded(".lanes-tab > .lanes > div > .direction-lanes").then(() => {
const fwdEle = seg?.fromNodeLanesCount && seg.fromNodeLanesCount > 0
? getIcons($(".fwd-lanes")
.find(".lane-arrow")
.map(function () {
return this;
})
.get())
: false;
const revEle = seg?.toNodeLanesCount && seg.toNodeLanesCount > 0
? getIcons($(".rev-lanes")
.find(".lane-arrow")
.map(function () {
return this;
})
.get())
: false;
const fwdImgs = fwdEle !== false ? convertToBase64(fwdEle) : false;
const revImgs = revEle !== false ? convertToBase64(revEle) : false;
if (fwdEle) {
if (fwdEle.length === 0) {
// setTimeout(displayLaneGraphics, 200);
return;
}
drawIcons(seg, !seg || !seg.toNodeId ? null : sdk.DataModel.Nodes.getById({ nodeId: seg?.toNodeId }), fwdImgs);
}
if (revEle) {
if (revEle.length === 0) {
// setTimeout(displayLaneGraphics, 200);
return;
}
drawIcons(seg, !seg || !seg.fromNodeId ? null : sdk.DataModel.Nodes.getById({ nodeId: seg?.fromNodeId }), revImgs);
}
});
// There are now 23 zoom levels where 22 is fully zoomed and currently 14 is where major road types load data and 16 loads the rest
}
laneToolsBootstrap();
}