WME ClickSaver

Various UI changes to make editing faster and easier.

目前为 2022-12-11 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name WME ClickSaver
  3. // @namespace https://gf.qytechs.cn/users/45389
  4. // @version 2022.12.11.001
  5. // @description Various UI changes to make editing faster and easier.
  6. // @author MapOMatic
  7. // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
  8. // @license GNU GPLv3
  9. // @connect sheets.googleapis.com
  10. // @contributionURL https://github.com/WazeDev/Thank-The-Authors
  11. // @grant GM_xmlhttpRequest
  12. // @grant GM_addElement
  13. // @require https://gf.qytechs.cn/scripts/24851-wazewrap/code/WazeWrap.js
  14. // ==/UserScript==
  15.  
  16. /* global GM_info */
  17. /* global W */
  18. /* global I18n */
  19. /* global OL */
  20. /* global $ */
  21. /* global WazeWrap */
  22. /* global GM_addElement */
  23.  
  24. (function() {
  25. 'use strict';
  26.  
  27. const UPDATE_MESSAGE = '';
  28.  
  29. const SCRIPT_NAME = GM_info.script.name;
  30. const SCRIPT_VERSION = GM_info.script.version;
  31. const FORUM_URL = 'https://www.waze.com/forum/viewtopic.php?f=819&t=199894';
  32. const TRANSLATIONS_URL = 'https://sheets.googleapis.com/v4/spreadsheets/1ZlE9yhNncP9iZrPzFFa-FCtYuK58wNOEcmKqng4sH1M/values/ClickSaver';
  33. const API_KEY = 'YTJWNVBVRkplbUZUZVVGMFl6aFVjMjVOTW0wNU5GaG5kVE40TUZoNWJVZEhWbU5rUjNacVdtdFlWUT09';
  34. const DEC = s => atob(atob(s));
  35. const EXTERNAL_SETTINGS = {
  36. toggleTwoWaySegDrawingShortcut: null
  37. };
  38. const EXTERNAL_SETTINGS_NAME = 'clicksaver_settings_ext';
  39.  
  40. // This function is injected into the page.
  41. function main(argsObject) {
  42. /* eslint-disable object-curly-newline */
  43. const ROAD_TYPE_DROPDOWN_SELECTOR = 'wz-select[name="roadType"]';
  44. const ROAD_TYPE_CHIP_SELECTOR = 'wz-chip-select[class="road-type-chip-select"]';
  45. const PARKING_SPACES_DROPDOWN_SELECTOR = 'select[name="estimatedNumberOfSpots"]';
  46. const PARKING_COST_DROPDOWN_SELECTOR = 'select[name="costType"]';
  47. const SETTINGS_STORE_NAME = 'clicksaver_settings';
  48. const DEFAULT_TRANSLATION = {
  49. roadTypeButtons: {
  50. St: { text: 'St' },
  51. PS: { text: 'PS' },
  52. mH: { text: 'mH' },
  53. MH: { text: 'MH' },
  54. Fw: { text: 'Fw' },
  55. Rmp: { text: 'Rmp' },
  56. OR: { text: 'OR' },
  57. PLR: { text: 'PLR' },
  58. PR: { text: 'PR' },
  59. Fer: { text: 'Fer' },
  60. WT: { text: 'WT' },
  61. PB: { text: 'PB' },
  62. Sw: { text: 'Sw' },
  63. RR: { text: 'RR' },
  64. RT: { text: 'RT' },
  65. Pw: { text: 'Pw' }
  66. },
  67. multiLockLevelWarning: 'Multiple lock levels selected!',
  68. prefs: {
  69. dropdownHelperGroup: 'DROPDOWN HELPERS',
  70. roadTypeButtons: 'Add road type buttons',
  71. useOldRoadColors: 'Use old road colors (requires refresh)',
  72. setStreetCityToNone: 'Set Street/City to None (new seg\'s only)',
  73. setStreetCityToNone_Title: 'NOTE: Only works if connected directly or indirectly'
  74. + ' to a segment with State / Country already set.',
  75. setCityToConnectedSegCity: 'Set City to connected segment\'s City',
  76. parkingCostButtons: 'Add PLA cost buttons',
  77. parkingSpacesButtons: 'Add PLA estimated spaces buttons',
  78. timeSaversGroup: 'TIME SAVERS',
  79. discussionForumLinkText: 'Discussion Forum',
  80. showAddAltCityButton: 'Show "Add alt city" button',
  81. showSwapDrivingWalkingButton: 'Show "Swap driving<->walking segment type" button',
  82. showSwapDrivingWalkingButton_Title: 'Swap between driving-type and walking-type segments. WARNING! This will DELETE and recreate the segment. Nodes may need to be reconnected.'
  83. },
  84. swapSegmentTypeWarning: 'This will DELETE the segment and recreate it. Any speed data will be lost, and nodes will need to be reconnected. This message will only be displayed once. Continue?',
  85. swapSegmentTypeError_Paths: 'Paths must be removed from segment before changing between driving and pedestrian road type.',
  86. addAltCityButtonText: 'Add alt city'
  87. };
  88. const ROAD_TYPES = {
  89. St: { val: 1, wmeColor: '#ffffeb', svColor: '#ffffff', category: 'streets', visible: true },
  90. PS: { val: 2, wmeColor: '#f0ea58', svColor: '#cba12e', category: 'streets', visible: true },
  91. Pw: { val: 22, wmeColor: '#64799a', svColor: '#64799a', category: 'streets', visible: false },
  92. mH: { val: 7, wmeColor: '#69bf88', svColor: '#ece589', category: 'highways', visible: true },
  93. MH: { val: 6, wmeColor: '#45b8d1', svColor: '#c13040', category: 'highways', visible: true },
  94. Fw: { val: 3, wmeColor: '#c577d2', svColor: '#387fb8', category: 'highways', visible: false },
  95. Rmp: { val: 4, wmeColor: '#b3bfb3', svColor: '#58c53b', category: 'highways', visible: false },
  96. OR: { val: 8, wmeColor: '#867342', svColor: '#82614a', category: 'otherDrivable', visible: false },
  97. PLR: { val: 20, wmeColor: '#ababab', svColor: '#2282ab', category: 'otherDrivable', visible: true },
  98. PR: { val: 17, wmeColor: '#beba6c', svColor: '#00ffb3', category: 'otherDrivable', visible: true },
  99. Fer: { val: 15, wmeColor: '#d7d8f8', svColor: '#ff8000', category: 'otherDrivable', visible: false },
  100. RR: { val: 18, wmeColor: '#c62925', svColor: '#ffffff', category: 'nonDrivable', visible: false },
  101. RT: { val: 19, wmeColor: '#ffffff', svColor: '#00ff00', category: 'nonDrivable', visible: false },
  102. WT: { val: 5, wmeColor: '#b0a790', svColor: '#00ff00', category: 'pedestrian', visible: false },
  103. PB: { val: 10, wmeColor: '#9a9a9a', svColor: '#0000ff', category: 'pedestrian', visible: false },
  104. Sw: { val: 16, wmeColor: '#999999', svColor: '#b700ff', category: 'pedestrian', visible: false }
  105. };
  106.  
  107. /* eslint-enable object-curly-newline */
  108. let _settings = {};
  109. let _trans; // Translation object
  110.  
  111. // Do not make these const values. They may get assigned before require() is defined. Trust me. Don't do it.
  112. let UpdateObject;
  113. let UpdateFeatureAddress;
  114. let MultiAction;
  115. let AddSeg;
  116. let Segment;
  117. let DelSeg;
  118.  
  119. // function log(message) {
  120. // console.log('ClickSaver:', message);
  121. // }
  122.  
  123. function logDebug(message) {
  124. console.debug('ClickSaver:', message);
  125. }
  126.  
  127. // function logWarning(message) {
  128. // console.warn('ClickSaver:', message);
  129. // }
  130.  
  131. // function logError(message) {
  132. // console.error('ClickSaver:', message);
  133. // }
  134.  
  135. function isChecked(checkboxId) {
  136. return $(`#${checkboxId}`).is(':checked');
  137. }
  138.  
  139. function isSwapPedestrianPermitted() {
  140. const { user } = W.loginManager;
  141. const rank = user.rank + 1;
  142. return rank >= 4 || (rank === 3 && user.isAreaManager);
  143. }
  144.  
  145. function setChecked(checkboxId, checked) {
  146. $(`#${checkboxId}`).prop('checked', checked);
  147. }
  148. function loadSettingsFromStorage() {
  149. const loadedSettings = $.parseJSON(localStorage.getItem(SETTINGS_STORE_NAME));
  150. const defaultSettings = {
  151. lastVersion: null,
  152. roadButtons: true,
  153. roadTypeButtons: ['St', 'PS', 'mH', 'MH', 'Fw', 'Rmp', 'PLR', 'PR', 'PB'],
  154. parkingCostButtons: true,
  155. parkingSpacesButtons: true,
  156. setNewPLRStreetToNone: true,
  157. setNewPLRCity: true,
  158. setNewPRStreetToNone: false,
  159. setNewPRCity: false,
  160. setNewRRStreetToNone: true, // added by jm6087
  161. setNewRRCity: false, // added by jm6087
  162. setNewPBStreetToNone: true, // added by jm6087
  163. setNewPBCity: true, // added by jm6087
  164. setNewORStreetToNone: false,
  165. setNewORCity: false,
  166. addAltCityButton: true,
  167. addSwapPedestrianButton: false,
  168. useOldRoadColors: false,
  169. warnOnPedestrianTypeSwap: true
  170. };
  171. _settings = loadedSettings || defaultSettings;
  172. Object.keys(defaultSettings).forEach(prop => {
  173. if (!_settings.hasOwnProperty(prop)) {
  174. _settings[prop] = defaultSettings[prop];
  175. }
  176. });
  177.  
  178. setChecked('csRoadTypeButtonsCheckBox', _settings.roadButtons);
  179. if (_settings.roadTypeButtons) {
  180. Object.keys(ROAD_TYPES).forEach(roadTypeAbbr1 => {
  181. setChecked(`cs${roadTypeAbbr1}CheckBox`, _settings.roadTypeButtons.indexOf(roadTypeAbbr1) !== -1);
  182. });
  183. }
  184.  
  185. if (_settings.roadButtons) {
  186. $('.csRoadTypeButtonsCheckBoxContainer').show();
  187. } else {
  188. $('.csRoadTypeButtonsCheckBoxContainer').hide();
  189. }
  190. setChecked('csParkingSpacesButtonsCheckBox', _settings.parkingSpacesButtons);
  191. setChecked('csParkingCostButtonsCheckBox', _settings.parkingCostButtons);
  192. setChecked('csSetNewPLRCityCheckBox', _settings.setNewPLRCity);
  193. setChecked('csClearNewPLRCheckBox', _settings.setNewPLRStreetToNone);
  194. setChecked('csSetNewPRCityCheckBox', _settings.setNewPRCity);
  195. setChecked('csClearNewPRCheckBox', _settings.setNewPRStreetToNone);
  196. setChecked('csSetNewRRCityCheckBox', _settings.setNewRRCity);
  197. setChecked('csClearNewRRCheckBox', _settings.setNewRRStreetToNone); // added by jm6087
  198. setChecked('csSetNewPBCityCheckBox', _settings.setNewPBCity);
  199. setChecked('csClearNewPBCheckBox', _settings.setNewPBStreetToNone); // added by jm6087
  200. setChecked('csSetNewORCityCheckBox', _settings.setNewORCity);
  201. setChecked('csClearNewORCheckBox', _settings.setNewORStreetToNone);
  202. setChecked('csUseOldRoadColorsCheckBox', _settings.useOldRoadColors);
  203. setChecked('csAddAltCityButtonCheckBox', _settings.addAltCityButton);
  204. setChecked('csAddSwapPedestrianButtonCheckBox', _settings.addSwapPedestrianButton);
  205. }
  206.  
  207. function saveSettingsToStorage() {
  208. if (localStorage) {
  209. const settings = {
  210. lastVersion: argsObject.scriptVersion,
  211. roadButtons: _settings.roadButtons,
  212. parkingCostButtons: _settings.parkingCostButtons,
  213. parkingSpacesButtons: _settings.parkingSpacesButtons,
  214. setNewPLRCity: _settings.setNewPLRCity,
  215. setNewPLRStreetToNone: _settings.setNewPLRStreetToNone,
  216. setNewPRCity: _settings.setNewPRCity,
  217. setNewPRStreetToNone: _settings.setNewPRStreetToNone,
  218. setNewRRCity: _settings.setNewRRCity,
  219. setNewRRStreetToNone: _settings.setNewRRStreetToNone, // added by jm6087
  220. setNewPBCity: _settings.setNewPBCity,
  221. setNewPBStreetToNone: _settings.setNewPBStreetToNone, // added by jm6087
  222. setNewORCity: _settings.setNewORCity,
  223. setNewORStreetToNone: _settings.setNewORStreetToNone,
  224. useOldRoadColors: _settings.useOldRoadColors,
  225. addAltCityButton: _settings.addAltCityButton,
  226. addSwapPedestrianButton: _settings.addSwapPedestrianButton,
  227. warnOnPedestrianTypeSwap: _settings.warnOnPedestrianTypeSwap
  228. };
  229. settings.roadTypeButtons = [];
  230. Object.keys(ROAD_TYPES).forEach(roadTypeAbbr => {
  231. if (_settings.roadTypeButtons.indexOf(roadTypeAbbr) !== -1) {
  232. settings.roadTypeButtons.push(roadTypeAbbr);
  233. }
  234. });
  235. localStorage.setItem(SETTINGS_STORE_NAME, JSON.stringify(settings));
  236. logDebug('Settings saved');
  237. }
  238. }
  239.  
  240. function isPedestrianTypeSegment(segment) {
  241. return [5, 10, 16].includes(segment.attributes.roadType);
  242. }
  243.  
  244. function getConnectedSegmentIDs(segmentID) {
  245. const IDs = [];
  246. const segment = W.model.segments.getObjectById(segmentID);
  247. [
  248. W.model.nodes.getObjectById(segment.attributes.fromNodeID),
  249. W.model.nodes.getObjectById(segment.attributes.toNodeID)
  250. ].forEach(node => {
  251. if (node) {
  252. node.attributes.segIDs.forEach(segID => {
  253. if (segID !== segmentID) { IDs.push(segID); }
  254. });
  255. }
  256. });
  257. return IDs;
  258. }
  259.  
  260. function getFirstConnectedSegmentAddress(startSegment) {
  261. const nonMatches = [];
  262. const segmentIDsToSearch = [startSegment.getID()];
  263. while (segmentIDsToSearch.length > 0) {
  264. const startSegmentID = segmentIDsToSearch.pop();
  265. startSegment = W.model.segments.getObjectById(startSegmentID);
  266. const connectedSegmentIDs = getConnectedSegmentIDs(startSegmentID);
  267. for (let i = 0; i < connectedSegmentIDs.length; i++) {
  268. const addr = W.model.segments.getObjectById(connectedSegmentIDs[i]).getAddress();
  269. if (!addr.isEmpty()) {
  270. return addr;
  271. }
  272. }
  273.  
  274. nonMatches.push(startSegmentID);
  275. connectedSegmentIDs.forEach(segmentID => {
  276. if (nonMatches.indexOf(segmentID) === -1 && segmentIDsToSearch.indexOf(segmentID) === -1) {
  277. segmentIDsToSearch.push(segmentID);
  278. }
  279. });
  280. }
  281. return undefined;
  282. }
  283.  
  284. function setStreetAndCity(setCity) {
  285. const segments = W.selectionManager.getSelectedFeatures();
  286. if (segments.length === 0 || segments[0].model.type !== 'segment') {
  287. return;
  288. }
  289.  
  290. const mAction = new MultiAction();
  291. mAction.setModel(W.model);
  292. segments.forEach(segment => {
  293. const segModel = segment.model;
  294. if (segModel.attributes.primaryStreetID === null) {
  295. const addr = getFirstConnectedSegmentAddress(segModel);
  296. if (addr && !addr.isEmpty()) {
  297. const cityNameToSet = setCity && !addr.getCity().isEmpty() ? addr.getCityName() : '';
  298. const action = new UpdateFeatureAddress(segModel, {
  299. countryID: addr.getCountry().id,
  300. stateID: addr.getState().id,
  301. cityName: cityNameToSet,
  302. emptyStreet: true,
  303. emptyCity: !setCity
  304. }, { streetIDField: 'primaryStreetID' });
  305. mAction.doSubAction(action);
  306. }
  307. }
  308. });
  309. const count = mAction.subActions.length;
  310. if (count) {
  311. mAction._description = `Updated address on ${count} segment${count > 1 ? 's' : ''}`;
  312. W.model.actionManager.add(mAction);
  313. }
  314. }
  315.  
  316. function waitForElem(selector, callback) {
  317. const elem = document.querySelector(selector);
  318. setTimeout(() => {
  319. if (!elem) {
  320. waitForElem(selector, callback);
  321. } else {
  322. callback(elem);
  323. }
  324. }, 10);
  325. }
  326.  
  327. function waitForShadowElem(parentElemSelector, shadowElemSelector, callback) {
  328. setTimeout(() => {
  329. const parentElem = document.querySelector(parentElemSelector);
  330. const sRoot = parentElem ? parentElem.shadowRoot : null;
  331. const shadowElem = sRoot ? sRoot.querySelector(shadowElemSelector) : null;
  332. if (!shadowElem) {
  333. waitForShadowElem(parentElemSelector, shadowElemSelector, callback);
  334. } else {
  335. callback(shadowElem, parentElem);
  336. }
  337. }, 10);
  338. }
  339.  
  340. // eslint-disable-next-line no-unused-vars
  341. function onAddAltCityButtonClick() {
  342. const streetID = W.selectionManager.getSelectedFeatures()[0].model.attributes.primaryStreetID;
  343. $('wz-button[class="add-alt-street-btn"]').click();
  344. waitForElem('wz-autocomplete.alt-street-name', elem => {
  345. elem.focus();
  346. waitForShadowElem('wz-autocomplete.alt-street-name', `wz-menu-item[item-id="${streetID}"]`, shadowElem => {
  347. shadowElem.click();
  348. const emptyCityCheckbox = $('wz-checkbox.empty-city');
  349. if (emptyCityCheckbox[0].checked) { emptyCityCheckbox.click(); }
  350. waitForShadowElem('wz-autocomplete.alt-city-name', 'wz-text-input', (cityTextElem, cityAutocompleteElem) => {
  351. cityTextElem.value = null;
  352. cityAutocompleteElem.focus();
  353. });
  354. });
  355. });
  356. }
  357.  
  358. function onRoadTypeButtonClick(roadTypeVal) {
  359. const segments = W.selectionManager.getSelectedFeatures();
  360. let action;
  361. if (segments.length > 1) {
  362. action = new MultiAction();
  363. action.setModel(W.model);
  364. segments.forEach(segment => {
  365. const subAction = new UpdateObject(segment.model, { roadType: roadTypeVal });
  366. action.doSubAction(subAction);
  367. });
  368. action._description = I18n.t(
  369. 'save.changes_log.actions.UpdateObject.changed',
  370. {
  371. propertyName: I18n.t('objects.segment.fields.roadType'),
  372. objectsString: I18n.t('objects.segment.multi', { count: segments.length }),
  373. value: I18n.t('segment.road_types')[roadTypeVal]
  374. }
  375. );
  376. } else {
  377. action = new UpdateObject(segments[0].model, { roadType: roadTypeVal });
  378. }
  379. W.model.actionManager.add(action);
  380.  
  381. if (roadTypeVal === 20 && isChecked('csClearNewPLRCheckBox') && typeof require !== 'undefined') {
  382. setStreetAndCity(isChecked('csSetNewPLRCityCheckBox'));
  383. } else if (roadTypeVal === 17 && isChecked('csClearNewPRCheckBox') && typeof require !== 'undefined') {
  384. setStreetAndCity(isChecked('csSetNewPRCityCheckBox'));
  385. } else if (roadTypeVal === 18 && isChecked('csClearNewRRCheckBox') && typeof require !== 'undefined') { // added by jm6087
  386. setStreetAndCity(isChecked('csSetNewRRCityCheckBox')); // added by jm6087
  387. } else if (roadTypeVal === 10 && isChecked('csClearNewPBCheckBox') && typeof require !== 'undefined') { // added by jm6087
  388. setStreetAndCity(isChecked('csSetNewPBCityCheckBox')); // added by jm6087
  389. } else if (roadTypeVal === 8 && isChecked('csClearNewORCheckBox') && typeof require !== 'undefined') {
  390. setStreetAndCity(isChecked('csSetNewORCityCheckBox'));
  391. }
  392. }
  393.  
  394. function addRoadTypeButtons() {
  395. const seg = W.selectionManager.getSelectedFeatures()[0].model;
  396. if (seg.type !== 'segment') return;
  397. const isPed = isPedestrianTypeSegment(seg);
  398. const $dropDown = $(ROAD_TYPE_DROPDOWN_SELECTOR);
  399. $('#csRoadTypeButtonsContainer').remove();
  400. const $container = $('<div>', { id: 'csRoadTypeButtonsContainer', class: 'cs-rt-buttons-container' });
  401. const $street = $('<div>', { id: 'csStreetButtonContainer', class: 'cs-rt-buttons-group' });
  402. const $highway = $('<div>', { id: 'csHighwayButtonContainer', class: 'cs-rt-buttons-group' });
  403. const $otherDrivable = $('<div>', { id: 'csOtherDrivableButtonContainer', class: 'cs-rt-buttons-group' });
  404. const $nonDrivable = $('<div>', { id: 'csNonDrivableButtonContainer', class: 'cs-rt-buttons-group' });
  405. const $pedestrian = $('<div>', { id: 'csPedestrianButtonContainer', class: 'cs-rt-buttons-group' });
  406. const divs = {
  407. streets: $street,
  408. highways: $highway,
  409. otherDrivable: $otherDrivable,
  410. nonDrivable: $nonDrivable,
  411. pedestrian: $pedestrian
  412. };
  413. Object.keys(ROAD_TYPES).forEach(roadTypeKey => {
  414. if (_settings.roadTypeButtons.includes(roadTypeKey)) {
  415. const roadType = ROAD_TYPES[roadTypeKey];
  416. const isDisabled = $dropDown[0].hasAttribute('disabled') && $dropDown[0].getAttribute('disabled') === 'true';
  417. if (!isDisabled && ((roadType.category === 'pedestrian' && isPed) || (roadType.category !== 'pedestrian' && !isPed))) {
  418. const $div = divs[roadType.category];
  419. $div.append(
  420. $('<div>', {
  421. class: `btn cs-rt-button cs-rt-button-${roadTypeKey} btn-positive`,
  422. title: I18n.t('segment.road_types')[roadType.val]
  423. })
  424. .text(_trans.roadTypeButtons[roadTypeKey].text)
  425. .prop('checked', roadType.visible)
  426. .data('val', roadType.val)
  427. .click(function rtbClick() { onRoadTypeButtonClick($(this).data('key')); })
  428. );
  429. }
  430. }
  431. });
  432. if (isPed) {
  433. $container.append($pedestrian);
  434. } else {
  435. $container.append($street).append($highway).append($otherDrivable).append($nonDrivable);
  436. }
  437. $dropDown.before($container);
  438. }
  439.  
  440. // Function to add an event listener to the chip select for the road type in compact mode
  441. function addCompactRoadTypeChangeEvents() {
  442. const chipSelect = document.getElementsByClassName('road-type-chip-select')[0];
  443. chipSelect.addEventListener('chipSelected', evt => {
  444. const rtValue = evt.detail.value;
  445. onRoadTypeButtonClick(rtValue);
  446. });
  447. }
  448.  
  449. function isPLA(item) {
  450. return (item.model.type === 'venue') && item.model.attributes.categories.includes('PARKING_LOT');
  451. }
  452.  
  453. function addParkingSpacesButtons() {
  454. const $dropDown = $(PARKING_SPACES_DROPDOWN_SELECTOR);
  455. const selItems = W.selectionManager.getSelectedFeatures();
  456. const item = selItems[0];
  457.  
  458. // If it's not a PLA, exit.
  459. if (!isPLA(item)) return;
  460.  
  461. $('#csParkingSpacesContainer').remove();
  462. const $div = $('<div>', { id: 'csParkingSpacesContainer' });
  463. const dropdownDisabled = $dropDown.attr('disabled') === 'disabled';
  464. const optionNodes = $(`${PARKING_SPACES_DROPDOWN_SELECTOR} option`);
  465.  
  466. for (let i = 0; i < optionNodes.length; i++) {
  467. const $option = $(optionNodes[i]);
  468. const text = $option.text();
  469. const selected = $option.val() === $dropDown.val();
  470. $div.append(
  471. // TODO css
  472. $('<div>', {
  473. class: `btn waze-btn waze-btn-white${selected ? ' waze-btn-blue' : ''}${dropdownDisabled ? ' disabled' : ''}`,
  474. style: 'margin-bottom: 5px; height: 22px; padding: 2px 8px 0px 8px; margin-right: 3px;'
  475. })
  476. .text(text)
  477. .data('val', $option.val())
  478. // eslint-disable-next-line func-names
  479. .hover(() => { })
  480. .click(function onParkingSpacesButtonClick() {
  481. if (!dropdownDisabled) {
  482. $(PARKING_SPACES_DROPDOWN_SELECTOR).val($(this).data('val')).change();
  483. addParkingSpacesButtons();
  484. }
  485. })
  486. );
  487. }
  488.  
  489. $dropDown.before($div);
  490. $dropDown.hide();
  491. }
  492.  
  493. function addParkingCostButtons() {
  494. const $dropDown = $(PARKING_COST_DROPDOWN_SELECTOR);
  495. const selItems = W.selectionManager.getSelectedFeatures();
  496. const item = selItems[0];
  497.  
  498. // If it's not a PLA, exit.
  499. if (!isPLA(item)) return;
  500.  
  501. $('#csParkingCostContainer').remove();
  502. const $div = $('<div>', { id: 'csParkingCostContainer' });
  503. const dropdownDisabled = $dropDown.attr('disabled') === 'disabled';
  504. const optionNodes = $(`${PARKING_COST_DROPDOWN_SELECTOR} option`);
  505. for (let i = 0; i < optionNodes.length; i++) {
  506. const $option = $(optionNodes[i]);
  507. const text = $option.text();
  508. const selected = $option.val() === $dropDown.val();
  509. $div.append(
  510. $('<div>', {
  511. class: `btn waze-btn waze-btn-white${selected ? ' waze-btn-blue' : ''}${dropdownDisabled ? ' disabled' : ''}`,
  512. // TODO css
  513. style: 'margin-bottom: 5px; height: 22px; padding: 2px 8px 0px 8px; margin-right: 4px;'
  514. })
  515. .text(text !== '' ? text : '?')
  516. .data('val', $option.val())
  517. // eslint-disable-next-line func-names
  518. .hover(() => { })
  519. .click(function onParkingCostButtonClick() {
  520. if (!dropdownDisabled) {
  521. $(PARKING_COST_DROPDOWN_SELECTOR).val($(this).data('val')).change();
  522. addParkingCostButtons();
  523. }
  524. })
  525. );
  526. }
  527.  
  528. $dropDown.before($div);
  529. $dropDown.hide();
  530. }
  531.  
  532. function addAddAltCityButton() {
  533. const selFeatures = W.selectionManager.getSelectedFeatures();
  534. const streetID = selFeatures[0].model.attributes.primaryStreetID;
  535. // Only show the button if every segment has the same primary city and street.
  536. if (selFeatures.length > 1 && !selFeatures.every(f => f.model.attributes.primaryStreetID === streetID)) return;
  537.  
  538. const id = 'csAddAltCityButton';
  539. if (selFeatures[0].model.type === 'segment' && $(`#${id}`).length === 0) {
  540. $('div.address-edit').prev('wz-label').append(
  541. $('<a>', {
  542. href: '#',
  543. // TODO css
  544. style: 'float: right;text-transform: none;'
  545. + 'font-family: "Helvetica Neue", Helvetica, "Open Sans", sans-serif;color: #26bae8;'
  546. + 'font-weight: normal;'
  547. }).text(_trans.addAltCityButtonText).click(onAddAltCityButtonClick)
  548. );
  549. }
  550. }
  551.  
  552. function addSwapPedestrianButton(displayMode) { // Added displayMode argument to identify compact vs. regular mode.
  553. const id = 'csSwapPedestrianContainer';
  554. $(`#${id}`).remove();
  555. const selectedFeatures = W.selectionManager.getSelectedFeatures();
  556. if (selectedFeatures.length === 1 && selectedFeatures[0].model.type === 'segment') {
  557. // TODO css
  558. const $container = $('<div>', { id, style: 'white-space: nowrap;float: right;display: inline;' });
  559. const $button = $('<div>', {
  560. id: 'csBtnSwapPedestrianRoadType',
  561. title: '',
  562. // TODO css
  563. style: 'display:inline-block;cursor:pointer;'
  564. });
  565. $button.append('<i class="fa fa-blind fa-lg"></i><i class="fa fa-arrows-h fa-lg" style="color:#e84545"></i><i class="fa fa-car fa-lg"></i>')
  566. .attr({
  567. title: _trans.prefs.showSwapDrivingWalkingButton_Title
  568. });
  569. $container.append($button);
  570.  
  571. // Insert swap button in the correct location based on display mode.
  572. if (displayMode === 'compact') {
  573. const $label = $('wz-chip-select[class="road-type-chip-select"]').parent().find('wz-label');
  574. $label.css({ display: 'inline' }).before($container);
  575. } else {
  576. const $label = $('wz-select[name="roadType"]').closest('form').find('wz-label').first();
  577. $label.css({ display: 'inline' }).after($container);
  578. }
  579. // TODO css
  580.  
  581. $('#csBtnSwapPedestrianRoadType').click(() => {
  582. if (_settings.warnOnPedestrianTypeSwap) {
  583. _settings.warnOnPedestrianTypeSwap = false;
  584. saveSettingsToStorage();
  585. if (!confirm(_trans.swapSegmentTypeWarning)) {
  586. return;
  587. }
  588. }
  589.  
  590. // Check for paths before deleting.
  591. let segment = W.selectionManager.getSelectedFeatures()[0];
  592. if (segment.model.hasPaths()) {
  593. WazeWrap.Alerts.error('Clicksaver', _trans.swapSegmentTypeError_Paths);
  594. return;
  595. }
  596.  
  597. // delete the selected segment
  598. const oldGeom = segment.geometry.clone();
  599. W.model.actionManager.add(new DelSeg(segment.model));
  600.  
  601. // create the replacement segment in the other segment type (pedestrian -> road & vice versa)
  602. // Note: this doesn't work in a MultiAction for some reason.
  603. const newRoadType = isPedestrianTypeSegment(segment.model) ? 1 : 5;
  604. segment = new Segment({ geometry: oldGeom, roadType: newRoadType });
  605. segment.state = OL.State.INSERT;
  606. W.model.actionManager.add(new AddSeg(segment, {
  607. createNodes: !0,
  608. openAllTurns: W.prefs.get('enableTurnsByDefault'),
  609. createTwoWay: W.prefs.get('twoWaySegmentsByDefault'),
  610. snappedFeatures: [null, null]
  611. }));
  612. const newId = W.model.repos.segments.idGenerator.lastValue;
  613. const newSegment = W.model.segments.getObjectById(newId);
  614. W.selectionManager.setSelectedModels([newSegment]);
  615. });
  616. }
  617. }
  618.  
  619. /* eslint-disable no-bitwise, no-mixed-operators */
  620. function shadeColor2(color, percent) {
  621. const f = parseInt(color.slice(1), 16);
  622. const t = percent < 0 ? 0 : 255;
  623. const p = percent < 0 ? percent * -1 : percent;
  624. const R = f >> 16;
  625. const G = f >> 8 & 0x00FF;
  626. const B = f & 0x0000FF;
  627. return `#${(0x1000000 + (Math.round((t - R) * p) + R) * 0x10000 + (Math.round((t - G) * p) + G)
  628. * 0x100 + (Math.round((t - B) * p) + B)).toString(16).slice(1)}`;
  629. }
  630. /* eslint-enable no-bitwise, no-mixed-operators */
  631.  
  632. function buildRoadTypeButtonCss() {
  633. const lines = [];
  634. const useOldColors = _settings.useOldRoadColors;
  635. Object.keys(ROAD_TYPES).forEach(roadTypeAbbr => {
  636. const roadType = ROAD_TYPES[roadTypeAbbr];
  637. const bgColor = useOldColors ? roadType.svColor : roadType.wmeColor;
  638. let output = `.cs-rt-buttons-container .cs-rt-button-${roadTypeAbbr} {background-color:${
  639. bgColor};box-shadow:0 2px ${shadeColor2(bgColor, -0.5)};border-color:${shadeColor2(bgColor, -0.15)};}`;
  640. output += ` .cs-rt-buttons-container .cs-rt-button-${roadTypeAbbr}:hover {background-color:${
  641. shadeColor2(bgColor, 0.2)}}`;
  642. lines.push(output);
  643. });
  644. return lines.join(' ');
  645. }
  646.  
  647. function injectCss() {
  648. const css = [
  649. // Road type button formatting
  650. '.csRoadTypeButtonsCheckBoxContainer {margin-left:15px;}',
  651. '.cs-rt-buttons-container {margin-bottom:5px;height:21px;}',
  652. '.cs-rt-buttons-container .cs-rt-button {font-size:11px;line-height:20px;color:black;padding:0px 4px;height:20px;'
  653. + 'margin-right:2px;border-style:solid;border-width:1px;}',
  654. buildRoadTypeButtonCss(),
  655. '.btn.cs-rt-button:active {box-shadow:none;transform:translateY(2px)}',
  656. 'div .cs-rt-buttons-group {float:left; margin: 0px 5px 5px 0px;}',
  657. '#sidepanel-clicksaver .controls-container {padding:0px;}',
  658. '#sidepanel-clicksaver .controls-container label {white-space: normal;}',
  659.  
  660. // Lock button formatting
  661. '.cs-group-label {font-size: 11px; width: 100%; font-family: Poppins, sans-serif;'
  662. + ' text-transform: uppercase; font-weight: 700; color: #354148; margin-bottom: 6px;}'
  663. ].join(' ');
  664. $(`<style type="text/css">${css}</style>`).appendTo('head');
  665. }
  666.  
  667. function createSettingsCheckbox(id, settingName, labelText, titleText, divCss, labelCss, optionalAttributes) {
  668. const $container = $('<div>', { class: 'controls-container' });
  669. const $input = $('<input>', {
  670. type: 'checkbox', class: 'csSettingsCheckBox', name: id, id, 'data-setting-name': settingName
  671. }).appendTo($container);
  672. const $label = $('<label>', { for: id }).text(labelText).appendTo($container);
  673. // TODO css
  674. if (divCss) $container.css(divCss);
  675. // TODO css
  676. if (labelCss) $label.css(labelCss);
  677. if (titleText) $container.attr({ title: titleText });
  678. if (optionalAttributes) $input.attr(optionalAttributes);
  679. return $container;
  680. }
  681.  
  682. function initUserPanel() {
  683. const $roadTypesDiv = $('<div>', { class: 'csRoadTypeButtonsCheckBoxContainer' });
  684. $roadTypesDiv.append(
  685. createSettingsCheckbox('csUseOldRoadColorsCheckBox', 'useOldRoadColors', _trans.prefs.useOldRoadColors)
  686. );
  687. Object.keys(ROAD_TYPES).forEach(roadTypeAbbr => {
  688. const roadType = ROAD_TYPES[roadTypeAbbr];
  689. const id = `cs${roadTypeAbbr}CheckBox`;
  690. const title = I18n.t('segment.road_types')[roadType.val];
  691. $roadTypesDiv.append(
  692. createSettingsCheckbox(id, 'roadType', title, null, null, null, {
  693. 'data-road-type': roadTypeAbbr
  694. })
  695. );
  696. if (['PLR', 'PR', 'RR', 'PB', 'OR'].includes(roadTypeAbbr)) { // added RR & PB by jm6087
  697. $roadTypesDiv.append(
  698. // TODO css
  699. createSettingsCheckbox(`csClearNew${roadTypeAbbr}CheckBox`, `setNew${roadTypeAbbr}StreetToNone`,
  700. _trans.prefs.setStreetCityToNone, _trans.prefs.setStreetCityToNone_Title,
  701. { paddingLeft: '20px', marginRight: '4px' }, { fontStyle: 'italic' }),
  702. createSettingsCheckbox(`csSetNew${roadTypeAbbr}CityCheckBox`, `setNew${roadTypeAbbr}City`,
  703. _trans.prefs.setCityToConnectedSegCity, '',
  704. { paddingLeft: '30px', marginRight: '4px' }, { fontStyle: 'italic' })
  705. );
  706. }
  707. });
  708.  
  709. const $tab = $('<li>', { title: argsObject.scriptName }).append(
  710. $('<a>', { 'data-toggle': 'tab', href: '#sidepanel-clicksaver' }).append($('<span>').text('CS'))
  711. );
  712.  
  713. const $panel = $('<div>', { class: 'tab-pane', id: 'sidepanel-clicksaver' }).append(
  714. $('<div>', { class: 'side-panel-section>' }).append(
  715. // TODO css
  716. $('<div>', { style: 'margin-bottom:8px;' }).append(
  717. $('<div>', { class: 'form-group' }).append(
  718. $('<label>', { class: 'cs-group-label' }).text(_trans.prefs.dropdownHelperGroup),
  719. $('<div>').append(
  720. createSettingsCheckbox('csRoadTypeButtonsCheckBox', 'roadButtons',
  721. _trans.prefs.roadTypeButtons)
  722. ).append($roadTypesDiv),
  723. createSettingsCheckbox('csParkingCostButtonsCheckBox', 'parkingCostButtons',
  724. _trans.prefs.parkingCostButtons),
  725. createSettingsCheckbox('csParkingSpacesButtonsCheckBox', 'parkingSpacesButtons',
  726. _trans.prefs.parkingSpacesButtons)
  727. ),
  728. $('<label>', { class: 'cs-group-label' }).text(_trans.prefs.timeSaversGroup),
  729. $('<div>', { style: 'margin-bottom:8px;' }).append(
  730. // THIS IS CURRENTLY DISABLED
  731. createSettingsCheckbox('csAddAltCityButtonCheckBox', 'addAltCityButton',
  732. _trans.prefs.showAddAltCityButton),
  733. isSwapPedestrianPermitted() ? createSettingsCheckbox('csAddSwapPedestrianButtonCheckBox',
  734. 'addSwapPedestrianButton', _trans.prefs.showSwapDrivingWalkingButton) : ''
  735. )
  736. )
  737. )
  738. );
  739.  
  740. $panel.append(
  741. // TODO css
  742. $('<div>', { style: 'margin-top:20px;font-size:10px;color:#999999;' }).append(
  743. $('<div>').text(`version ${argsObject.scriptVersion}${argsObject.scriptName.toLowerCase().includes('beta') ? ' beta' : ''}`),
  744. $('<div>').append(
  745. $('<a>', { href: argsObject.forumUrl, target: '__blank' }).text(_trans.prefs.discussionForumLinkText)
  746. )
  747. )
  748. );
  749.  
  750. $('#user-tabs > .nav-tabs').append($tab);
  751. $('#user-info > .flex-parent > .tab-content').append($panel);
  752.  
  753. // Add change events
  754. $('#csRoadTypeButtonsCheckBox').change(function onRoadTypeButtonCheckChanged() {
  755. if (this.checked) {
  756. $('.csRoadTypeButtonsCheckBoxContainer').show();
  757. } else {
  758. $('.csRoadTypeButtonsCheckBoxContainer').hide();
  759. }
  760. saveSettingsToStorage();
  761. });
  762. $('.csSettingsCheckBox').change(function onSettingsCheckChanged() {
  763. const { checked } = this;
  764. const settingName = $(this).data('setting-name');
  765. if (settingName === 'roadType') {
  766. const roadType = $(this).data('road-type');
  767. const array = _settings.roadTypeButtons;
  768. const index = array.indexOf(roadType);
  769. if (checked && index === -1) {
  770. array.push(roadType);
  771. } else if (!checked && index !== -1) {
  772. array.splice(index, 1);
  773. }
  774. } else {
  775. _settings[settingName] = checked;
  776. }
  777. saveSettingsToStorage();
  778. });
  779. }
  780.  
  781. function updateControls() {
  782. if ($(ROAD_TYPE_DROPDOWN_SELECTOR).length > 0) {
  783. if (isChecked('csRoadTypeButtonsCheckBox')) addRoadTypeButtons();
  784. }
  785. if ($(PARKING_SPACES_DROPDOWN_SELECTOR).length > 0 && isChecked('csParkingSpacesButtonsCheckBox')) {
  786. addParkingSpacesButtons(); // TODO - add option setting
  787. }
  788. if ($(PARKING_COST_DROPDOWN_SELECTOR).length > 0 && isChecked('csParkingCostButtonsCheckBox')) {
  789. addParkingCostButtons(); // TODO - add option setting
  790. }
  791. }
  792.  
  793. function replaceWord(target, searchWord, replaceWithWord) {
  794. return target.replace(new RegExp(`\\b${searchWord}\\b`, 'g'), replaceWithWord);
  795. }
  796.  
  797. function titleCase(word) {
  798. return word.charAt(0).toUpperCase() + word.substring(1).toLowerCase();
  799. }
  800. function mcCase(word) {
  801. return word.charAt(0).toUpperCase() + word.charAt(1).toLowerCase()
  802. + word.charAt(2).toUpperCase() + word.substring(3).toLowerCase();
  803. }
  804. function upperCase(word) {
  805. return word.toUpperCase();
  806. }
  807.  
  808. function processSubstring(target, substringRegex, processFunction) {
  809. const substrings = target.match(substringRegex);
  810. if (substrings) {
  811. for (let idx = 0; idx < substrings.length; idx++) {
  812. const substring = substrings[idx];
  813. const newSubstring = processFunction(substring);
  814. target = replaceWord(target, substring, newSubstring);
  815. }
  816. }
  817. return target;
  818. }
  819.  
  820. function onPaste(e) {
  821. const targetNode = e.target;
  822. if (targetNode.name === 'streetName' || targetNode.className.includes('street-name')) {
  823. // Get the text that's being pasted.
  824. let pastedText = e.clipboardData.getData('text/plain');
  825.  
  826. // If pasting text in ALL CAPS...
  827. if (/^[^a-z]*$/.test(pastedText)) {
  828. [
  829. // Title case all words first.
  830. [/\b[a-zA-Z]+(?:'S)?\b/g, titleCase],
  831.  
  832. // Then process special cases.
  833. [/\bMC\w+\b/ig, mcCase], // e.g. McCaulley
  834. [/\b(?:I|US|SH|SR|CH|CR|CS|PR|PS)\s*-?\s*\d+\w*\b/ig, upperCase], // e.g. US-25, US25
  835. /* eslint-disable-next-line max-len */
  836. [/\b(?:AL|AK|AS|AZ|AR|CA|CO|CT|DE|DC|FM|FL|GA|GU|HI|ID|IL|IN|IA|KS|KY|LA|ME|MH|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|MP|OH|OK|OR|PW|PA|PR|RI|SC|SD|TN|TX|UT|VT|VI|VA|WA|WV|WI|WY)\s*-?\s*\d+\w*\b/ig, upperCase], // e.g. WV-52
  837. [/\b(?:NE|NW|SE|SW)\b/ig, upperCase]
  838. ].forEach(item => {
  839. pastedText = processSubstring(pastedText, item[0], item[1]);
  840. });
  841.  
  842. // Insert new text in the focused node.
  843. document.execCommand('insertText', false, pastedText);
  844.  
  845. // Prevent the default paste behavior.
  846. e.preventDefault();
  847. return false;
  848. }
  849. }
  850. return true;
  851. }
  852.  
  853. function getTranslationObject() {
  854. if (argsObject.useDefaultTranslation) {
  855. return DEFAULT_TRANSLATION;
  856. }
  857. let locale = I18n.currentLocale().toLowerCase();
  858. if (!argsObject.translations.hasOwnProperty(locale)) {
  859. locale = 'en-us';
  860. }
  861. return argsObject.translations[locale];
  862. }
  863.  
  864. function errorHandler(callback) {
  865. try {
  866. callback();
  867. } catch (ex) {
  868. console.error(`${argsObject.scriptName}:`, ex);
  869. }
  870. }
  871.  
  872. function init() {
  873. UpdateObject = require('Waze/Action/UpdateObject');
  874. UpdateFeatureAddress = require('Waze/Action/UpdateFeatureAddress');
  875. MultiAction = require('Waze/Action/MultiAction');
  876. AddSeg = require('Waze/Action/AddSegment');
  877. Segment = require('Waze/Feature/Vector/Segment');
  878. DelSeg = require('Waze/Action/DeleteSegment');
  879.  
  880. _trans = getTranslationObject();
  881. Object.keys(ROAD_TYPES).forEach(rtName => {
  882. ROAD_TYPES[rtName].text = _trans.roadTypeButtons[rtName].text;
  883. });
  884.  
  885. document.addEventListener('paste', onPaste);
  886.  
  887. // check for changes in the edit-panel
  888. const observer = new MutationObserver(mutations => {
  889. mutations.forEach(mutation => {
  890. for (let i = 0; i < mutation.addedNodes.length; i++) {
  891. const addedNode = mutation.addedNodes[i];
  892.  
  893. if (addedNode.nodeType === Node.ELEMENT_NODE) {
  894. // Checks to identify if this is a segment in regular display mode.
  895. if (addedNode.querySelector(ROAD_TYPE_DROPDOWN_SELECTOR)) {
  896. if (isChecked('csRoadTypeButtonsCheckBox')) addRoadTypeButtons();
  897. if (isSwapPedestrianPermitted() && isChecked('csAddSwapPedestrianButtonCheckBox')) {
  898. addSwapPedestrianButton('regular');
  899. }
  900. }
  901. // Checks to identify if this is a segment in compact display mode.
  902. if (addedNode.querySelector(ROAD_TYPE_CHIP_SELECTOR)) {
  903. if (isChecked('csRoadTypeButtonsCheckBox')) addCompactRoadTypeChangeEvents();
  904. if (isSwapPedestrianPermitted() && isChecked('csAddSwapPedestrianButtonCheckBox')) {
  905. addSwapPedestrianButton('compact');
  906. }
  907. }
  908. if (addedNode.querySelector(PARKING_SPACES_DROPDOWN_SELECTOR) && isChecked('csParkingSpacesButtonsCheckBox')) {
  909. addParkingSpacesButtons();
  910. }
  911. if (addedNode.querySelector(PARKING_COST_DROPDOWN_SELECTOR)
  912. && isChecked('csParkingCostButtonsCheckBox')) {
  913. addParkingCostButtons();
  914. }
  915. if (addedNode.querySelector('.side-panel-section') && isChecked('csAddAltCityButtonCheckBox')) {
  916. addAddAltCityButton();
  917. }
  918. }
  919. }
  920. });
  921. });
  922.  
  923. observer.observe(document.getElementById('edit-panel'), { childList: true, subtree: true });
  924. initUserPanel();
  925. loadSettingsFromStorage();
  926. injectCss();
  927. // W.prefs.on('change:isImperial', () => errorHandler(() => { initUserPanel(); loadSettingsFromStorage(); }));
  928. updateControls(); // In case of PL w/ segments selected.
  929. W.selectionManager.events.register('selectionchanged', null, () => errorHandler(updateControls));
  930.  
  931. logDebug('Initialized');
  932. }
  933.  
  934. function bootstrap() {
  935. if (typeof require !== 'undefined' && W && W.loginManager && W.loginManager.events.register && W.map && W.loginManager.user) {
  936. logDebug('Initializing...');
  937. init();
  938. } else {
  939. logDebug('Bootstrap failed. Trying again...');
  940. setTimeout(bootstrap, 250);
  941. }
  942. }
  943.  
  944. // Not sure if the document.ready is necessary but I'm leaving it because of some random errors
  945. // that people were having with "require is not defined". I tried several things to fix it and
  946. // I'm leaving those things, though all may not be needed.
  947. $(document).ready(() => {
  948. logDebug('Bootstrap...');
  949. bootstrap();
  950. });
  951. } // END Main function (code to be injected)
  952.  
  953. function injectMain(argsObject) {
  954. if (typeof require !== 'undefined' && typeof $ !== 'undefined') {
  955. // const scriptElem = document.createElement('script');
  956. // scriptElem.textContent = `(function(){${main.toString()}\n main(${JSON.stringify(argsObject).replace('\'', '\\\'')})})();`;
  957. // scriptElem.setAttribute('type', 'application/javascript');
  958. // document.body.appendChild(scriptElem);
  959.  
  960. GM_addElement('script', {
  961. textContent: `(function(){${main.toString()}\n main(${JSON.stringify(argsObject).replace('\'', '\\\'')})})();`
  962. });
  963. } else {
  964. setTimeout(() => injectMain(argsObject), 250);
  965. }
  966. }
  967.  
  968. function setValue(object, path, value) {
  969. const pathParts = path.split('.');
  970. for (let i = 0; i < pathParts.length - 1; i++) {
  971. const pathPart = pathParts[i];
  972. if (pathPart in object) {
  973. object = object[pathPart];
  974. } else {
  975. object[pathPart] = {};
  976. object = object[pathPart];
  977. }
  978. }
  979. object[pathParts[pathParts.length - 1]] = value;
  980. }
  981.  
  982. function convertTranslationsArrayToObject(arrayIn) {
  983. const translations = {};
  984. let iRow;
  985. let iCol;
  986. const languages = arrayIn[0].map(lang => lang.toLowerCase());
  987. for (iCol = 1; iCol < languages.length; iCol++) {
  988. translations[languages[iCol]] = {};
  989. }
  990. for (iRow = 1; iRow < arrayIn.length; iRow++) {
  991. const row = arrayIn[iRow];
  992. const propertyPath = row[0];
  993. for (iCol = 1; iCol < row.length; iCol++) {
  994. setValue(translations[languages[iCol]], propertyPath, row[iCol]);
  995. }
  996. }
  997. return translations;
  998. }
  999.  
  1000. function loadTranslations() {
  1001. if (typeof $ === 'undefined') {
  1002. setTimeout(loadTranslations, 250);
  1003. console.debug('ClickSaver:', 'jQuery not ready. Retry loading translations...');
  1004. } else {
  1005. // This call retrieves the data from the translations spreadsheet and then injects
  1006. // the main code into the page. If the spreadsheet call fails, the default English
  1007. // translation is used.
  1008. const args = {
  1009. scriptName: SCRIPT_NAME,
  1010. scriptVersion: SCRIPT_VERSION,
  1011. forumUrl: FORUM_URL
  1012. };
  1013. $.getJSON(`${TRANSLATIONS_URL}?${DEC(API_KEY)}`).then(res => {
  1014. args.translations = convertTranslationsArrayToObject(res.values);
  1015. console.debug('ClickSaver:', 'Translations loaded.');
  1016. }).fail(() => {
  1017. console.error('ClickSaver: Error loading translations spreadsheet. Using default translation (English).');
  1018. args.useDefaultTranslation = true;
  1019. }).always(() => {
  1020. // Leave this document.ready function. Some people randomly get a "require is not defined" error unless the injectMain function
  1021. // is called late enough. Even with a "typeof require !== 'undefined'" check.
  1022. $(document).ready(() => {
  1023. injectMain(args);
  1024. });
  1025. });
  1026. }
  1027. }
  1028.  
  1029. // This function requires WazeWrap so it must be called outside of the injected code, as
  1030. // WazeWrap is not guaranteed to be available in the page's scope.
  1031. function addToggleDrawNewRoadsAsTwoWayShortcut() {
  1032. new WazeWrap.Interface.Shortcut('ToggleTwoWayNewSeg', 'Toggle new segment two-way drawing',
  1033. 'editing', 'editToggleNewSegTwoWayDrawing', EXTERNAL_SETTINGS.toggleTwoWaySegDrawingShortcut,
  1034. () => { $('wz-checkbox[name="twoWaySegmentsByDefault"]').click(); }, null).add();
  1035. }
  1036.  
  1037. function sandboxLoadSettings() {
  1038. const loadedSettings = JSON.parse(localStorage.getItem(EXTERNAL_SETTINGS_NAME)) || {};
  1039. EXTERNAL_SETTINGS.toggleTwoWaySegDrawingShortcut = loadedSettings.toggleTwoWaySegDrawingShortcut || '';
  1040. addToggleDrawNewRoadsAsTwoWayShortcut();
  1041. $(window).on('beforeunload', () => sandboxSaveSettings());
  1042. }
  1043.  
  1044. function sandboxSaveSettings() {
  1045. let keys = '';
  1046. const { shortcut } = W.accelerators.Actions.ToggleTwoWayNewSeg;
  1047. if (shortcut) {
  1048. if (shortcut.altKey) keys += 'A';
  1049. if (shortcut.shiftKey) keys += 'S';
  1050. if (shortcut.ctrlKey) keys += 'C';
  1051. if (keys.length) keys += '+';
  1052. if (shortcut.keyCode) keys += shortcut.keyCode;
  1053. }
  1054. EXTERNAL_SETTINGS.toggleTwoWaySegDrawingShortcut = keys;
  1055. localStorage.setItem(EXTERNAL_SETTINGS_NAME, JSON.stringify(EXTERNAL_SETTINGS));
  1056. }
  1057.  
  1058. function sandboxBootstrap() {
  1059. if (WazeWrap && WazeWrap.Ready) {
  1060. WazeWrap.Interface.ShowScriptUpdate(SCRIPT_NAME, SCRIPT_VERSION, UPDATE_MESSAGE, FORUM_URL);
  1061. sandboxLoadSettings();
  1062. } else {
  1063. setTimeout(sandboxBootstrap, 250);
  1064. }
  1065. }
  1066.  
  1067. // Go ahead and start loading translations, and inject the main code into the page.
  1068. loadTranslations();
  1069.  
  1070. // Start the "sandboxed" code.
  1071. sandboxBootstrap();
  1072. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址