您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Various UI changes to make editing faster and easier.
当前为
// ==UserScript== // @name WME ClickSaver // @namespace https://greasyfork.org/users/45389 // @version 2025.05.01.000 // @description Various UI changes to make editing faster and easier. // @author MapOMatic // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/ // @license GNU GPLv3 // @connect sheets.googleapis.com // @connect greasyfork.org // @contributionURL https://github.com/WazeDev/Thank-The-Authors // @grant GM_xmlhttpRequest // @grant GM_addElement // @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js // @require https://update.greasyfork.org/scripts/509664/WME%20Utils%20-%20Bootstrap.js // ==/UserScript== /* global I18n */ /* global WazeWrap */ /* global bootstrap */ /* eslint-disable max-classes-per-file */ (function main() { 'use strict'; const updateMessage = 'New: option to display a button to remove segment address (street, city, or street+city)'; const scriptName = GM_info.script.name; const scriptVersion = GM_info.script.version; const downloadUrl = 'https://greasyfork.org/scripts/369629-wme-clicksaver/code/WME%20ClickSaver.user.js'; const forumUrl = 'https://www.waze.com/forum/viewtopic.php?f=819&t=199894'; const translationsUrl = 'https://sheets.googleapis.com/v4/spreadsheets/1ZlE9yhNncP9iZrPzFFa-FCtYuK58wNOEcmKqng4sH1M/values/ClickSaver'; const apiKey = 'YTJWNVBVRkplbUZUZVVGMFl6aFVjMjVOTW0wNU5GaG5kVE40TUZoNWJVZEhWbU5rUjNacVdtdFlWUT09'; const DEC = s => atob(atob(s)); let sdk; // This function is injected into the page. async function clicksaver(argsObject) { /* eslint-disable object-curly-newline */ const roadTypeDropdownSelector = 'wz-select[name="roadType"]'; const roadTypeChipSelector = 'wz-chip-select[class="road-type-chip-select"]'; // const PARKING_SPACES_DROPDOWN_SELECTOR = 'select[name="estimatedNumberOfSpots"]'; // const PARKING_COST_DROPDOWN_SELECTOR = 'select[name="costType"]'; const settingsStoreName = 'clicksaver_settings'; const defaultTranslation = { roadTypeButtons: { St: { text: 'St' }, PS: { text: 'PS' }, mH: { text: 'mH' }, MH: { text: 'MH' }, Fw: { text: 'Fw' }, Rmp: { text: 'Rmp' }, OR: { text: 'OR' }, PLR: { text: 'PLR' }, PR: { text: 'PR' }, Fer: { text: 'Fer' }, WT: { text: 'WT' }, PB: { text: 'PB' }, Sw: { text: 'Sw' }, RR: { text: 'RR' }, RT: { text: 'RT' }, Pw: { text: 'Pw' } }, prefs: { dropdownHelperGroup: 'DROPDOWN HELPERS', roadTypeButtons: 'Add road type buttons', useOldRoadColors: 'Use old road colors (requires refresh)', setCityToDefault: 'Keep default value', setStreetCityToNone: 'Set Street/City to None (new seg\'s only)', // eslint-disable-next-line camelcase setStreetCityToNone_Title: 'NOTE: Only works if connected directly or indirectly' + ' to a segment with State / Country already set.', setCityToConnectedSegCity: 'Set City to connected segment\'s City', parkingCostButtons: 'Add PLA cost buttons', parkingSpacesButtons: 'Add PLA estimated spaces buttons', timeSaversGroup: 'TIME SAVERS', discussionForumLinkText: 'Discussion Forum', showAddAltCityButton: 'Show "Add alt city" button', showSwapDrivingWalkingButton: 'Show "Swap driving<->walking segment type" button', // eslint-disable-next-line camelcase showSwapDrivingWalkingButton_Title: 'Swap between driving-type and walking-type segments. WARNING! This will DELETE and recreate the segment. Nodes may need to be reconnected.', showSwitchStreetNamesButton: 'Show swap primary and alternative street name button', addCompactColors: 'Add colors to compact mode road type buttons', hideUncheckedRoadTypeButtons: 'Hide unchecked road type buttons in compact mode', enableAddressRemovalButton: 'Enable address removal button', addressRemovalButtonTooltipText: 'Select at least one, choosing both will combine the buttons', showRemoveStreetNameButton: 'Show "Remove street" button', removeStreetNameTooltipText: 'If you have different cities selected and you remove the street name, the street name will display as "No common street".', showRemoveCityNameButton: 'Show "Remove city" button' }, 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?', // eslint-disable-next-line camelcase swapSegmentTypeError_Paths: 'Paths must be removed from segment before changing between driving and pedestrian road type.', addAltCityButtonText: 'Add alt city', removeStreetNameButtonText: 'Remove street', removeCityNameButtonText: 'Remove city', removeStreetAndCityNameButtonText: 'Remove street+city', segmentHasStreetNameAndHouseNumbers: 'Cannot remove street name from a segment with house numbers' }; const roadTypeDropdownOption = { DEFAULT: 'DEFAULT', NONE: 'NONE', CONNECTED_CITY: 'CONNECTED_CITY' }; // Road types defined in the WME SDK documentation const wmeRoadType = { ALLEY: 22, FERRY: 15, FREEWAY: 3, MAJOR_HIGHWAY: 6, MINOR_HIGHWAY: 7, OFF_ROAD: 8, PARKING_LOT_ROAD: 20, PEDESTRIAN_BOARDWALK: 10, PRIMARY_STREET: 2, PRIVATE_ROAD: 17, RAILROAD: 18, RAMP: 4, RUNWAY_TAXIWAY: 19, STAIRWAY: 16, STREET: 1, WALKING_TRAIL: 5, WALKWAY: 9 }; const roadTypeSettings = { St: { id: wmeRoadType.STREET, wmeColor: '#ffffeb', svColor: '#ffffff', category: 'streets', visible: true }, PS: { id: wmeRoadType.PRIMARY_STREET, wmeColor: '#f0ea58', svColor: '#cba12e', category: 'streets', visible: true }, Pw: { id: wmeRoadType.ALLEY, wmeColor: '#64799a', svColor: '#64799a', category: 'streets', visible: false }, mH: { id: wmeRoadType.MINOR_HIGHWAY, wmeColor: '#69bf88', svColor: '#ece589', category: 'highways', visible: true }, MH: { id: wmeRoadType.MAJOR_HIGHWAY, wmeColor: '#45b8d1', svColor: '#c13040', category: 'highways', visible: true }, Fw: { id: wmeRoadType.FREEWAY, wmeColor: '#c577d2', svColor: '#387fb8', category: 'highways', visible: false }, Rmp: { id: wmeRoadType.RAMP, wmeColor: '#b3bfb3', svColor: '#58c53b', category: 'highways', visible: false }, OR: { id: wmeRoadType.OFF_ROAD, wmeColor: '#867342', svColor: '#82614a', category: 'otherDrivable', visible: false }, PLR: { id: wmeRoadType.PARKING_LOT_ROAD, wmeColor: '#ababab', svColor: '#2282ab', category: 'otherDrivable', visible: true }, PR: { id: wmeRoadType.PRIVATE_ROAD, wmeColor: '#beba6c', svColor: '#00ffb3', category: 'otherDrivable', visible: true }, Fer: { id: wmeRoadType.FERRY, wmeColor: '#d7d8f8', svColor: '#ff8000', category: 'otherDrivable', visible: false }, RR: { id: wmeRoadType.RAILROAD, wmeColor: '#c62925', svColor: '#ffffff', category: 'nonDrivable', visible: false }, RT: { id: wmeRoadType.RUNWAY_TAXIWAY, wmeColor: '#ffffff', svColor: '#00ff00', category: 'nonDrivable', visible: false }, WT: { id: wmeRoadType.WALKING_TRAIL, wmeColor: '#b0a790', svColor: '#00ff00', category: 'pedestrian', visible: false }, PB: { id: wmeRoadType.PEDESTRIAN_BOARDWALK, wmeColor: '#9a9a9a', svColor: '#0000ff', category: 'pedestrian', visible: false }, Sw: { id: wmeRoadType.STAIRWAY, wmeColor: '#999999', svColor: '#b700ff', category: 'pedestrian', visible: false } }; /* eslint-enable object-curly-newline */ let _settings = {}; let trans; // Translation object // function log(message) { // console.log('ClickSaver:', message); // } function logDebug(message) { console.debug('ClickSaver:', message); } // function logWarning(message) { // console.warn('ClickSaver:', message); // } // function logError(message) { // console.error('ClickSaver:', message); // } function isChecked(checkboxId) { return $(`#${checkboxId}`).is(':checked'); } function isSwapPedestrianPermitted() { const userInfo = sdk.State.getUserInfo(); const rank = userInfo.rank + 1; return rank >= 4 || (rank === 3 && userInfo.isAreaManager); } function setChecked(checkboxId, checked) { $(`#${checkboxId}`).prop('checked', checked); } function loadSettingsFromStorage() { const loadedSettings = $.parseJSON(localStorage.getItem(settingsStoreName)); const defaultSettings = { lastVersion: null, roadButtons: true, roadTypeButtons: ['St', 'PS', 'mH', 'MH', 'Fw', 'Rmp', 'PLR', 'PR', 'PB'], parkingCostButtons: true, parkingSpacesButtons: true, setNewPLRCity: roadTypeDropdownOption.DEFAULT, setNewPRCity: roadTypeDropdownOption.DEFAULT, setNewRRCity: roadTypeDropdownOption.DEFAULT, setNewPBCity: roadTypeDropdownOption.DEFAULT, setNewORCity: roadTypeDropdownOption.DEFAULT, addAltCityButton: true, addSwapPedestrianButton: false, useOldRoadColors: false, warnOnPedestrianTypeSwap: true, addCompactColors: true, addSwitchPrimaryNameButton: false, hideUncheckedRoadTypeButtons: false, addRemoveAddressButton: false, removeStreetName: false, removeCityName: false, shortcuts: {} }; _settings = { ...defaultSettings, ...loadedSettings }; setChecked('csRoadTypeButtonsCheckBox', _settings.roadButtons); if (_settings.roadTypeButtons) { Object.keys(roadTypeSettings).forEach(roadTypeAbbr => { const checked = _settings.roadTypeButtons.indexOf(roadTypeAbbr) !== -1; const selector = `cs${roadTypeAbbr}CheckBox`; setChecked(selector, checked); if (!checked) { $(`#${selector}`).siblings('.csDropdownContainer').hide(); } }); } $('.csRoadTypeButtonsCheckBoxContainer').toggle(_settings.roadButtons); $('.csAddRemoveAddressButtonCheckBoxContainer').toggle(_settings.addRemoveAddressButton); // setChecked('csParkingSpacesButtonsCheckBox', _settings.parkingSpacesButtons); // setChecked('csParkingCostButtonsCheckBox', _settings.parkingCostButtons); setDropdownValue('csSetPLRCityDropdown', _settings.setNewPLRCity); setDropdownValue('csSetPRCityDropdown', _settings.setNewPRCity); setDropdownValue('csSetRRCityDropdown', _settings.setNewRRCity); setDropdownValue('csSetPBCityDropdown', _settings.setNewPBCity); setDropdownValue('csSetORCityDropdown', _settings.setNewORCity); setChecked('csUseOldRoadColorsCheckBox', _settings.useOldRoadColors); setChecked('csAddAltCityButtonCheckBox', _settings.addAltCityButton); setChecked('csAddSwapPedestrianButtonCheckBox', _settings.addSwapPedestrianButton); setChecked('csAddCompactColorsCheckBox', _settings.addCompactColors); setChecked('csAddSwitchPrimaryNameCheckBox', _settings.addSwitchPrimaryNameButton); setChecked('csHideUncheckedRoadTypeButtonsCheckBox', _settings.hideUncheckedRoadTypeButtons); setChecked('csAddRemoveAddressButtonCheckBox', _settings.addRemoveAddressButton); setChecked('csRemoveStreetNameCheckBox', _settings.removeStreetName); setChecked('csRemoveCityNameCheckBox', _settings.removeCityName); } function setDropdownValue(dropdownId, value) { $(`#${dropdownId}`).val(value); } function saveSettingsToStorage() { const settings = { lastVersion: argsObject.scriptVersion, roadButtons: _settings.roadButtons, parkingCostButtons: _settings.parkingCostButtons, parkingSpacesButtons: _settings.parkingSpacesButtons, setNewPLRCity: _settings.setNewPLRCity, setNewPRCity: _settings.setNewPRCity, setNewRRCity: _settings.setNewRRCity, setNewPBCity: _settings.setNewPBCity, setNewORCity: _settings.setNewORCity, useOldRoadColors: _settings.useOldRoadColors, addAltCityButton: _settings.addAltCityButton, addSwapPedestrianButton: _settings.addSwapPedestrianButton, warnOnPedestrianTypeSwap: _settings.warnOnPedestrianTypeSwap, addCompactColors: _settings.addCompactColors, addSwitchPrimaryNameButton: _settings.addSwitchPrimaryNameButton, hideUncheckedRoadTypeButtons: _settings.hideUncheckedRoadTypeButtons, addRemoveAddressButton: _settings.addRemoveAddressButton, removeStreetName: _settings.removeStreetName, removeCityName: _settings.removeCityName, shortcuts: {} }; sdk.Shortcuts.getAllShortcuts().forEach(shortcut => { settings.shortcuts[shortcut.shortcutId] = shortcut.shortcutKeys; }); settings.roadTypeButtons = []; Object.keys(roadTypeSettings).forEach(roadTypeAbbr => { if (_settings.roadTypeButtons.indexOf(roadTypeAbbr) !== -1) { settings.roadTypeButtons.push(roadTypeAbbr); } }); localStorage.setItem(settingsStoreName, JSON.stringify(settings)); logDebug('Settings saved'); } function isPedestrianTypeSegment(segment) { const pedRoadTypes = Object.values(roadTypeSettings) .filter(roadType => roadType.category === 'pedestrian') .map(roadType => roadType.id); return pedRoadTypes.includes(segment.roadType); } function getConnectedSegmentIDs(segmentId) { return [ ...sdk.DataModel.Segments.getConnectedSegments({ segmentId, reverseDirection: false }), ...sdk.DataModel.Segments.getConnectedSegments({ segmentId, reverseDirection: true }) ].map(segment => segment.id); } function getFirstConnectedSegmentAddress(segmentId) { const nonMatches = []; const segmentIDsToSearch = [segmentId]; const hasAddress = id => !sdk.DataModel.Segments.getAddress({ segmentId: id }).isEmpty; while (segmentIDsToSearch.length > 0) { const startSegmentID = segmentIDsToSearch.pop(); const connectedSegmentIDs = getConnectedSegmentIDs(startSegmentID); const hasAddrSegmentId = connectedSegmentIDs.find(hasAddress); if (hasAddrSegmentId) return sdk.DataModel.Segments.getAddress({ segmentId: hasAddrSegmentId }); nonMatches.push(startSegmentID); connectedSegmentIDs.forEach(segmentID => { if (nonMatches.indexOf(segmentID) === -1 && segmentIDsToSearch.indexOf(segmentID) === -1) { segmentIDsToSearch.push(segmentID); } }); } return null; } function setStreetAndCity(setCity) { const selection = sdk.Editing.getSelection(); selection?.ids.forEach(segmentId => { if (sdk.DataModel.Segments.getAddress({ segmentId }).isEmpty) { const addr = getFirstConnectedSegmentAddress(segmentId); if (addr) { // Process the city const newCityProperties = { cityName: setCity && !addr.city?.isEmpty ? addr.city.name : '', countryId: addr.country.id, stateId: addr.state.id }; let newCityId = sdk.DataModel.Cities.getCity(newCityProperties)?.id; if (newCityId == null) { newCityId = sdk.DataModel.Cities.addCity(newCityProperties).id; } // Process the street const newPrimaryStreetId = getOrCreateStreet('', newCityId).id; // Update the segment with the new street sdk.DataModel.Segments.updateAddress({ segmentId, primaryStreetId: newPrimaryStreetId }); } } }); } class WaitForElementError extends Error { } function waitForElem(selector) { return new Promise((resolve, reject) => { function checkIt(tries = 0) { if (tries < 150) { // try for about 3 seconds; const elem = document.querySelector(selector); setTimeout(() => { if (!elem) { checkIt(++tries); } else { resolve(elem); } }, 20); } else { reject(new WaitForElementError(`Element was not found within 3 seconds: ${selector}`)); } } checkIt(); }); } async function waitForShadowElem(parentElemSelector, shadowElemSelectors) { const parentElem = await waitForElem(parentElemSelector); return new Promise((resolve, reject) => { shadowElemSelectors.forEach((shadowElemSelector, idx) => { function checkIt(parent, tries = 0) { if (tries < 150) { // try for about 3 seconds; const shadowElem = parent.shadowRoot.querySelector(shadowElemSelector); setTimeout(() => { if (!shadowElem) { checkIt(parent, ++tries); } else if (idx === shadowElemSelectors.length - 1) { resolve({ shadowElem, parentElem }); } else { checkIt(shadowElem, 0); } }, 20); } else { reject(new WaitForElementError(`Shadow element was not found within 3 seconds: ${shadowElemSelector}`)); } } checkIt(parentElem); }); }); } async function onAddAltCityButtonClick() { const segmentId = sdk.Editing.getSelection().ids[0]; const addr = sdk.DataModel.Segments.getAddress({ segmentId }); $('wz-button[class="add-alt-street-btn"]').click(); await waitForElem('wz-autocomplete.alt-street-name'); // Set the street name field let result = await waitForShadowElem('wz-autocomplete.alt-street-name', ['wz-text-input']); result.shadowElem.focus(); result.shadowElem.value = addr?.street?.name ?? ''; // Clear the city name field result = await waitForShadowElem('wz-autocomplete.alt-city-name', ['wz-text-input']); result.shadowElem.focus(); result.shadowElem.value = null; } function onRoadTypeButtonClick(roadType) { const selection = sdk.Editing.getSelection(); // Temporarily remove this while bugs are worked out. // WS.SDKMultiActionHack.groupActions(() => { selection?.ids.forEach(segmentId => { // Check for same roadType is necessary to prevent an error. if (sdk.DataModel.Segments.getById({ segmentId }).roadType !== roadType) { sdk.DataModel.Segments.updateSegment({ segmentId, roadType }); } }); if (_settings.roadTypeButtons.map(rtb => roadTypeSettings[rtb].id).includes(roadType)) { const roadTypeSettingsMap = { [roadTypeSettings.PLR.id]: _settings.setNewPLRCity, [roadTypeSettings.PR.id]: _settings.setNewPRCity, [roadTypeSettings.RR.id]: _settings.setNewRRCity, [roadTypeSettings.PB.id]: _settings.setNewPBCity, [roadTypeSettings.OR.id]: _settings.setNewORCity }; const setting = roadTypeSettingsMap[roadType]; if (!setting || setting === roadTypeDropdownOption.DEFAULT) { return; } setStreetAndCity(setting === roadTypeDropdownOption.CONNECTED_CITY); } } function addRoadTypeButtons() { const selection = sdk.Editing.getSelection(); if (selection?.objectType !== 'segment') return; const segmentId = selection.ids[0]; if (segmentId == null) return; const segment = sdk.DataModel.Segments.getById({ segmentId }); if (!segment) return; const isPed = isPedestrianTypeSegment(segment); const $dropDown = $(roadTypeDropdownSelector); $('#csRoadTypeButtonsContainer').remove(); const $container = $('<div>', { id: 'csRoadTypeButtonsContainer', class: 'cs-rt-buttons-container', style: 'display: inline-table;' }); const $street = $('<div>', { id: 'csStreetButtonContainer', class: 'cs-rt-buttons-group' }); const $highway = $('<div>', { id: 'csHighwayButtonContainer', class: 'cs-rt-buttons-group' }); const $otherDrivable = $('<div>', { id: 'csOtherDrivableButtonContainer', class: 'cs-rt-buttons-group' }); const $nonDrivable = $('<div>', { id: 'csNonDrivableButtonContainer', class: 'cs-rt-buttons-group' }); const $pedestrian = $('<div>', { id: 'csPedestrianButtonContainer', class: 'cs-rt-buttons-group' }); const divs = { streets: $street, highways: $highway, otherDrivable: $otherDrivable, nonDrivable: $nonDrivable, pedestrian: $pedestrian }; Object.keys(roadTypeSettings).forEach(roadTypeKey => { if (_settings.roadTypeButtons.includes(roadTypeKey)) { const roadTypeSetting = roadTypeSettings[roadTypeKey]; const isDisabled = $dropDown[0].hasAttribute('disabled') && $dropDown[0].getAttribute('disabled') === 'true'; if (!isDisabled && ((roadTypeSetting.category === 'pedestrian' && isPed) || (roadTypeSetting.category !== 'pedestrian' && !isPed))) { const $div = divs[roadTypeSetting.category]; $div.append( $('<div>', { class: `btn cs-rt-button cs-rt-button-${roadTypeKey} btn-positive`, title: I18n.t('segment.road_types')[roadTypeSetting.id] }) .text(trans.roadTypeButtons[roadTypeKey].text) .prop('checked', roadTypeSetting.visible) .data('rtId', roadTypeSetting.id) .click(function rtbClick() { onRoadTypeButtonClick($(this).data('rtId')); }) ); } } }); if (isPed) { $container.append($pedestrian); } else { $container.append($street).append($highway).append($otherDrivable).append($nonDrivable); } $dropDown.before($container); } // Function to add an event listener to the chip select for the road type in compact mode function addCompactRoadTypeChangeEvents() { const chipSelect = document.getElementsByClassName('road-type-chip-select')[0]; chipSelect.addEventListener('chipSelected', evt => { const rtValue = evt.detail.value; onRoadTypeButtonClick(rtValue); }); } // Function to add road type colors to the chips in compact mode async function addCompactRoadTypeColors() { // TODO: Clean this up. Was combined from two functions. try { if (sdk.Settings.getUserSettings().isCompactMode && isChecked('csAddCompactColorsCheckBox') && sdk.Editing.getSelection()) { const useOldColors = _settings.useOldRoadColors; await waitForElem('.road-type-chip-select wz-checkable-chip'); $('.road-type-chip-select wz-checkable-chip').addClass('cs-compact-button'); Object.values(roadTypeSettings).forEach(roadType => { const bgColor = useOldColors ? roadType.svColor : roadType.wmeColor; const rtChip = $(`.road-type-chip-select wz-checkable-chip[value=${roadType.id}]`); if (rtChip.length !== 1) return; waitForShadowElem(`.road-type-chip-select wz-checkable-chip[value='${roadType.id}']`, ['div']).then(result => { const $elem = $(result.shadowElem); const padding = $elem.hasClass('checked') ? '0px 3px' : '0px 4px'; $elem.css({ backgroundColor: bgColor, padding, color: 'black' }); }); }); const result = await waitForShadowElem('.road-type-chip-select wz-checkable-chip[checked=""]', ['div']); $(result.shadowElem).css({ border: 'black 2px solid', padding: '0px 3px' }); $('.road-type-chip-select wz-checkable-chip').each(function updateRoadTypeChip() { const style = {}; if (this.getAttribute('checked') === 'false') { style.border = ''; style.padding = '0px 4px'; } else { style.border = 'black 2px solid'; style.padding = '0px 3px'; } $(this.shadowRoot.querySelector('div')).css(style); }); } } catch (ex) { if (ex instanceof WaitForElementError) { // waitForElem will throw an error if Undo causes a deselection. Ignore it. } else { throw ex; } } } // function isPLA(item) { // return (item.model.type === 'venue') && item.model.attributes.categories.includes('PARKING_LOT'); // } // function addParkingSpacesButtons() { // const $dropDown = $(PARKING_SPACES_DROPDOWN_SELECTOR); // const selItems = W.selectionManager.getSelectedFeatures(); // const item = selItems[0]; // // If it's not a PLA, exit. // if (!isPLA(item)) return; // $('#csParkingSpacesContainer').remove(); // const $div = $('<div>', { id: 'csParkingSpacesContainer' }); // const dropdownDisabled = $dropDown.attr('disabled') === 'disabled'; // const optionNodes = $(`${PARKING_SPACES_DROPDOWN_SELECTOR} option`); // for (let i = 0; i < optionNodes.length; i++) { // const $option = $(optionNodes[i]); // const text = $option.text(); // const selected = $option.val() === $dropDown.val(); // $div.append( // // TODO css // $('<div>', { // class: `btn waze-btn waze-btn-white${selected ? ' waze-btn-blue' : ''}${dropdownDisabled ? ' disabled' : ''}`, // style: 'margin-bottom: 5px; height: 22px; padding: 2px 8px 0px 8px; margin-right: 3px;' // }) // .text(text) // .data('val', $option.val()) // // eslint-disable-next-line func-names // .hover(() => { }) // .click(function onParkingSpacesButtonClick() { // if (!dropdownDisabled) { // $(PARKING_SPACES_DROPDOWN_SELECTOR).val($(this).data('val')).change(); // addParkingSpacesButtons(); // } // }) // ); // } // $dropDown.before($div); // $dropDown.hide(); // } // function addParkingCostButtons() { // const $dropDown = $(PARKING_COST_DROPDOWN_SELECTOR); // const selItems = W.selectionManager.getSelectedFeatures(); // const item = selItems[0]; // // If it's not a PLA, exit. // if (!isPLA(item)) return; // $('#csParkingCostContainer').remove(); // const $div = $('<div>', { id: 'csParkingCostContainer' }); // const dropdownDisabled = $dropDown.attr('disabled') === 'disabled'; // const optionNodes = $(`${PARKING_COST_DROPDOWN_SELECTOR} option`); // for (let i = 0; i < optionNodes.length; i++) { // const $option = $(optionNodes[i]); // const text = $option.text(); // const selected = $option.val() === $dropDown.val(); // $div.append( // $('<div>', { // class: `btn waze-btn waze-btn-white${selected ? ' waze-btn-blue' : ''}${dropdownDisabled ? ' disabled' : ''}`, // // TODO css // style: 'margin-bottom: 5px; height: 22px; padding: 2px 8px 0px 8px; margin-right: 4px;' // }) // .text(text !== '' ? text : '?') // .data('val', $option.val()) // // eslint-disable-next-line func-names // .hover(() => { }) // .click(function onParkingCostButtonClick() { // if (!dropdownDisabled) { // $(PARKING_COST_DROPDOWN_SELECTOR).val($(this).data('val')).change(); // addParkingCostButtons(); // } // }) // ); // } // $dropDown.before($div); // $dropDown.hide(); // } function addAddAltCityButton() { // Only show the button if every segment has the same primary city and street. if (!selectedPrimaryStreetsAreEqual()) { return; } const $button = $('<wz-button>') .text(trans.addAltCityButtonText) .click(onAddAltCityButtonClick) .attr({ size: 'sm', color: 'text' }); $('#csAddressButtonContainer').append($button); } async function addSwitchPrimaryNameButton() { if (!isChecked('csAddSwitchPrimaryNameCheckBox')) { return; } if (!selectedPrimaryStreetsAreEqual() || !selectedAltStreetsAreEqual()) { return; } await waitForElem('.alt-streets-control'); // eslint-disable-next-line func-names $('span.alt-street-preview').each(function() { const id = 'csAddSwitchPrimaryName'; const altStreetId = Number($(this).attr('data-id')); const switchingIconElement = $(this).find(`#${id}`); if (streetEqualsPrimaryStreetName(altStreetId)) { switchingIconElement.remove(); return; } const switchingIconExists = switchingIconElement.length > 0; if (switchingIconExists) { return; } const switchStreetNameButton = $('<i>', { id, class: 'w-icon w-icon-arrow-up alt-edit-button' }); $(this).append(switchStreetNameButton); switchStreetNameButton.click(onSwitchStreetNamesClick); }); } function onSwitchStreetNamesClick() { const selectedSegments = getSelectedSegments(); const currentPrimaryStreet = sdk.DataModel.Segments.getAddress({ segmentId: selectedSegments[0] }); const currentAltStreets = currentPrimaryStreet.altStreets.map(street => street.street); const selectedStreetId = Number($(this).parent().attr('data-id')); const newPrimary = currentAltStreets .find(street => street.id === selectedStreetId); // WS.SDKMultiActionHack.groupActions(() => { const newPrimaryStreet = getOrCreateStreet(newPrimary.name, currentPrimaryStreet.city.id); const primaryToAltStreet = getOrCreateStreet(currentPrimaryStreet.street.name, newPrimary.cityId); const newAltStreetsIds = currentAltStreets .map(alt => alt.id) .filter(id => id !== selectedStreetId); newAltStreetsIds.push(primaryToAltStreet.id); selectedSegments.forEach(segmentId => sdk.DataModel.Segments.updateAddress({ segmentId, primaryStreetId: newPrimaryStreet.id, alternateStreetIds: newAltStreetsIds })); // }); } function addRemoveAddressButton() { if (!isChecked('csRemoveStreetNameCheckBox') && !isChecked('csRemoveCityNameCheckBox')) { return; } const translation = getRemoveAddressButtonTranslation(); const hasHouseNumbers = segmentWithStreetNameHasHouseNumbers(); const $button = $('<wz-button>') .text(translation) .click(onRemoveAddressButton) .attr({ size: 'sm', color: 'text', disabled: hasHouseNumbers, title: hasHouseNumbers ? trans.segmentHasStreetNameAndHouseNumbers : '' }); $('#csAddressButtonContainer').append($button); } function segmentWithStreetNameHasHouseNumbers() { const selectedSegmentIds = getSelectedSegments(); if (!selectedSegmentIds) { return false; } const isStreetNameChecked = isChecked('csRemoveStreetNameCheckBox'); if (!isStreetNameChecked) { return false; } return selectedSegmentIds.some(segmentId => { const segment = sdk.DataModel.Segments.getById({ segmentId }); return segment.hasHouseNumbers; }); } function getRemoveAddressButtonTranslation() { if (isChecked('csRemoveStreetNameCheckBox') && isChecked('csRemoveCityNameCheckBox')) { return trans.removeStreetAndCityNameButtonText; } if (isChecked('csRemoveCityNameCheckBox')) { return trans.removeCityNameButtonText; } if (isChecked('csRemoveStreetNameCheckBox')) { return trans.removeStreetNameButtonText; } return ''; } async function onRemoveAddressButton() { const selectedSegmentIds = getSelectedSegments(); if (!selectedSegmentIds) { return; } const emptyCityId = getOrCreateEmptyCity().id; const isStreetNameChecked = isChecked('csRemoveStreetNameCheckBox'); const isCityNameChecked = isChecked('csRemoveCityNameCheckBox'); selectedSegmentIds .forEach(segmentId => { const address = sdk.DataModel.Segments.getAddress({ segmentId }); const streetName = isStreetNameChecked ? '' : address.street?.name ?? ''; const cityId = isCityNameChecked ? emptyCityId : address.city?.id ?? ''; const newStreetId = getOrCreateStreet(streetName, cityId).id; sdk.DataModel.Segments.updateAddress({ segmentId, primaryStreetId: newStreetId }); }); } function getOrCreateEmptyCity() { return sdk.DataModel.Cities.getAll().find(city => city.isEmpty) ?? sdk.DataModel.Cities.addCity({ cityName: '' }); } function addSwapPedestrianButton() { // Added displayMode argument to identify compact vs. regular mode. const id = 'csSwapPedestrianContainer'; $(`#${id}`).remove(); const selection = sdk.Editing.getSelection(); if (selection?.ids.length === 1 && selection.objectType === 'segment') { // TODO css const $container = $('<div>', { id, style: 'white-space: nowrap;float: right;display: inline;' }); const $button = $('<div>', { id: 'csBtnSwapPedestrianRoadType', title: '', // TODO css style: 'display:inline-block;cursor:pointer;' }); $button.append('<i class="w-icon w-icon-streetview w-icon-lg"></i><i class="fa fa-arrows-h fa-lg" style="color: #e84545;vertical-align: top;"></i><i class="w-icon w-icon-car w-icon-lg"></i>') .attr({ title: trans.prefs.showSwapDrivingWalkingButton_Title }); $container.append($button); // Insert swap button in the correct location based on display mode. const $label = $('#segment-edit-general > form > div > div.road-type-control > wz-label'); $label.css({ display: 'inline' }).append($container); $('#csBtnSwapPedestrianRoadType').click(onSwapPedestrianButtonClick); } } function onSwapPedestrianButtonClick() { if (_settings.warnOnPedestrianTypeSwap) { _settings.warnOnPedestrianTypeSwap = false; saveSettingsToStorage(); if (!confirm(trans.swapSegmentTypeWarning)) { return; } } const originalSegment = sdk.DataModel.Segments.getById({ segmentId: sdk.Editing.getSelection().ids[0] }); // Copy the selected segment geometry and attributes, then delete it. const oldPrimaryStreetId = originalSegment.primaryStreetId; const oldAltStreetIds = originalSegment.alternateStreetIds; // WS.SDKMultiActionHack.groupActions(() => { const newRoadType = isPedestrianTypeSegment(originalSegment) ? wmeRoadType.STREET : wmeRoadType.WALKING_TRAIL; try { sdk.DataModel.Segments.deleteSegment({ segmentId: originalSegment.id }); } catch (ex) { if (ex instanceof sdk.Errors.InvalidStateError) { WazeWrap.Alerts.error(scriptName, 'Something prevents this segment from being deleted.'); return; } } // create the replacement segment in the other segment type (pedestrian -> road & vice versa) const newSegmentId = sdk.DataModel.Segments.addSegment({ geometry: originalSegment.geometry, roadType: newRoadType }); sdk.DataModel.Segments.updateAddress({ segmentId: newSegmentId, primaryStreetId: oldPrimaryStreetId, alternateStreetIds: oldAltStreetIds }); sdk.Editing.setSelection({ selection: { ids: [newSegmentId], objectType: 'segment' } }); // }); } function getSelectedSegments() { const selection = sdk.Editing.getSelection(); if (selection?.objectType !== 'segment') { return null; } return selection.ids; } function selectedPrimaryStreetsAreEqual() { const selection = getSelectedSegments(); if (!selection) { return false; } if (selection.length === 1) { return true; } const firstStreetId = sdk.DataModel.Segments.getAddress({ segmentId: selection[0] })?.street?.id; return selection .map(segmentId => sdk.DataModel.Segments.getAddress({ segmentId })) .every(addr => addr.street?.id === firstStreetId); } function selectedAltStreetsAreEqual() { const selection = getSelectedSegments(); if (!selection) { return false; } const addresses = selection.map(segmentId => sdk.DataModel.Segments.getAddress({ segmentId })) .map(street => street.altStreets.map(altStreet => altStreet.street.id)) .map(addr => new Set(addr)); const firstAltAddresses = addresses[0]; return addresses .every(address => address.size === firstAltAddresses.size && Array.from(address).every(value => firstAltAddresses.has(value))); } function getOrCreateStreet(streetName, cityId) { return sdk.DataModel.Streets.getStreet({ streetName, cityId }) ?? sdk.DataModel.Streets.addStreet({ streetName, cityId }); } function streetEqualsPrimaryStreetName(altStreetId) { const selection = getSelectedSegments(); const primaryStreetName = selection .map(segmentId => sdk.DataModel.Segments.getAddress({ segmentId }))[0].street?.name; const selectedStreetName = sdk.DataModel.Streets.getById({ streetId: altStreetId })?.name; return primaryStreetName === selectedStreetName; } /* eslint-disable no-bitwise, no-mixed-operators */ function shadeColor2(color, percent) { const f = parseInt(color.slice(1), 16); const t = percent < 0 ? 0 : 255; const p = percent < 0 ? percent * -1 : percent; const R = f >> 16; const G = f >> 8 & 0x00FF; const B = f & 0x0000FF; return `#${(0x1000000 + (Math.round((t - R) * p) + R) * 0x10000 + (Math.round((t - G) * p) + G) * 0x100 + (Math.round((t - B) * p) + B)).toString(16).slice(1)}`; } /* eslint-enable no-bitwise, no-mixed-operators */ function buildRoadTypeButtonCss() { const lines = []; const useOldColors = _settings.useOldRoadColors; Object.keys(roadTypeSettings).forEach(roadTypeAbbr => { const roadType = roadTypeSettings[roadTypeAbbr]; const bgColor = useOldColors ? roadType.svColor : roadType.wmeColor; let output = `.cs-rt-buttons-container .cs-rt-button-${roadTypeAbbr} {background-color:${ bgColor};box-shadow:0 2px ${shadeColor2(bgColor, -0.5)};border-color:${shadeColor2(bgColor, -0.15)};}`; output += ` .cs-rt-buttons-container .cs-rt-button-${roadTypeAbbr}:hover {background-color:${ shadeColor2(bgColor, 0.2)}}`; lines.push(output); }); return lines.join(' '); } function injectCss() { const css = [ // Road type button formatting '.csRoadTypeButtonsCheckBoxContainer {margin-left:15px;}', '.cs-rt-buttons-container {margin-bottom:5px;height:21px;}', '.cs-rt-buttons-container .cs-rt-button {font-size:11px;line-height:20px;color:black;padding:0px 4px;height:20px;' + 'margin-right:2px;border-style:solid;border-width:1px;}', buildRoadTypeButtonCss(), '.btn.cs-rt-button:active {box-shadow:none;transform:translateY(2px)}', 'div .cs-rt-buttons-group {float:left; margin: 0px 5px 5px 0px;}', '#sidepanel-clicksaver .controls-container {padding:0px;}', '#sidepanel-clicksaver .controls-container label {white-space: normal;}', '#sidepanel-clicksaver {font-size:13px;}', // Compact moad road type button formatting. '.cs-compact-button[checked="false"] {opacity: 0.65;}', // Lock button formatting '.cs-group-label {font-size: 11px; width: 100%; font-family: Poppins, sans-serif;' + ' text-transform: uppercase; font-weight: 700; color: #354148; margin-bottom: 6px;}' ].join(' '); $(`<style type="text/css">${css}</style>`).appendTo('head'); } function createSettingsDropdown(id, settingName, titleText, divCss, options, optionalAttributes) { const $container = $('<div>', { class: 'controls-container' }); const $select = $('<select>', { class: 'csSettingsControl', id, // TODO css style: 'font-size: 12px; border-color: #cbcbcb;border-radius: 10px; white-space: nowrap; width: 100%; text-overflow: ellipsis;', 'data-setting-name': settingName }).appendTo($container); // TODO css if (divCss) $container.css(divCss); // TODO css if (titleText) $container.attr({ title: titleText }); if (optionalAttributes) $select.attr(optionalAttributes); options.forEach(option => { $select.append($('<option>', { value: option.value, text: option.text })); }); return $container; } function createSettingsCheckbox(id, settingName, labelText, titleText, divCss, labelCss, optionalAttributes) { const $container = $('<div>', { class: 'controls-container' }); const $input = $('<input>', { type: 'checkbox', class: 'csSettingsControl', name: id, id, 'data-setting-name': settingName }).appendTo($container); if (titleText) { labelText += '*'; } const $label = $('<label>', { for: id }).text(labelText).appendTo($container); // TODO css if (divCss) $container.css(divCss); // TODO css if (labelCss) $label.css(labelCss); if (titleText) $container.attr({ title: titleText }); if (optionalAttributes) $input.attr(optionalAttributes); return $container; } async function initUserPanel() { const $roadTypesDiv = $('<div>', { class: 'csRoadTypeButtonsCheckBoxContainer' }); $roadTypesDiv.append( createSettingsCheckbox('csUseOldRoadColorsCheckBox', 'useOldRoadColors', trans.prefs.useOldRoadColors) ); Object.keys(roadTypeSettings).forEach(roadTypeAbbr => { const roadType = roadTypeSettings[roadTypeAbbr]; const id = `cs${roadTypeAbbr}CheckBox`; const title = I18n.t('segment.road_types')[roadType.id]; const $roadTypeContainer = createSettingsCheckbox(id, 'roadType', title, null, null, null, { 'data-road-type': roadTypeAbbr }); $roadTypesDiv.append($roadTypeContainer); if (['PLR', 'PR', 'RR', 'PB', 'OR'].includes(roadTypeAbbr)) { // added RR & PB by jm6087 const $dropdownContainer = $('<div>', { class: 'csDropdownContainer' }); const options = [ { value: roadTypeDropdownOption.DEFAULT, text: trans.prefs.setCityToDefault }, { value: roadTypeDropdownOption.NONE, text: trans.prefs.setStreetCityToNone }, { value: roadTypeDropdownOption.CONNECTED_CITY, text: trans.prefs.setCityToConnectedSegCity } ]; $dropdownContainer.append( // TODO css createSettingsDropdown( `csSet${roadTypeAbbr}CityDropdown`, `setNew${roadTypeAbbr}City`, '', { paddingLeft: '20px', marginRight: '4px' }, options ) ); $roadTypeContainer.append($dropdownContainer); } }); const $streetDetailDiv = $('<div>', { class: 'csAddRemoveAddressButtonCheckBoxContainer' }).append( createSettingsCheckbox( 'csRemoveStreetNameCheckBox', 'removeStreetName', trans.prefs.showRemoveStreetNameButton, trans.prefs.removeStreetNameTooltipText, { paddingLeft: '20px' } ), createSettingsCheckbox( 'csRemoveCityNameCheckBox', 'removeCityName', trans.prefs.showRemoveCityNameButton, '', { paddingLeft: '20px' } ) ); const $panel = $('<div>', { id: 'sidepanel-clicksaver' }).append( $('<div>', { class: 'side-panel-section>' }).append( // TODO css $('<div>', { style: 'margin-bottom:8px;' }).append( $('<div>', { class: 'form-group' }).append( $('<label>', { class: 'cs-group-label' }).text(trans.prefs.dropdownHelperGroup), $('<div>').append( createSettingsCheckbox( 'csRoadTypeButtonsCheckBox', 'roadButtons', trans.prefs.roadTypeButtons ) ).append($roadTypesDiv), createSettingsCheckbox( 'csAddCompactColorsCheckBox', 'addCompactColors', trans.prefs.addCompactColors ), createSettingsCheckbox( 'csHideUncheckedRoadTypeButtonsCheckBox', 'hideUncheckedRoadTypeButtons', trans.prefs.hideUncheckedRoadTypeButtons ) ), $('<label>', { class: 'cs-group-label' }).text(trans.prefs.timeSaversGroup), $('<div>', { style: 'margin-bottom:8px;' }).append( createSettingsCheckbox( 'csAddAltCityButtonCheckBox', 'addAltCityButton', trans.prefs.showAddAltCityButton ), createSettingsCheckbox( 'csAddRemoveAddressButtonCheckBox', 'addRemoveAddressButton', trans.prefs.enableAddressRemovalButton, trans.prefs.addressRemovalButtonTooltipText ).append($streetDetailDiv), isSwapPedestrianPermitted() ? createSettingsCheckbox( 'csAddSwapPedestrianButtonCheckBox', 'addSwapPedestrianButton', trans.prefs.showSwapDrivingWalkingButton ) : '', createSettingsCheckbox( 'csAddSwitchPrimaryNameCheckBox', 'addSwitchPrimaryNameButton', trans.prefs.showSwitchStreetNamesButton ) ) ) ) ); $panel.append( // TODO css $('<div>', { style: 'margin-top:20px;font-size:10px;color:#999999;' }).append( $('<div>').text(`v. ${argsObject.scriptVersion}${argsObject.scriptName.toLowerCase().includes('beta') ? ' beta' : ''}`), $('<div>').append( $('<a>', { href: argsObject.forumUrl, target: '__blank' }).text(trans.prefs.discussionForumLinkText) ) ) ); const { tabLabel, tabPane } = await sdk.Sidebar.registerScriptTab(); $(tabLabel).text('CS'); $(tabPane).append($panel); // Decrease spacing around the tab contents. $(tabPane).parent().css({ 'padding-top': '0px', 'padding-left': '8px' }); // Add change events $('#csRoadTypeButtonsCheckBox').change(function onRoadTypeButtonCheckChanged() { $('.csRoadTypeButtonsCheckBoxContainer').toggle(this.checked); saveSettingsToStorage(); }); $('#csAddRemoveAddressButtonCheckBox').change(function onStreetDetailsButtonCheckChanged() { $('.csAddRemoveAddressButtonCheckBoxContainer').toggle(this.checked); saveSettingsToStorage(); }); $('.csSettingsControl').change(function onSettingsCheckChanged() { const { checked } = this; const $this = $(this); const settingName = $this.data('setting-name'); $this.siblings('.csDropdownContainer').toggle(checked); if (settingName === 'roadType') { const roadType = $this.data('road-type'); const array = _settings.roadTypeButtons; const index = array.indexOf(roadType); if (checked && index === -1) { array.push(roadType); } else if (!checked && index !== -1) { array.splice(index, 1); } } else if (settingName.includes('setNew') && settingName.includes('City')) { _settings[settingName] = $this.val(); } else { _settings[settingName] = checked; } saveSettingsToStorage(); }); } function updateControls() { if ($(roadTypeDropdownSelector).length > 0) { if (isChecked('csRoadTypeButtonsCheckBox')) addRoadTypeButtons(); } addCompactRoadTypeColors(); if (isSwapPedestrianPermitted() && isChecked('csAddSwapPedestrianButtonCheckBox')) { addSwapPedestrianButton(); } // if ($(PARKING_SPACES_DROPDOWN_SELECTOR).length > 0 && isChecked('csParkingSpacesButtonsCheckBox')) { // addParkingSpacesButtons(); // TODO - add option setting // } // if ($(PARKING_COST_DROPDOWN_SELECTOR).length > 0 && isChecked('csParkingCostButtonsCheckBox')) { // addParkingCostButtons(); // TODO - add option setting // } } function replaceWord(target, searchWord, replaceWithWord) { return target.replace(new RegExp(`\\b${searchWord}\\b`, 'g'), replaceWithWord); } function titleCase(word) { return word.charAt(0).toUpperCase() + word.substring(1).toLowerCase(); } function mcCase(word) { return word.charAt(0).toUpperCase() + word.charAt(1).toLowerCase() + word.charAt(2).toUpperCase() + word.substring(3).toLowerCase(); } function upperCase(word) { return word.toUpperCase(); } function processSubstring(target, substringRegex, processFunction) { const substrings = target.match(substringRegex); if (substrings) { for (let idx = 0; idx < substrings.length; idx++) { const substring = substrings[idx]; const newSubstring = processFunction(substring); target = replaceWord(target, substring, newSubstring); } } return target; } function onPaste(e) { const targetNode = e.target; if (targetNode.name === 'streetName' || targetNode.className.includes('street-name')) { // Get the text that's being pasted. let pastedText = e.clipboardData.getData('text/plain'); // If pasting text in ALL CAPS... if (/^[^a-z]*$/.test(pastedText)) { [ // Title case all words first. [/\b[a-zA-Z]+(?:'S)?\b/g, titleCase], // Then process special cases. [/\bMC\w+\b/ig, mcCase], // e.g. McCaulley [/\b(?:I|US|SH|SR|CH|CR|CS|PR|PS)\s*-?\s*\d+\w*\b/ig, upperCase], // e.g. US-25, US25 /* eslint-disable-next-line max-len */ [/\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 [/\b(?:NE|NW|SE|SW)\b/ig, upperCase] ].forEach(item => { pastedText = processSubstring(pastedText, item[0], item[1]); }); // Insert new text in the focused node. document.execCommand('insertText', false, pastedText); // Prevent the default paste behavior. e.preventDefault(); return false; } } return true; } function getTranslationObject() { if (argsObject.useDefaultTranslation) { return defaultTranslation; } let locale = I18n.currentLocale().toLowerCase(); if (!argsObject.translations.hasOwnProperty(locale)) { locale = 'en-us'; } return argsObject.translations[locale]; } function errorHandler(callback) { try { callback(); } catch (ex) { console.error(`${argsObject.scriptName}:`, ex); } } /** * This event handler is needed in the following scenarios: * 1. When the user changes the selected compact road type chip to adjust its styling. * 2. When the switch alternative name button is clicked. */ function onSegmentsChanged() { addCompactRoadTypeColors(); addSwitchPrimaryNameButton(); } async function onCopyCoordinatesShortcut() { try { const center = sdk.Map.getMapCenter(); const output = `${center.lat.toFixed(5)}, ${center.lon.toFixed(5)}`; await navigator.clipboard.writeText(output); WazeWrap.Alerts.info('WME ClickSaver', `Map center coordinate copied to clipboard:\n${output}`, false, false, 2000); // console.debug('Map coordinates copied to clipboard:', center); } catch (err) { console.error('Failed to copy map center coordinates to clipboard: ', err); } } function onToggleDrawNewRoadsAsTwoWayShortcut() { const options = sdk.Settings.getUserSettings(); options.isCreateRoadsAsTwoWay = !options.isCreateRoadsAsTwoWay; sdk.Settings.setUserSettings(options); WazeWrap.Alerts.info('WME ClickSaver', `New segments will be drawn as <b>${options.isCreateRoadsAsTwoWay ? 'two-way' : 'one-way'}</b>.`, false, false, 2000); } function createShortcut(shortcutId, description, callback) { let shortcutKeys = _settings.shortcuts?.[shortcutId] ?? null; if (shortcutKeys && sdk.Shortcuts.areShortcutKeysInUse({ shortcutKeys })) { shortcutKeys = null; } sdk.Shortcuts.createShortcut({ shortcutId, shortcutKeys, description, callback }); } function hideUncheckedRoadTypeButtons() { const selection = getSelectedSegments(); if (!selection) { return; } const selectedRoadTypes = selection .map(segmentId => sdk.DataModel.Segments.getById({ segmentId })) .map(segment => segment.roadType); const checkedRoadTypes = new Set( _settings.roadTypeButtons .map(roadType => roadTypeSettings[roadType]) .map(setting => setting.id) .concat(selectedRoadTypes) .map(id => id.toString()) ); // eslint-disable-next-line func-names $('wz-chip-select.road-type-chip-select wz-checkable-chip').each(function() { const buttonValue = $(this).attr('value'); if (buttonValue === 'MIXED') { return; } if (!checkedRoadTypes.has(buttonValue)) { $(this).parent().parent().remove(); } }); } async function init() { logDebug('Initializing...'); trans = getTranslationObject(); Object.keys(roadTypeSettings).forEach(rtName => { roadTypeSettings[rtName].text = trans.roadTypeButtons[rtName].text; }); document.addEventListener('paste', onPaste); sdk.Events.trackDataModelEvents({ dataModelName: 'segments' }); sdk.Events.on({ eventName: 'wme-data-model-objects-changed', eventHandler: () => errorHandler(onSegmentsChanged) }); sdk.Events.on({ eventName: 'wme-selection-changed', eventHandler: () => errorHandler(updateControls) }); // check for changes in the edit-panel const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { for (let i = 0; i < mutation.addedNodes.length; i++) { const addedNode = mutation.addedNodes[i]; if (addedNode.nodeType === Node.ELEMENT_NODE) { // Checks to identify if this is a segment in regular display mode. if (addedNode.querySelector(roadTypeDropdownSelector)) { if (isChecked('csRoadTypeButtonsCheckBox')) addRoadTypeButtons(); if (isSwapPedestrianPermitted() && isChecked('csAddSwapPedestrianButtonCheckBox')) { addSwapPedestrianButton(); } } // Checks to identify if this is a segment in compact display mode. if (addedNode.querySelector(roadTypeChipSelector)) { if (isChecked('csRoadTypeButtonsCheckBox')) { addCompactRoadTypeChangeEvents(); } if (isSwapPedestrianPermitted() && isChecked('csAddSwapPedestrianButtonCheckBox')) { addSwapPedestrianButton(); } if (isChecked('csHideUncheckedRoadTypeButtonsCheckBox')) { hideUncheckedRoadTypeButtons(); } } // if (addedNode.querySelector(PARKING_SPACES_DROPDOWN_SELECTOR) && isChecked('csParkingSpacesButtonsCheckBox')) { // addParkingSpacesButtons(); // } // if (addedNode.querySelector(PARKING_COST_DROPDOWN_SELECTOR) // && isChecked('csParkingCostButtonsCheckBox')) { // addParkingCostButtons(); // } if (addedNode.querySelector('.side-panel-section') && (isChecked('csAddAltCityButtonCheckBox') || isChecked('csAddRemoveAddressButtonCheckBox'))) { createSharedAddressButtonContainer(); if (isChecked('csAddRemoveAddressButtonCheckBox')) { addRemoveAddressButton(); } if (isChecked('csAddAltCityButtonCheckBox')) { addAddAltCityButton(); } } if (addedNode.querySelector('.alt-streets') && isChecked('csAddSwitchPrimaryNameCheckBox')) { // Cancel button doesn't change the datamodel so re-add the switch arrow on cancel click // eslint-disable-next-line func-names addedNode.addEventListener('click', event => { if (event.target.classList.contains('alt-address-cancel-button')) { addSwitchPrimaryNameButton(); } }); addSwitchPrimaryNameButton(); } } } }); }); observer.observe(document.getElementById('edit-panel'), { childList: true, subtree: true }); await initUserPanel(); loadSettingsFromStorage(); createShortcut('toggleTwoWaySegDrawingShortcut', 'Toggle new segment two-way drawing', onToggleDrawNewRoadsAsTwoWayShortcut); createShortcut('copyCoordinatesShortcut', 'Copy map center coordinates', onCopyCoordinatesShortcut); window.addEventListener('beforeunload', saveSettingsToStorage, false); injectCss(); updateControls(); // In case of PL w/ segments selected. logDebug('Initialized'); } function createSharedAddressButtonContainer() { const $addressEdit = $('div.address-edit'); const $wzLabel = $addressEdit.prev('wz-label'); const $container = $('<div>', { style: 'display: flex; gap: 0.5em; place-content: flex-end;', id: 'csAddressButtonContainer' }); if ($wzLabel.css('display') === 'none') { $container.css('padding-bottom', '4px'); } else { $container.append($wzLabel); } $addressEdit.before($container); } function skipLoginDialog(tries = 0) { if (sdk || tries === 1000) return; if ($('wz-button.do-login').length) { $('wz-button.do-login').click(); return; } setTimeout(skipLoginDialog, 100, ++tries); } skipLoginDialog(); sdk = await bootstrap({ scriptUpdateMonitor: { downloadUrl } }); init(); } // END clicksaver function (used to be injected, now just runs as a function) // function exists(...objects) { // return objects.every(object => typeof object !== 'undefined' && object !== null); // } function injectScript(argsObject) { // 3/31/2023 - removing script injection due to loading errors that I can't track down ("require is not defined"). // Not sure if injection is needed anymore. I believe it was to get around an issue with Greasemonkey / Firefox. clicksaver(argsObject); // if (exists(require, $)) { // GM_addElement('script', { // textContent: `(function(){${clicksaver.toString()}\n clicksaver(${JSON.stringify(argsObject).replace('\'', '\\\'')})})();` // }); // } else { // setTimeout(() => injectScript(argsObject), 250); // } } function setValue(object, path, value) { const pathParts = path.split('.'); for (let i = 0; i < pathParts.length - 1; i++) { const pathPart = pathParts[i]; if (pathPart in object) { object = object[pathPart]; } else { object[pathPart] = {}; object = object[pathPart]; } } object[pathParts[pathParts.length - 1]] = value; } function convertTranslationsArrayToObject(arrayIn) { const translations = {}; let iRow; let iCol; const languages = arrayIn[0].map(lang => lang.toLowerCase()); for (iCol = 1; iCol < languages.length; iCol++) { translations[languages[iCol]] = {}; } for (iRow = 1; iRow < arrayIn.length; iRow++) { const row = arrayIn[iRow]; const propertyPath = row[0]; for (iCol = 1; iCol < row.length; iCol++) { setValue(translations[languages[iCol]], propertyPath, row[iCol]); } } return translations; } function loadTranslations() { if (typeof $ === 'undefined') { setTimeout(loadTranslations, 250); console.debug('ClickSaver:', 'jQuery not ready. Retry loading translations...'); } else { // This call retrieves the data from the translations spreadsheet and then injects // the main code into the page. If the spreadsheet call fails, the default English // translation is used. const args = { scriptName, scriptVersion, forumUrl }; $.getJSON(`${translationsUrl}?${DEC(apiKey)}`).then(res => { args.translations = convertTranslationsArrayToObject(res.values); console.debug('ClickSaver:', 'Translations loaded.'); }).fail(() => { console.error('ClickSaver: Error loading translations spreadsheet. Using default translation (English).'); args.useDefaultTranslation = true; }).always(() => { // Leave this document.ready function. Some people randomly get a "require is not defined" error unless the injectMain function // is called late enough. Even with a "typeof require !== 'undefined'" check. $(document).ready(() => { injectScript(args); }); }); } } function sandboxBootstrap() { if (WazeWrap?.Ready) { WazeWrap.Interface.ShowScriptUpdate(scriptName, scriptVersion, updateMessage, forumUrl); } else { setTimeout(sandboxBootstrap, 250); } } // Go ahead and start loading translations, and inject the main code into the page. loadTranslations(); // Start the "sandboxed" code. sandboxBootstrap(); })();