WME HN NavPoints

Shows navigation points of all house numbers in WME

  1. // ==UserScript==
  2. // @name WME HN NavPoints
  3. // @namespace https://greatest.deepsurf.us/users/166843
  4. // @description Shows navigation points of all house numbers in WME
  5. // @version 2024.08.18.01
  6. // @author dBsooner
  7. // @grant GM_xmlhttpRequest
  8. // @connect greatest.deepsurf.us
  9. // @require https://greatest.deepsurf.us/scripts/24851-wazewrap/code/WazeWrap.js
  10. // @license GPLv3
  11. // @match http*://*.waze.com/*editor*
  12. // @exclude http*://*.waze.com/user/editor*
  13. // @contributionURL https://github.com/WazeDev/Thank-The-Authors
  14. // ==/UserScript==
  15.  
  16. /* global _, GM_info, GM_xmlhttpRequest, OpenLayers, W, WazeWrap */
  17.  
  18. /*
  19. * Original concept and code for WME HN NavPoints was written by MajkiiTelini. After version 0.6.6, this
  20. * script is maintained by the WazeDev team. Special thanks is definitely given to MajkiiTelini for his
  21. * hard work and dedication to the original script.
  22. *
  23. */
  24.  
  25. (function () {
  26. 'use strict';
  27.  
  28. // eslint-disable-next-line no-nested-ternary
  29. const _SCRIPT_SHORT_NAME = `HN NavPoints${(/beta/.test(GM_info.script.name) ? ' β' : /\(DEV\)/i.test(GM_info.script.name) ? ' Ω' : '')}`,
  30. _SCRIPT_LONG_NAME = GM_info.script.name,
  31. _IS_ALPHA_VERSION = /[Ω]/.test(_SCRIPT_SHORT_NAME),
  32. _IS_BETA_VERSION = /[β]/.test(_SCRIPT_SHORT_NAME),
  33. _PROD_DL_URL = 'https://greatest.deepsurf.us/scripts/390565-wme-hn-navpoints/code/WME%20HN%20NavPoints.user.js',
  34. _FORUM_URL = 'https://www.waze.com/forum/viewtopic.php?f=819&t=269397',
  35. _SETTINGS_STORE_NAME = 'WMEHNNavPoints',
  36. _BETA_DL_URL = 'YUhSMGNITTZMeTluY21WaGMzbG1iM0pyTG05eVp5OXpZM0pwY0hSekx6TTVNRFUzTXkxM2JXVXRhRzR0Ym1GMmNHOXBiblJ6TFdKbGRHRXZZMjlrWlM5WFRVVWxNakJJVGlVeU1FNWhkbEJ2YVc1MGN5VXlNQ2hpWlhSaEtTNTFjMlZ5TG1weg==',
  37. _ALERT_UPDATE = true,
  38. _SCRIPT_VERSION = GM_info.script.version.toString(),
  39. _SCRIPT_VERSION_CHANGES = ['CHANGE: WME beta release v2.242 compatibility.'],
  40. _DEBUG = /[βΩ]/.test(_SCRIPT_SHORT_NAME),
  41. _LOAD_BEGIN_TIME = performance.now(),
  42. _elems = {
  43. div: document.createElement('div'),
  44. h4: document.createElement('h4'),
  45. h6: document.createElement('h6'),
  46. form: document.createElement('form'),
  47. i: document.createElement('i'),
  48. label: document.createElement('label'),
  49. li: document.createElement('li'),
  50. p: document.createElement('p'),
  51. svg: document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
  52. svgText: document.createElementNS('http://www.w3.org/2000/svg', 'text'),
  53. ul: document.createElement('ul'),
  54. 'wz-checkbox': document.createElement('wz-checkbox'),
  55. 'wz-text-input': document.createElement('wz-text-input')
  56. },
  57. _spinners = {
  58. destroyAllHNs: false,
  59. drawHNs: false,
  60. processSegs: false
  61. },
  62. _timeouts = {
  63. checkMarkersEvents: {},
  64. hideTooltip: undefined,
  65. onWmeReady: undefined,
  66. saveSettingsToStorage: undefined,
  67. stripTooltipHTML: undefined
  68. },
  69. dec = (s = '') => atob(atob(s));
  70.  
  71. let _settings = {},
  72. _scriptActive = false,
  73. _saveButtonObserver,
  74. _HNNavPointsLayer,
  75. _HNNavPointsNumbersLayer,
  76. _processedSegments = [],
  77. _segmentsToProcess = [],
  78. _segmentsToRemove = [],
  79. _hnNavPointsTooltipDiv,
  80. _popup = {
  81. inUse: false,
  82. hnNumber: -1,
  83. segmentId: -1
  84. };
  85.  
  86. function log(message, data = '') { console.log(`${_SCRIPT_SHORT_NAME}:`, message, data); }
  87. function logError(message, data = '') { console.error(`${_SCRIPT_SHORT_NAME}:`, new Error(message), data); }
  88. // function logWarning(message, data = '') { console.warn(`${_SCRIPT_SHORT_NAME}:`, message, data); }
  89. function logDebug(message, data = '') {
  90. if (_DEBUG)
  91. log(message, data);
  92. }
  93.  
  94. function $extend(...args) {
  95. const extended = {},
  96. deep = Object.prototype.toString.call(args[0]) === '[object Boolean]' ? args[0] : false,
  97. merge = function (obj) {
  98. Object.keys(obj).forEach((prop) => {
  99. if (Object.prototype.hasOwnProperty.call(obj, prop)) {
  100. if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]')
  101. extended[prop] = $extend(true, extended[prop], obj[prop]);
  102. else if ((obj[prop] !== undefined) && (obj[prop] !== null))
  103. extended[prop] = obj[prop];
  104. }
  105. });
  106. };
  107. for (let i = deep ? 1 : 0, { length } = args; i < length; i++) {
  108. if (args[i])
  109. merge(args[i]);
  110. }
  111. return extended;
  112. }
  113.  
  114. function createElem(type = '', attrs = {}, eventListener = []) {
  115. const el = _elems[type]?.cloneNode(false) || _elems.div.cloneNode(false),
  116. applyEventListeners = function ([evt, cb]) {
  117. return this.addEventListener(evt, cb);
  118. };
  119. Object.keys(attrs).forEach((attr) => {
  120. if ((attrs[attr] !== undefined) && (attrs[attr] !== 'undefined') && (attrs[attr] !== null) && (attrs[attr] !== 'null')) {
  121. if ((attr === 'disabled') || (attr === 'checked') || (attr === 'selected') || (attr === 'textContent') || (attr === 'innerHTML'))
  122. el[attr] = attrs[attr];
  123. else
  124. el.setAttribute(attr, attrs[attr]);
  125. }
  126. });
  127. if (eventListener.length > 0) {
  128. eventListener.forEach((obj) => {
  129. Object.entries(obj).map(applyEventListeners.bind(el));
  130. });
  131. }
  132. return el;
  133. }
  134.  
  135. async function loadSettingsFromStorage() {
  136. const defaultSettings = {
  137. disableBelowZoom: 17,
  138. enableTooltip: true,
  139. hnLines: true,
  140. hnNumbers: true,
  141. keepHNLayerOnTop: true,
  142. toggleHNNavPointsShortcut: '',
  143. toggleHNNavPointsNumbersShortcut: '',
  144. lastSaved: 0,
  145. lastVersion: undefined
  146. },
  147. loadedSettings = JSON.parse(localStorage.getItem(_SETTINGS_STORE_NAME));
  148. _settings = $extend(true, {}, defaultSettings, loadedSettings);
  149. const serverSettings = await WazeWrap.Remote.RetrieveSettings(_SETTINGS_STORE_NAME);
  150. if (serverSettings?.lastSaved > _settings.lastSaved)
  151. _settings = $extend(true, _settings, serverSettings);
  152. if (_settings.disableBelowZoom < 11)
  153. _settings.disableBelowZoom += 12;
  154. _timeouts.saveSettingsToStorage = window.setTimeout(saveSettingsToStorage, 5000);
  155.  
  156. return Promise.resolve();
  157. }
  158.  
  159. function saveSettingsToStorage() {
  160. checkTimeout({ timeout: 'saveSettingsToStorage' });
  161. if (localStorage) {
  162. ['toggleHNNavPointsShortcut', 'toggleHNNavPointsNumbersShortcut'].forEach((k) => {
  163. let keys = '';
  164. const { shortcut } = W.accelerators.Actions[k];
  165. if (shortcut) {
  166. if (shortcut.altKey)
  167. keys += 'A';
  168. if (shortcut.shiftKey)
  169. keys += 'S';
  170. if (shortcut.ctrlKey)
  171. keys += 'C';
  172. if (keys !== '')
  173. keys += '+';
  174. if (shortcut.keyCode)
  175. keys += shortcut.keyCode;
  176. }
  177. _settings[k] = keys;
  178. });
  179. _settings.lastVersion = _SCRIPT_VERSION;
  180. _settings.lastSaved = Date.now();
  181. localStorage.setItem(_SETTINGS_STORE_NAME, JSON.stringify(_settings));
  182. WazeWrap.Remote.SaveSettings(_SETTINGS_STORE_NAME, _settings);
  183. logDebug('Settings saved.');
  184. }
  185. }
  186. function showScriptInfoAlert() {
  187. if (_ALERT_UPDATE && (_SCRIPT_VERSION !== _settings.lastVersion)) {
  188. const divElemRoot = createElem('div');
  189. divElemRoot.appendChild(createElem('p', { textContent: 'What\'s New:' }));
  190. const ulElem = createElem('ul');
  191. if (_SCRIPT_VERSION_CHANGES.length > 0) {
  192. for (let idx = 0, { length } = _SCRIPT_VERSION_CHANGES; idx < length; idx++)
  193. ulElem.appendChild(createElem('li', { innerHTML: _SCRIPT_VERSION_CHANGES[idx] }));
  194. }
  195. else {
  196. ulElem.appendChild(createElem('li', { textContent: 'Nothing major.' }));
  197. }
  198. divElemRoot.appendChild(ulElem);
  199. WazeWrap.Interface.ShowScriptUpdate(_SCRIPT_SHORT_NAME, _SCRIPT_VERSION, divElemRoot.innerHTML, (_IS_BETA_VERSION ? dec(_BETA_DL_URL) : _PROD_DL_URL).replace(/code\/.*\.js/, ''), _FORUM_URL);
  200. }
  201. }
  202.  
  203. function checkTimeout(obj) {
  204. if (obj.toIndex) {
  205. if (_timeouts[obj.timeout]?.[obj.toIndex]) {
  206. window.clearTimeout(_timeouts[obj.timeout][obj.toIndex]);
  207. delete (_timeouts[obj.timeout][obj.toIndex]);
  208. }
  209. }
  210. else {
  211. if (_timeouts[obj.timeout])
  212. window.clearTimeout(_timeouts[obj.timeout]);
  213. _timeouts[obj.timeout] = undefined;
  214. }
  215. }
  216.  
  217. function doSpinner(spinnerName = '', spin = true) {
  218. const btn = document.getElementById('hnNPSpinner');
  219. if (!spin) {
  220. _spinners[spinnerName] = false;
  221. if (!Object.values(_spinners).some((a) => a === true)) {
  222. if (btn) {
  223. btn.classList.remove('fa-spin');
  224. document.getElementById('divHnNPSpinner').style.display = 'none';
  225. }
  226. else {
  227. const topBar = document.querySelector('#topbar-container .topbar'),
  228. divElem = createElem('div', {
  229. id: 'divHnNPSpinner', title: 'WME HN NavPoints is currently processing house numbers.', style: 'font-size:20px;background:white;float:left;display:none;'
  230. });
  231. divElem.appendChild(createElem('i', { id: 'hnNPSpinner', class: 'fa fa-spinner' }));
  232. topBar.insertBefore(divElem, topBar.firstChild);
  233. }
  234. }
  235. }
  236. else {
  237. _spinners[spinnerName] = true;
  238. if (!btn) {
  239. _spinners[spinnerName] = true;
  240. const topBar = document.querySelector('#topbar-container .topbar'),
  241. divElem = createElem('div', {
  242. id: 'divHnNPSpinner', title: 'WME HN NavPoints is currently processing house numbers.', style: 'font-size:20px;background:white;float:left;'
  243. });
  244. divElem.appendChild(createElem('i', { id: 'hnNPSpinner', class: 'fa fa-spinner fa-spin' }));
  245. topBar.insertBefore(divElem, topBar.firstChild);
  246. }
  247. else if (!btn.classList.contains('fa-spin')) {
  248. btn.classList.add('fa-spin');
  249. document.getElementById('divHnNPSpinner').style.display = '';
  250. }
  251. }
  252. }
  253.  
  254. // eslint-disable-next-line default-param-last
  255. function processSegmentsToRemove(force = false, segmentsArr) {
  256. const segmentsToProcess = segmentsArr || _segmentsToRemove;
  257. if (segmentsToProcess.length > 0) {
  258. let linesToRemove = [],
  259. hnsToRemove = [];
  260. const filterMarkers = function (marker) { return marker?.segmentId === this; },
  261. processFilterMarkers = (marker) => hnsToRemove.push(marker);
  262. for (let i = segmentsToProcess.length - 1; i > -1; i--) {
  263. const segId = segmentsToProcess[i];
  264. if (!W.model.segments.getObjectById(segId) || force) {
  265. segmentsToProcess.splice(i, 1);
  266. linesToRemove = linesToRemove.concat(_HNNavPointsLayer.getFeaturesByAttribute('segmentId', segId));
  267. if (!_settings.enableTooltip)
  268. hnsToRemove = hnsToRemove.concat(_HNNavPointsNumbersLayer.getFeaturesByAttribute('segmentId', segId));
  269. else
  270. _HNNavPointsNumbersLayer.markers.filter(filterMarkers.bind(segId)).forEach(processFilterMarkers);
  271. }
  272. }
  273. if (linesToRemove.length > 0)
  274. _HNNavPointsLayer.removeFeatures(linesToRemove);
  275. if (hnsToRemove.length > 0) {
  276. if (!_settings.enableTooltip)
  277. _HNNavPointsNumbersLayer.removeFeatures(hnsToRemove);
  278. else
  279. hnsToRemove.forEach((marker) => _HNNavPointsNumbersLayer.removeMarker(marker));
  280. }
  281. }
  282. }
  283.  
  284. async function hnLayerToggled(checked) {
  285. _HNNavPointsLayer.setVisibility(checked);
  286. _settings.hnLines = checked;
  287. saveSettingsToStorage();
  288. if (checked) {
  289. if (!_scriptActive)
  290. await initBackgroundTasks('enable');
  291. processSegs('hnLayerToggled', W.model.segments.getObjectArray().filter((o) => o.getAttribute('hasHNs')));
  292. }
  293. else if (!_settings.hnNumbers && _scriptActive) {
  294. initBackgroundTasks('disable');
  295. }
  296. }
  297.  
  298. async function hnNumbersLayerToggled(checked) {
  299. _HNNavPointsNumbersLayer.setVisibility(checked);
  300. _settings.hnNumbers = checked;
  301. saveSettingsToStorage();
  302. if (checked) {
  303. if (!_scriptActive)
  304. await initBackgroundTasks('enable');
  305. processSegs('hnNumbersLayerToggled', W.model.segments.getObjectArray().filter((o) => o.getAttribute('hasHNs')));
  306. }
  307. else if (!_settings.hnLines && _scriptActive) {
  308. initBackgroundTasks('disable');
  309. }
  310. }
  311.  
  312. function observeHNLayer() {
  313. if (W.editingMediator.get('editingHouseNumbers')) {
  314. _segmentsToProcess = W.selectionManager.getSegmentSelection().segments.map((o) => o.getID());
  315. _segmentsToRemove = [];
  316. }
  317. else {
  318. W.model.segmentHouseNumbers.clear();
  319. processSegmentsToRemove(true, [..._segmentsToProcess]);
  320. processSegs('exithousenumbers', W.model.segments.getByIds([..._segmentsToProcess]), true);
  321. _segmentsToProcess = [];
  322. _segmentsToRemove = [];
  323. _timeouts.checkMarkersEvents = {};
  324. }
  325. _saveButtonObserver.disconnect();
  326. _saveButtonObserver.observe(document.getElementById('save-button'), {
  327. childList: false, attributes: true, attributeOldValue: true, characterData: false, characterDataOldValue: false, subtree: false
  328. });
  329. }
  330.  
  331. function removeHNs(objArr) {
  332. let linesToRemove = [],
  333. hnsToRemove = [];
  334. const filterMarkers = function (marker) { return marker?.featureId === this.attributes.id; },
  335. processFilterMarkers = (marker) => {
  336. hnsToRemove.push(marker);
  337. };
  338. objArr.forEach((hnObj) => {
  339. linesToRemove = linesToRemove.concat(_HNNavPointsLayer.getFeaturesByAttribute('featureId', hnObj.getID()));
  340. if (!_settings.enableTooltip)
  341. hnsToRemove = hnsToRemove.concat(_HNNavPointsNumbersLayer.getFeaturesByAttribute('featureId', hnObj.getID()));
  342. else
  343. _HNNavPointsNumbersLayer.markers.filter(filterMarkers.bind(hnObj)).forEach(processFilterMarkers);
  344. });
  345. if (linesToRemove.length > 0)
  346. _HNNavPointsLayer.removeFeatures(linesToRemove);
  347. if (hnsToRemove.length > 0) {
  348. if (!_settings.enableTooltip)
  349. _HNNavPointsNumbersLayer.removeFeatures(hnsToRemove);
  350. else
  351. hnsToRemove.forEach((marker) => _HNNavPointsNumbersLayer.removeMarker(marker));
  352. }
  353. }
  354.  
  355. function drawHNs(houseNumberArr) {
  356. if (houseNumberArr.length === 0)
  357. return;
  358. doSpinner('drawHNs', true);
  359. let svg,
  360. svgText,
  361. hnsToRemove = [],
  362. linesToRemove = [];
  363. const lineFeatures = [],
  364. numberFeatures = [],
  365. invokeTooltip = _settings.enableTooltip ? (evt) => { showTooltip(evt); } : undefined,
  366. mapFeatureId = (marker) => marker.featureId;
  367. if (_settings.enableTooltip) {
  368. svg = createElem('svg', { xlink: 'http://www.w3.org/1999/xlink', xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 40 14' });
  369. svgText = createElem('svgText', { 'text-anchor': 'middle', x: '20', y: '10' });
  370. }
  371. for (let i = 0, { length } = houseNumberArr; i < length; i++) {
  372. const hnObj = houseNumberArr[i],
  373. segmentId = hnObj.getSegmentId();
  374. if (W.model.segments.getObjectById(segmentId)) {
  375. const featureId = hnObj.getID(),
  376. markerIdx = _settings.enableTooltip ? _HNNavPointsNumbersLayer.markers.map(mapFeatureId).indexOf(featureId) : undefined,
  377. // eslint-disable-next-line no-nested-ternary
  378. hnToRemove = _settings.enableTooltip ? (markerIdx > -1) ? _HNNavPointsNumbersLayer.markers[markerIdx] : [] : _HNNavPointsNumbersLayer.getFeaturesByAttribute('featureId', featureId),
  379. rtlChar = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg,
  380. textDir = (hnObj.getNumber().match(rtlChar) !== null) ? 'rtl' : 'ltr';
  381. linesToRemove = linesToRemove.concat(_HNNavPointsLayer.getFeaturesByAttribute('featureId', featureId));
  382. if (hnToRemove.length > 0) {
  383. if (!_settings.enableTooltip)
  384. hnsToRemove = hnsToRemove.concat(_HNNavPointsNumbersLayer.getFeaturesByAttribute('featureId', featureId));
  385. else
  386. hnsToRemove.push(hnToRemove);
  387. }
  388. //Fix this mess once WME beta v2.188 is released to production.
  389. const betaFractionPoint = (hnObj.getFractionPoint().coordinates)
  390. ? WazeWrap.Geometry.ConvertTo900913(hnObj.getFractionPoint().coordinates[0], hnObj.getFractionPoint().coordinates[1])
  391. : undefined,
  392. fractionX = betaFractionPoint ? betaFractionPoint.lon : hnObj.getFractionPoint().x,
  393. fractionY = betaFractionPoint ? betaFractionPoint.lat : hnObj.getFractionPoint().y,
  394. geometryX = hnObj.getOLGeometry ? hnObj.getOLGeometry().x : hnObj.getGeometry().x,
  395. geometryY = hnObj.getOLGeometry ? hnObj.getOLGeometry().y : hnObj.getGeometry().y,
  396. p1 = new OpenLayers.Geometry.Point(fractionX, fractionY),
  397. p2 = new OpenLayers.Geometry.Point(geometryX, geometryY),
  398. // eslint-disable-next-line no-nested-ternary
  399. strokeColor = (hnObj.isForced()
  400. ? (!hnObj.getUpdatedBy()) ? 'red' : 'orange'
  401. : (!hnObj.getUpdatedBy()) ? 'yellow' : 'white'
  402. );
  403. let lineString = new OpenLayers.Geometry.LineString([p1, p2]),
  404. lineFeature = new OpenLayers.Feature.Vector(
  405. lineString,
  406. { segmentId, featureId },
  407. {
  408. strokeWidth: 4, strokeColor: 'black', strokeOpacity: 0.5, strokeDashstyle: 'dash', strokeDashArray: '8, 8'
  409. }
  410. );
  411. lineFeatures.push(lineFeature);
  412. lineString = new OpenLayers.Geometry.LineString([p1, p2]);
  413. lineFeature = new OpenLayers.Feature.Vector(
  414. lineString,
  415. { segmentId, featureId },
  416. {
  417. strokeWidth: 2, strokeColor, strokeOpacity: 1, strokeDashstyle: 'dash', strokeDashArray: '8, 8'
  418. }
  419. );
  420. lineFeatures.push(lineFeature);
  421. if (_settings.enableTooltip) {
  422. svg.setAttribute('style', `text-shadow:0 0 3px ${strokeColor},0 0 3px ${strokeColor},0 0 3px ${strokeColor},0 0 3px ${strokeColor},0 0 3px ${strokeColor},0 0 3px ${strokeColor};font-size:14px;font-weight:bold;font-family:"Open Sans", "Arial Unicode MS", "sans-serif";direction:${textDir}`);
  423. svgText.textContent = hnObj.getNumber();
  424. svg.replaceChildren(svgText);
  425. const svgIcon = new WazeWrap.Require.Icon(`data:image/svg+xml,${svg.outerHTML}`, { w: 40, h: 18 }),
  426. markerFeature = new OpenLayers.Marker(new OpenLayers.LonLat(p2.x, p2.y), svgIcon);
  427. markerFeature.events.register('mouseover', null, invokeTooltip);
  428. markerFeature.events.register('mouseout', null, hideTooltipDelay);
  429. markerFeature.featureId = featureId;
  430. markerFeature.segmentId = segmentId;
  431. markerFeature.hnNumber = hnObj.getNumber() || '';
  432. numberFeatures.push(markerFeature);
  433. }
  434. else {
  435. // eslint-disable-next-line new-cap
  436. numberFeatures.push(new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Polygon.createRegularPolygon(p2, 1, 20), {
  437. segmentId, featureId, hNumber: hnObj.getNumber(), strokeWidth: 3, Color: strokeColor, textDir
  438. }));
  439. }
  440. }
  441. }
  442. if (linesToRemove.length > 0)
  443. _HNNavPointsLayer.removeFeatures(linesToRemove);
  444. if (hnsToRemove.length > 0) {
  445. if (!_settings.enableTooltip)
  446. _HNNavPointsNumbersLayer.removeFeatures(hnsToRemove);
  447. else
  448. hnsToRemove.forEach((marker) => _HNNavPointsNumbersLayer.removeMarker(marker));
  449. }
  450. if (lineFeatures.length > 0)
  451. _HNNavPointsLayer.addFeatures(lineFeatures);
  452. if (numberFeatures.length > 0) {
  453. if (!_settings.enableTooltip)
  454. _HNNavPointsNumbersLayer.addFeatures(numberFeatures);
  455. else
  456. numberFeatures.forEach((marker) => _HNNavPointsNumbersLayer.addMarker(marker));
  457. }
  458. doSpinner('drawHNs', false);
  459. }
  460.  
  461. function destroyAllHNs() {
  462. doSpinner('destroyAllHNs', true);
  463. _HNNavPointsLayer.destroyFeatures();
  464. if (_settings.enableTooltip)
  465. _HNNavPointsNumbersLayer.clearMarkers();
  466. else
  467. _HNNavPointsNumbersLayer.destroyFeatures();
  468. _processedSegments = [];
  469. doSpinner('destroyAllHNs', false);
  470. Promise.resolve();
  471. }
  472.  
  473. function getOLMapExtent() {
  474. let extent = W.map.getExtent();
  475. if (Array.isArray(extent)) {
  476. extent = new OpenLayers.Bounds(extent);
  477. extent.transform('EPSG:4326', 'EPSG:3857');
  478. }
  479. return extent;
  480. }
  481.  
  482. function processSegs(action, arrSegObjs, processAll = false, retry = 0) {
  483. /* As of 2020.06.08 (sometime before this date) updatedOn does not get updated when updating house numbers. Looking for a new
  484. * way to track which segments have been updated most recently to prevent a total refresh of HNs after an event.
  485. * Changed to using a global to keep track of segmentIds touched during HN edit mode.
  486. */
  487. if ((action === 'settingChanged') && (W.map.getOLMap().getZoom() < _settings.disableBelowZoom)) {
  488. destroyAllHNs();
  489. return;
  490. }
  491. if (!arrSegObjs || (arrSegObjs.length === 0) || (W.map.getOLMap().getZoom() < _settings.disableBelowZoom) || preventProcess())
  492. return;
  493. doSpinner('processSegs', true);
  494. const eg = getOLMapExtent().toGeometry(),
  495. findObjIndex = (array, fldName, value) => array.map((a) => a[fldName]).indexOf(value),
  496. processError = (err, chunk) => {
  497. logDebug(`Retry: ${retry}`);
  498. if (retry < 5)
  499. processSegs(action, chunk, true, ++retry);
  500. else
  501. logError(`Get HNs for ${chunk.length} segments failed. Code: ${err.status} - Text: ${err.responseText}`);
  502. },
  503. processJSON = (jsonData) => {
  504. if ((jsonData?.error === undefined) && (typeof jsonData?.segmentHouseNumbers?.objects !== 'undefined'))
  505. drawHNs(jsonData.segmentHouseNumbers.objects);
  506. },
  507. mapHouseNumbers = (segObj) => segObj.getID(),
  508. invokeProcessError = function (err) { return processError(err, this); };
  509. if ((action === 'objectsremoved')) {
  510. if (arrSegObjs?.length > 0) {
  511. const removedSegIds = [];
  512. let hnNavPointsToRemove = [],
  513. hnNavPointsNumbersToRemove = [];
  514. arrSegObjs.forEach((segObj) => {
  515. const segmentId = segObj.getID();
  516. if (!eg.intersects(segObj.getAttribute('geometry')) && (segmentId > 0)) {
  517. hnNavPointsToRemove = hnNavPointsToRemove.concat(_HNNavPointsLayer.getFeaturesByAttribute('segmentId', segmentId));
  518. if (!_settings.enableTooltip)
  519. hnNavPointsNumbersToRemove = hnNavPointsNumbersToRemove.concat(_HNNavPointsNumbersLayer.getFeaturesByAttribute('segmentId', segmentId));
  520. else
  521. removedSegIds.push(segmentId);
  522. const segIdx = findObjIndex(_processedSegments, 'segId', segmentId);
  523. if (segIdx > -1)
  524. _processedSegments.splice(segIdx, 1);
  525. }
  526. });
  527. if (hnNavPointsToRemove.length > 0)
  528. _HNNavPointsLayer.removeFeatures(hnNavPointsToRemove);
  529. if (hnNavPointsNumbersToRemove.length > 0)
  530. _HNNavPointsNumbersLayer.removeFeatures(hnNavPointsNumbersToRemove);
  531. if (removedSegIds.length > 0) {
  532. _HNNavPointsNumbersLayer.markers.filter((marker) => removedSegIds.includes(marker.segmentId)).forEach((marker) => {
  533. _HNNavPointsNumbersLayer.removeMarker(marker);
  534. });
  535. }
  536. }
  537. }
  538. else { // action = 'objectsadded', 'zoomend', 'init', 'exithousenumbers', 'hnLayerToggled', 'hnNumbersLayerToggled', 'settingChanged', 'afterSave', 'afterclearactions'
  539. let i = arrSegObjs.length;
  540. while (i--) {
  541. if (arrSegObjs[i].getID() < 0) {
  542. arrSegObjs.splice(i, 1);
  543. }
  544. else {
  545. const segIdx = findObjIndex(_processedSegments, 'segId', arrSegObjs[i].getID());
  546. if (segIdx > -1) {
  547. if (arrSegObjs[i].getUpdatedOn() > _processedSegments[segIdx].updatedOn)
  548. _processedSegments[segIdx].updatedOn = arrSegObjs[i].getUpdatedOn();
  549. else if (!processAll)
  550. arrSegObjs.splice(i, 1);
  551. }
  552. else {
  553. _processedSegments.push({ segId: arrSegObjs[i].getID(), updatedOn: arrSegObjs[i].getUpdatedOn() });
  554. }
  555. }
  556. }
  557. while (arrSegObjs.length > 0) {
  558. let chunk;
  559. if (retry === 1)
  560. chunk = arrSegObjs.splice(0, 250);
  561. else if (retry === 2)
  562. chunk = arrSegObjs.splice(0, 125);
  563. else if (retry === 3)
  564. chunk = arrSegObjs.splice(0, 100);
  565. else if (retry === 4)
  566. chunk = arrSegObjs.splice(0, 50);
  567. else
  568. chunk = arrSegObjs.splice(0, 500);
  569. try {
  570. W.controller.descartesClient.getHouseNumbers(chunk.map(mapHouseNumbers)).then(processJSON).catch(invokeProcessError.bind(chunk));
  571. }
  572. catch (error) {
  573. processError(error, [...chunk]);
  574. }
  575. }
  576. }
  577. doSpinner('processSegs', false);
  578. }
  579.  
  580. function preventProcess() {
  581. if (!_settings.hnLines && !_settings.hnNumbers) {
  582. if (_scriptActive)
  583. initBackgroundTasks('disable');
  584. destroyAllHNs();
  585. return true;
  586. }
  587. if (W.map.getOLMap().getZoom() < _settings.disableBelowZoom) {
  588. destroyAllHNs();
  589. return true;
  590. }
  591. return false;
  592. }
  593.  
  594. function segmentsEvent(evt) {
  595. if (!evt || preventProcess())
  596. return;
  597. if ((this.action === 'objectssynced') || (this.action === 'objectsremoved'))
  598. processSegmentsToRemove();
  599. if (this.action === 'objectschanged-id') {
  600. const oldSegmentId = evt.oldID,
  601. newSegmentID = evt.newID;
  602. _HNNavPointsLayer.getFeaturesByAttribute('segmentId', oldSegmentId).forEach((feature) => { feature.attributes.segmentId = newSegmentID; });
  603. if (_settings.enableTooltip)
  604. _HNNavPointsNumbersLayer.markers.filter((marker) => marker.segmentId === oldSegmentId).forEach((marker) => { marker.segmentId = newSegmentID; });
  605. else
  606. _HNNavPointsNumbersLayer.getFeaturesByAttribute('segmentId', oldSegmentId).forEach((feature) => { feature.attributes.segmentId = newSegmentID; });
  607. }
  608. else if (this.action === 'objects-state-deleted') {
  609. evt.forEach((obj) => {
  610. if (!_segmentsToRemove.includes(obj.getID()))
  611. _segmentsToRemove.push(obj.getID());
  612. });
  613. }
  614. else {
  615. processSegs(this.action, evt.filter((o) => o.getAttribute('hasHNs')));
  616. }
  617. }
  618.  
  619. function objectsChangedIdHNs(evt) {
  620. if (!evt || preventProcess())
  621. return;
  622. const oldFeatureId = evt.oldID,
  623. newFeatureId = evt.newID;
  624. _HNNavPointsLayer.getFeaturesByAttribute('featureId', oldFeatureId).forEach((feature) => { feature.attributes.featureId = newFeatureId; });
  625. if (_settings.enableTooltip)
  626. _HNNavPointsNumbersLayer.markers.filter((marker) => marker.featureId === oldFeatureId).forEach((marker) => { marker.featureId = newFeatureId; });
  627. else
  628. _HNNavPointsNumbersLayer.getFeaturesByAttribute('featureId', oldFeatureId).forEach((feature) => { feature.attributes.featureId = newFeatureId; });
  629. }
  630.  
  631. function objectsChangedHNs(evt) {
  632. if (!evt || preventProcess())
  633. return;
  634. if ((evt.length === 1) && evt[0].getSegmentId() && !_segmentsToProcess.includes(evt[0].getSegmentId()))
  635. _segmentsToProcess.push(evt[0].getSegmentId());
  636. }
  637.  
  638. function objectsStateDeletedHNs(evt) {
  639. if (!evt || preventProcess())
  640. return;
  641. if ((evt.length === 1) && evt[0].getSegmentId() && !_segmentsToProcess.includes(evt[0].getSegmentId()))
  642. _segmentsToProcess.push(evt[0].getSegmentId());
  643. removeHNs(evt);
  644. }
  645.  
  646. function objectsAddedHNs(evt) {
  647. if (!evt || preventProcess())
  648. return;
  649. if ((evt.length === 1) && evt[0].getSegmentId() && !_segmentsToProcess.includes(evt[0].getSegmentId()))
  650. _segmentsToProcess.push(evt[0].getSegmentId());
  651. }
  652.  
  653. function zoomEndEvent() {
  654. if (preventProcess())
  655. return;
  656. if ((W.map.getOLMap().getZoom() < _settings.disableBelowZoom))
  657. destroyAllHNs();
  658. if ((W.map.getOLMap().getZoom() > (_settings.disableBelowZoom - 1)) && (_processedSegments.length === 0))
  659. processSegs('zoomend', W.model.segments.getObjectArray().filter((o) => o.getAttribute('hasHNs')), true);
  660. }
  661.  
  662. function afterActionsEvent(evt) {
  663. if (!evt || preventProcess())
  664. return;
  665. if ((evt.type === 'afterclearactions') || (evt.type === 'noActions')) {
  666. processSegmentsToRemove(true, [..._segmentsToProcess]);
  667. processSegs('afterclearactions', W.model.segments.getByIds([..._segmentsToProcess]), true);
  668. }
  669. else if (evt.action?._description?.includes('Deleted house number')) {
  670. if (evt.type === 'afterundoaction')
  671. drawHNs([evt.action.object]);
  672. else
  673. removeHNs([evt.action.object]);
  674. }
  675. else if (evt.action?._description?.includes('Updated house number')) {
  676. const tempEvt = _.cloneDeep(evt);
  677. if (evt.type === 'afterundoaction') {
  678. if (tempEvt.action.newAttributes?.number)
  679. tempEvt.action.attributes.number = tempEvt.action.newAttributes.number;
  680. }
  681. else if (evt.type === 'afteraction') {
  682. if (tempEvt.action.oldAttributes?.number)
  683. tempEvt.action.attributes.number = tempEvt.action.oldAttributes.number;
  684. }
  685. removeHNs([tempEvt.action.object]);
  686. drawHNs([evt.action.object]);
  687. }
  688. else if (evt.action?._description?.includes('Added house number')) {
  689. if (evt.type === 'afterundoaction')
  690. removeHNs([evt.action.houseNumber]);
  691. else
  692. drawHNs([evt.action.houseNumber]);
  693. }
  694. else if (evt.action?._description?.includes('Moved house number')) {
  695. drawHNs([evt.action.newHouseNumber]);
  696. }
  697. else if (evt.action?.houseNumber) {
  698. drawHNs((evt.action.newHouseNumber ? [evt.action.newHouseNumber] : [evt.action.houseNumber]));
  699. }
  700. }
  701.  
  702. async function reloadClicked() {
  703. if (preventProcess() || document.querySelector('wz-button.overlay-button.reload-button').classList.contains('disabled'))
  704. return;
  705. await destroyAllHNs();
  706. processSegs('reload', W.model.segments.getObjectArray().filter((o) => o.getAttribute('hasHNs')));
  707. }
  708.  
  709. function initBackgroundTasks(status) {
  710. if (status === 'enable') {
  711. _saveButtonObserver = new MutationObserver((mutationsList) => {
  712. if ((W.model.actionManager._redoStack.length === 0)
  713. && mutationsList.some((mutation) => ((mutation.attributeName === 'disabled')
  714. && (mutation.oldValue === 'true')
  715. && (mutation.target.disabled === true)))
  716. ) {
  717. if (W.editingMediator.get('editingHouseNumbers'))
  718. processSegs('afterSave', W.model.segments.getByIds([..._segmentsToProcess]), true);
  719. else
  720. processSegmentsToRemove();
  721. }
  722. });
  723. _saveButtonObserver.observe(document.getElementById('save-button'), {
  724. childList: false, attributes: true, attributeOldValue: true, characterData: false, characterDataOldValue: false, subtree: false
  725. });
  726. _saveButtonObserver.observing = true;
  727. W.accelerators.events.on({ reloadData: destroyAllHNs });
  728. document.querySelector('wz-button.overlay-button.reload-button').addEventListener('click', reloadClicked);
  729. W.model.segments.on('objectsadded', segmentsEvent, { action: 'objectsadded' });
  730. W.model.segments.on('objectsremoved', segmentsEvent, { action: 'objectsremoved' });
  731. W.model.segments.on('objectssynced', segmentsEvent, { action: 'objectssynced' });
  732. W.model.segments.on('objects-state-deleted', segmentsEvent, { action: 'objects-state-deleted' });
  733. W.model.segments.on('objectschanged-id', segmentsEvent, { action: 'objectschanged-id' });
  734. W.model.segmentHouseNumbers.on({
  735. objectsadded: objectsAddedHNs,
  736. objectschanged: objectsChangedHNs,
  737. 'objectschanged-id': objectsChangedIdHNs,
  738. 'objects-state-deleted': objectsStateDeletedHNs
  739. });
  740. W.editingMediator.on({ 'change:editingHouseNumbers': observeHNLayer });
  741. W.map.events.on({
  742. zoomend: zoomEndEvent, addlayer: checkLayerIndex, removelayer: checkLayerIndex
  743. });
  744. WazeWrap.Events.register('afterundoaction', this, afterActionsEvent);
  745. WazeWrap.Events.register('afteraction', this, afterActionsEvent);
  746. WazeWrap.Events.register('afterclearactions', this, afterActionsEvent);
  747. _scriptActive = true;
  748. }
  749. else if (status === 'disable') {
  750. _saveButtonObserver = undefined;
  751. W.accelerators.events.on('reloadData', null, destroyAllHNs);
  752. document.querySelector('wz-button.overlay-button.reload-button').removeEventListener('click', reloadClicked);
  753. W.model.segments.off('objectsadded', segmentsEvent, { action: 'objectsadded' });
  754. W.model.segments.off('objectsremoved', segmentsEvent, { action: 'objectsremoved' });
  755. W.model.segments.off('objectschanged', segmentsEvent, { action: 'objectschanged' });
  756. W.model.segments.off('objects-state-deleted', segmentsEvent, { action: 'objects-state-deleted' });
  757. W.model.segments.off('objectschanged-id', segmentsEvent, { action: 'objectschanged-id' });
  758. W.model.segmentHouseNumbers.off({
  759. objectsadded: objectsAddedHNs,
  760. objectschanged: objectsChangedHNs,
  761. 'objectschanged-id': objectsChangedIdHNs,
  762. 'objects-state-deleted': objectsStateDeletedHNs,
  763. objectsremoved: removeHNs
  764. });
  765. W.editingMediator.off({ 'change:editingHouseNumbers': observeHNLayer });
  766. W.map.events.unregister('zoomend', null, zoomEndEvent);
  767. W.map.events.unregister('addlayer', null, checkLayerIndex);
  768. W.map.events.unregister('removelayer', null, checkLayerIndex);
  769. WazeWrap.Events.unregister('afterundoaction', this, afterActionsEvent);
  770. WazeWrap.Events.unregister('afteraction', this, afterActionsEvent);
  771. _scriptActive = false;
  772. }
  773. return Promise.resolve();
  774. }
  775.  
  776. function enterHNEditMode(segment, moveMap) {
  777. if (segment) {
  778. if (moveMap)
  779. W.map.setCenter({ lon: segment.getCenter().x, lat: segment.getCenter().y }, W.map.getOLMap().getZoom());
  780. W.selectionManager.setSelectedModels(segment);
  781. document.querySelector('#segment-edit-general .edit-house-numbers').dispatchEvent(new MouseEvent('click', { bubbles: true }));
  782. }
  783. }
  784.  
  785. function showTooltip(evt) {
  786. if ((W.map.getOLMap().getZoom() < 16) || W.editingMediator.get('editingHouseNumbers') || !_settings.enableTooltip)
  787. return;
  788. if (evt?.object?.featureId) {
  789. checkTooltip();
  790. let moveMap = false;
  791. const { segmentId, hnNumber } = evt.object;
  792. if (_popup.inUse && (_popup.hnNumber === hnNumber) && (_popup.segmentId === segmentId))
  793. return;
  794. const segment = W.model.segments.getObjectById(segmentId),
  795. street = W.model.streets.getObjectById(segment.getPrimaryStreetID()),
  796. popupPixel = W.map.getPixelFromLonLat(evt.object.lonlat),
  797. divElemRoot = createElem('div', {
  798. id: 'hnNavPointsTooltipDiv-tooltip',
  799. class: 'tippy-box',
  800. 'data-state': 'hidden',
  801. tabindex: '-1',
  802. 'data-theme': 'light-border',
  803. 'data-animation': 'fade',
  804. role: 'tooltip',
  805. 'data-placement': 'top',
  806. style: 'max-width: 350px; transition-duration:300ms;'
  807. }),
  808. invokeEnterHNEditMode = () => enterHNEditMode(segment, moveMap),
  809. divElemRootDivDiv = createElem('div', { class: 'house-number-marker-tooltip' });
  810. divElemRootDivDiv.appendChild(createElem('div', { class: 'title', dir: 'auto', textContent: `${hnNumber} ${(street ? street.getName() : '')}` }));
  811. divElemRootDivDiv.appendChild(createElem('div', {
  812. id: 'hnNavPointsTooltipDiv-edit', class: 'edit-button fa fa-pencil', style: segment.canEditHouseNumbers() ? '' : 'display:none;'
  813. }, [{ click: invokeEnterHNEditMode }]));
  814. const divElemRootDiv = createElem('div', {
  815. id: 'hnNavPointsTooltipDiv-content', class: 'tippy-content', 'data-state': 'hidden', style: 'transition-duration: 300ms;'
  816. });
  817. divElemRootDiv.appendChild(divElemRootDivDiv);
  818. divElemRoot.appendChild(divElemRootDiv);
  819. divElemRoot.appendChild(createElem('div', {
  820. id: 'hnNavPointsTooltipDiv-arrow', class: 'tippy-arrow', style: 'position: absolute; left: 0px;'
  821. }));
  822. _hnNavPointsTooltipDiv.replaceChildren(divElemRoot);
  823. popupPixel.origX = popupPixel.x;
  824. const popupWidthHalf = (_hnNavPointsTooltipDiv.clientWidth / 2);
  825. let arrowOffset = (popupWidthHalf - 15),
  826. dataPlacement = 'top';
  827. popupPixel.x = ((popupPixel.x - popupWidthHalf + 5) > 0) ? (popupPixel.x - popupWidthHalf + 5) : 10;
  828. if (popupPixel.x === 10)
  829. arrowOffset = popupPixel.origX - 22;
  830. if ((popupPixel.x + (popupWidthHalf * 2)) > W.map.getEl()[0].clientWidth) {
  831. popupPixel.x = (popupPixel.origX - _hnNavPointsTooltipDiv.clientWidth + 8);
  832. arrowOffset = (_hnNavPointsTooltipDiv.clientWidth - 30);
  833. moveMap = true;
  834. }
  835. if (popupPixel.y - [..._hnNavPointsTooltipDiv.children].reduce((height, elem) => height + elem.getBoundingClientRect().height, 0) < 0) {
  836. popupPixel.y += 14;
  837. dataPlacement = 'bottom';
  838. }
  839. else {
  840. popupPixel.y -= ([..._hnNavPointsTooltipDiv.children].reduce((height, elem) => height + elem.getBoundingClientRect().height, 0) + 14);
  841. }
  842. _hnNavPointsTooltipDiv.style.transform = `translate(${Math.round(popupPixel.x)}px, ${Math.round(popupPixel.y)}px)`;
  843. _hnNavPointsTooltipDiv.querySelector('#hnNavPointsTooltipDiv-arrow').style.transform = `translate(${Math.max(0, Math.round(arrowOffset))}px, 0px)`;
  844. _hnNavPointsTooltipDiv.querySelector('#hnNavPointsTooltipDiv-tooltip').setAttribute('data-placement', dataPlacement);
  845. _hnNavPointsTooltipDiv.querySelector('#hnNavPointsTooltipDiv-tooltip').setAttribute('data-state', 'visible');
  846. _hnNavPointsTooltipDiv.querySelector('#hnNavPointsTooltipDiv-content').setAttribute('data-state', 'visible');
  847. _popup = { segmentId, hNumber: hnNumber, inUse: true };
  848. }
  849. }
  850.  
  851. function stripTooltipHTML() {
  852. checkTimeout({ timeout: 'stripTooltipHTML' });
  853. _hnNavPointsTooltipDiv.replaceChildren();
  854. _popup = { segmentId: -1, hnNumber: -1, inUse: false };
  855. }
  856.  
  857. function hideTooltip() {
  858. checkTimeout({ timeout: 'hideTooltip' });
  859. _hnNavPointsTooltipDiv.querySelector('#hnNavPointsTooltipDiv-content')?.setAttribute('data-state', 'hidden');
  860. _hnNavPointsTooltipDiv.querySelector('#hnNavPointsTooltipDiv-tooltip')?.setAttribute('data-state', 'hidden');
  861. _timeouts.stripTooltipHTML = window.setTimeout(stripTooltipHTML, 400);
  862. }
  863.  
  864. function hideTooltipDelay(evt) {
  865. if (!evt)
  866. return;
  867. checkTimeout({ timeout: 'hideTooltip' });
  868. const parentsArr = evt.toElement?.offsetParent ? [evt.toElement.offsetParent, evt.toElement.offsetParent.offSetParent] : [];
  869. if (evt.toElement && (parentsArr.includes(_HNNavPointsNumbersLayer?.div) || parentsArr.includes(_hnNavPointsTooltipDiv)))
  870. return;
  871. _timeouts.hideTooltip = window.setTimeout(hideTooltip, 100, evt);
  872. }
  873.  
  874. function checkTooltip() {
  875. checkTimeout({ timeout: 'hideTooltip' });
  876. }
  877.  
  878. function checkLayerIndex() {
  879. const layerIdx = W.map.layers.map((a) => a.uniqueName).indexOf('__HNNavPointsNumbersLayer');
  880. let properIdx;
  881. if (_settings.keepHNLayerOnTop) {
  882. const layersIndexes = [],
  883. layersLoaded = W.map.layers.map((a) => a.uniqueName);
  884. ['wmeGISLayersDefault', '__HNNavPointsLayer'].forEach((layerUniqueName) => {
  885. if (layersLoaded.indexOf(layerUniqueName) > 0)
  886. layersIndexes.push(layersLoaded.indexOf(layerUniqueName));
  887. });
  888. properIdx = (Math.max(...layersIndexes) + 1);
  889. }
  890. else {
  891. properIdx = (W.map.layers.map((a) => a.uniqueName).indexOf('__HNNavPointsLayer') + 1);
  892. }
  893. if (layerIdx !== properIdx) {
  894. W.map.layers.splice(properIdx, 0, W.map.layers.splice(layerIdx, 1)[0]);
  895. W.map.getOLMap().resetLayersZIndex();
  896. }
  897. }
  898.  
  899. function checkHnNavpointsVersion() {
  900. if (_IS_ALPHA_VERSION)
  901. return;
  902. let updateMonitor;
  903. try {
  904. updateMonitor = new WazeWrap.Alerts.ScriptUpdateMonitor(_SCRIPT_LONG_NAME, _SCRIPT_VERSION, (_IS_BETA_VERSION ? dec(_BETA_DL_URL) : _PROD_DL_URL), GM_xmlhttpRequest);
  905. updateMonitor.start();
  906. }
  907. catch (err) {
  908. logError('Upgrade version check:', err);
  909. }
  910. }
  911.  
  912. async function onWazeWrapReady() {
  913. log('Initializing.');
  914. checkHnNavpointsVersion();
  915. const navPointsNumbersLayersOptions = {
  916. displayInLayerSwitcher: true,
  917. uniqueName: '__HNNavPointsNumbersLayer',
  918. selectable: true,
  919. labelSelect: true,
  920. rendererOptions: { zIndexing: true },
  921. styleMap: new OpenLayers.StyleMap({
  922. default: new OpenLayers.Style({
  923. strokeColor: '${Color}',
  924. strokeOpacity: 1,
  925. strokeWidth: 3,
  926. fillColor: '${Color}',
  927. fillOpacity: 0.5,
  928. pointerEvents: 'visiblePainted',
  929. label: '${hNumber}',
  930. fontSize: '12px',
  931. fontFamily: 'Rubik, Boing-light, sans-serif;',
  932. fontWeight: 'bold',
  933. direction: '${textDir}',
  934. labelOutlineColor: '${Color}',
  935. labelOutlineWidth: 3,
  936. labelSelect: true
  937. })
  938. })
  939. },
  940. handleCheckboxToggle = function () {
  941. const settingName = this.id.substring(14);
  942. if (settingName === 'enableTooltip') {
  943. if (!this.checked)
  944. _HNNavPointsNumbersLayer.clearMarkers();
  945. else
  946. _HNNavPointsNumbersLayer.destroyFeatures();
  947. W.map.removeLayer(_HNNavPointsNumbersLayer);
  948. if (this.checked)
  949. _HNNavPointsNumbersLayer = new OpenLayers.Layer.Markers('HN NavPoints Numbers Layer', navPointsNumbersLayersOptions);
  950. else
  951. _HNNavPointsNumbersLayer = new OpenLayers.Layer.Vector('HN NavPoints Numbers Layer', navPointsNumbersLayersOptions);
  952. W.map.addLayer(_HNNavPointsNumbersLayer);
  953. _HNNavPointsNumbersLayer.setVisibility(_settings.hnNumbers);
  954. }
  955. _settings[settingName] = this.checked;
  956. if (settingName === 'keepHNLayerOnTop')
  957. checkLayerIndex();
  958. saveSettingsToStorage();
  959. if ((settingName === 'enableTooltip') && (W.map.getOLMap().getZoom() > (_settings.disableBelowZoom - 1)) && (_settings.hnLines || _settings.hnNumbers))
  960. processSegs('settingChanged', W.model.segments.getObjectArray().filter((o) => o.getAttribute('hasHNs')), true, 0);
  961. },
  962. handleTextboxChange = function () {
  963. const newVal = Math.min(22, Math.max(16, +this.value));
  964. if ((newVal !== _settings.disableBelowZoom) || (+this.value !== newVal)) {
  965. if (newVal !== +this.value)
  966. this.value = newVal;
  967. _settings.disableBelowZoom = newVal;
  968. saveSettingsToStorage();
  969. if ((W.map.getOLMap().getZoom() < newVal) && (_settings.hnLines || _settings.hnNumbers))
  970. processSegs('settingChanged', null, true, 0);
  971. else if (_settings.hnLines || _settings.hnNumbers)
  972. processSegs('settingChanged', W.model.segments.getObjectArray().filter((o) => o.getAttribute('hasHNs')), true, 0);
  973. }
  974. },
  975. buildCheckbox = (id = '', textContent = '', checked = true, title = '', disabled = false) => createElem('wz-checkbox', {
  976. id, title, disabled, checked, textContent
  977. }, [{ change: handleCheckboxToggle }]),
  978. buildTextBox = (id = '', label = '', value = '', placeholder = '', maxlength = 0, autocomplete = 'off', title = '', disabled = false) => createElem('wz-text-input', {
  979. id, label, value, placeholder, maxlength, autocomplete, title, disabled
  980. }, [{ change: handleTextboxChange }]),
  981. toggleHNNavPoints = () => document.getElementById('layer-switcher-item_hn_navpoints').dispatchEvent(new MouseEvent('click', { bubbles: true })),
  982. toggleHNNavPointsNumbers = () => document.getElementById('layer-switcher-item_hn_navpoints_numbers').dispatchEvent(new MouseEvent('click', { bubbles: true }));
  983. await loadSettingsFromStorage();
  984. WazeWrap.Interface.AddLayerCheckbox('display', 'HN NavPoints', _settings.hnLines, hnLayerToggled);
  985. WazeWrap.Interface.AddLayerCheckbox('display', 'HN NavPoints Numbers', _settings.hnNumbers, hnNumbersLayerToggled);
  986.  
  987. _HNNavPointsLayer = new OpenLayers.Layer.Vector('HN NavPoints Layer', {
  988. displayInLayerSwitcher: true,
  989. uniqueName: '__HNNavPointsLayer'
  990. });
  991. _HNNavPointsNumbersLayer = _settings.enableTooltip
  992. ? new OpenLayers.Layer.Markers('HN NavPoints Numbers Layer', navPointsNumbersLayersOptions)
  993. : new OpenLayers.Layer.Vector('HN NavPoints Numbers Layer', navPointsNumbersLayersOptions);
  994. W.map.addLayers([_HNNavPointsLayer, _HNNavPointsNumbersLayer]);
  995. _HNNavPointsLayer.setVisibility(_settings.hnLines);
  996. _HNNavPointsNumbersLayer.setVisibility(_settings.hnNumbers);
  997. window.addEventListener('beforeunload', saveSettingsToStorage, false);
  998. new WazeWrap.Interface.Shortcut(
  999. 'toggleHNNavPointsShortcut',
  1000. 'Toggle HN NavPoints layer',
  1001. 'layers',
  1002. 'layersToggleHNNavPoints',
  1003. _settings.toggleHNNavPointsShortcut,
  1004. toggleHNNavPoints,
  1005. null
  1006. ).add();
  1007. new WazeWrap.Interface.Shortcut(
  1008. 'toggleHNNavPointsNumbersShortcut',
  1009. 'Toggle HN NavPoints Numbers layer',
  1010. 'layers',
  1011. 'layersToggleHNNavPointsNumbers',
  1012. _settings.toggleHNNavPointsNumbersShortcut,
  1013. toggleHNNavPointsNumbers,
  1014. null
  1015. ).add();
  1016. const { tabLabel, tabPane } = W.userscripts.registerSidebarTab('HN-NavPoints');
  1017. tabLabel.appendChild(createElem('i', { class: 'w-icon w-icon-location', style: 'font-size:15px;padding-top:4px;' }));
  1018. tabLabel.title = _SCRIPT_SHORT_NAME;
  1019. const docFrags = document.createDocumentFragment();
  1020. docFrags.appendChild(createElem('h4', { style: 'font-weight:bold;', textContent: _SCRIPT_LONG_NAME }));
  1021. docFrags.appendChild(createElem('h6', { style: 'margin-top:0px;', textContent: _SCRIPT_VERSION }));
  1022. let divElemRoot = createElem('div', { class: 'form-group' });
  1023. divElemRoot.appendChild(buildTextBox(
  1024. 'HNNavPoints_disableBelowZoom',
  1025. 'Disable when zoom level is (<) less than:',
  1026. _settings.disableBelowZoom,
  1027. '',
  1028. 2,
  1029. 'off',
  1030. 'Disable NavPoints and house numbers when zoom level is less than specified number.\r\nMinimum: 16\r\nDefault: 17',
  1031. false
  1032. ));
  1033. divElemRoot.appendChild(buildCheckbox(
  1034. 'HNNavPoints_cbenableTooltip',
  1035. 'Enable tooltip',
  1036. _settings.enableTooltip,
  1037. 'Enable tooltip when mousing over house numbers.\r\nWarning: This may cause performance issues.',
  1038. false
  1039. ));
  1040. divElemRoot.appendChild(buildCheckbox('HNNavPoints_cbkeepHNLayerOnTop', 'Keep HN layer on top', _settings.keepHNLayerOnTop, 'Keep house numbers layer on top of all other layers.', false));
  1041. const formElem = createElem('form', { class: 'attributes-form side-panel-section' });
  1042. formElem.appendChild(divElemRoot);
  1043. docFrags.appendChild(formElem);
  1044. docFrags.appendChild(createElem('label', { class: 'control-label', textContent: 'Color legend' }));
  1045. divElemRoot = createElem('div', { style: 'margin:0 10px 0 10px; width:130px; text-align:center; font-size:12px; background:black; font-weight:600;' });
  1046. divElemRoot.appendChild(createElem('div', {
  1047. style: 'text-shadow:0 0 3px white,0 0 3px white,0 0 3px white,0 0 3px white,0 0 3px white,0 0 3px white,0 0 3px white,0 0 3px white,0 0 3px white,0 0 3px white;', textContent: 'Touched'
  1048. }));
  1049. divElemRoot.appendChild(createElem('div', {
  1050. style: 'text-shadow:0 0 3px orange,0 0 3px orange,0 0 3px orange,0 0 3px orange,0 0 3px orange,0 0 3px orange,0 0 3px orange,0 0 3px orange,0 0 3px orange,0 0 3px orange;',
  1051. textContent: 'Touched forced'
  1052. }));
  1053. divElemRoot.appendChild(createElem('div', {
  1054. style: 'text-shadow:0 0 3px yellow,0 0 3px yellow,0 0 3px yellow, 0 0 3px yellow,0 0 3px yellow,0 0 3px yellow,0 0 3px yellow,0 0 3px yellow,0 0 3px yellow,0 0 3px yellow;',
  1055. textContent: 'Untouched'
  1056. }));
  1057. divElemRoot.appendChild(createElem('div', {
  1058. style: 'text-shadow:0 0 3px red,0 0 3px red,0 0 3px red,0 0 3px red,0 0 3px red,0 0 3px red,0 0 3px red,0 0 3px red,0 0 3px red,0 0 3px red;', textContent: 'Untouched forced'
  1059. }));
  1060. docFrags.appendChild(divElemRoot);
  1061. tabPane.appendChild(docFrags);
  1062. tabPane.id = 'sidepanel-hn-navpoints';
  1063. await W.userscripts.waitForElementConnected(tabPane);
  1064. if (!_hnNavPointsTooltipDiv) {
  1065. _hnNavPointsTooltipDiv = createElem('div', {
  1066. id: 'hnNavPointsTooltipDiv',
  1067. style: 'z-index:9999; visibility:visible; position:absolute; inset: auto auto 0px 0px; margin: 0px; top: 0px; left: 0px;',
  1068. 'data-tippy-root': false
  1069. }, [{ mouseenter: checkTooltip }, { mouseleave: hideTooltipDelay }]);
  1070. W.map.getEl()[0].appendChild(_hnNavPointsTooltipDiv);
  1071. }
  1072. await initBackgroundTasks('enable');
  1073. checkLayerIndex();
  1074. log(`Fully initialized in ${Math.round(performance.now() - _LOAD_BEGIN_TIME)} ms.`);
  1075. showScriptInfoAlert();
  1076. if (_scriptActive)
  1077. processSegs('init', W.model.segments.getObjectArray().filter((o) => o.getAttribute('hasHNs')));
  1078. setTimeout(saveSettingsToStorage, 10000);
  1079. }
  1080.  
  1081. function onWmeReady(tries = 1) {
  1082. if (typeof tries === 'object')
  1083. tries = 1;
  1084. checkTimeout({ timeout: 'onWmeReady' });
  1085. if (WazeWrap?.Ready) {
  1086. logDebug('WazeWrap is ready. Proceeding with initialization.');
  1087. onWazeWrapReady();
  1088. }
  1089. else if (tries < 1000) {
  1090. logDebug(`WazeWrap is not in Ready state. Retrying ${tries} of 1000.`);
  1091. _timeouts.onWmeReady = window.setTimeout(onWmeReady, 200, ++tries);
  1092. }
  1093. else {
  1094. logError(new Error('onWmeReady timed out waiting for WazeWrap Ready state.'));
  1095. }
  1096. }
  1097.  
  1098. function onWmeInitialized() {
  1099. if (W.userscripts?.state?.isReady) {
  1100. logDebug('W is ready and already in "wme-ready" state. Proceeding with initialization.');
  1101. onWmeReady(1);
  1102. }
  1103. else {
  1104. logDebug('W is ready, but state is not "wme-ready". Adding event listener.');
  1105. document.addEventListener('wme-ready', onWmeReady, { once: true });
  1106. }
  1107. }
  1108.  
  1109. function bootstrap() {
  1110. if (!W) {
  1111. logDebug('W is not available. Adding event listener.');
  1112. document.addEventListener('wme-initialized', onWmeInitialized, { once: true });
  1113. }
  1114. else {
  1115. onWmeInitialized();
  1116. }
  1117. }
  1118.  
  1119. bootstrap();
  1120. }
  1121. )();