您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Bán tự động điều hướng, chọn và cập nhật thành phố cho segment hoặc place trong WME từ file Excel & CSV.
当前为
// ==UserScript== // @name WME City Name Update // @namespace https://greasyfork.org/ // @version 1.0.5 // @description Bán tự động điều hướng, chọn và cập nhật thành phố cho segment hoặc place trong WME từ file Excel & CSV. // @author Minh Tan // @match https://www.waze.com/editor* // @match https://www.waze.com/*/editor* // @match https://beta.waze.com/editor* // @match https://beta.waze.com/*/editor* // @exclude https://www.waze.com/*user/editor* // @grant none // @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js // ==/UserScript== /* global W, WazeWrap, $, XLSX */ (function () { 'use strict'; let permalinks = []; let currentIndex = -1; function waitForElement(selector, timeout = 7000) { return new Promise((resolve, reject) => { const intervalTime = 100; let elapsedTime = 0; const interval = setInterval(() => { const element = document.querySelector(selector); if (element && element.offsetParent !== null) { clearInterval(interval); resolve(element); } elapsedTime += intervalTime; if (elapsedTime >= timeout) { clearInterval(interval); reject(new Error(`Element "${selector}" not found or not visible after ${timeout}ms`)); } }, intervalTime); }); } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function bootstrap() { if (W && W.map && W.model && W.loginManager.user && $ && WazeWrap.Ready) { init(); } else { setTimeout(bootstrap, 500); } } function init() { console.log("WME Batch Navigator & Updater: Initialized"); createUI(); registerHotkeys(); } function createUI() { const panel = document.createElement('div'); panel.id = 'batch-updater-panel'; panel.style.cssText = ` position: fixed; top: 80px; left: 15px; background: #fff; border: 1px solid #ccc; padding: 10px; z-index: 1000; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); font-family: sans-serif; font-size: 14px; width: 280px; `; panel.innerHTML = ` <h3 id="navigator-header" style="margin-top:0; cursor: move;">Batch Updater</h3> <div> <label for="excel_file">1. Tải file Excel:</label> <input type="file" id="excel_file" accept=".xlsx, .xls, .csv" style="margin-top:5px; width: 100%;"/> </div> <div style="margin-top: 5px;"> <label for="url_column">Cột chứa URL:</label> <input type="text" id="url_column" value="J" size="2" style="text-transform: uppercase;"> </div> <div id="status_info" style="margin-top:10px; font-style:italic;">Chưa có file nào được tải.</div> <hr style="margin: 10px 0;"> <div style="margin-top: 10px; display: flex; justify-content: space-between; align-items: center;"> <button id="prev_btn" disabled>◀ Trước</button> <input type="number" id="nav_index_input" min="1" style="width: 50px; text-align: center;" disabled> <span id="nav_total_count">/ N/A</span> <button id="next_btn" disabled> Tiếp theo ▶</button> </div> <button id="reselect_btn" style="width: 100%; margin-top: 10px; background-color: #f0ad4e; color: white; padding: 10px; border: none; cursor: pointer;" disabled>Tải lại & Chọn</button> <button id="update_city_btn" style="width: 100%; margin-top: 10px; background-color: #4CAF50; color: white; padding: 10px; border: none; cursor: pointer;" disabled>Áp dụng T.Phố</button> <div> <label>2. Loại đối tượng:</label><br> <input type="radio" id="type_street" name="object_type" value="street" checked> <label for="type_street">Đường (Street)</label><br> <input type="radio" id="type_place" name="object_type" value="place"> <label for="type_place">Địa điểm (Place)</label> </div> <div style="margin-top: 10px;"> <label for="new_city_name">3. Tên thành phố:</label> <input type="text" id="new_city_name" placeholder="Nhập tên thành phố..." style="width: 95%; margin-top:5px;"/> </div> <div id="log_info" style="margin-top:10px; font-size: 12px; height: 80px; overflow-y: auto; border: 1px solid #eee; padding: 5px; background: #f8f9fa;"></div> `; document.body.appendChild(panel); document.getElementById('excel_file').addEventListener('change', handleFile, false); document.getElementById('prev_btn').addEventListener('click', () => navigate(-1)); document.getElementById('next_btn').addEventListener('click', () => navigate(1)); document.getElementById('reselect_btn').addEventListener('click', processCurrentLink); document.getElementById('update_city_btn').addEventListener('click', updateCityForSelection); document.getElementById('nav_index_input').addEventListener('keypress', (e) => { if (e.key === 'Enter') { const targetIndex = parseInt(e.target.value, 10); if (!isNaN(targetIndex)) { navigate(0, targetIndex - 1); } else { log('Vui lòng nhập một số hợp lệ cho chỉ số.'); } } }); makeDraggable(panel, document.getElementById('navigator-header')); } function makeDraggable(panel, header) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; header.onmousedown = (e) => { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = () => { document.onmouseup = null; document.onmousemove = null; }; document.onmousemove = (e) => { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; panel.style.top = (panel.offsetTop - pos2) + "px"; panel.style.left = (panel.offsetLeft - pos1) + "px"; }; }; } function log(message) { const logBox = document.getElementById('log_info'); if (logBox) { logBox.innerHTML = `[${new Date().toLocaleTimeString()}] ${message}<br>` + logBox.innerHTML; } console.log(`[Batch Updater] ${message}`); } function handleFile(e) { permalinks = []; currentIndex = -1; const file = e.target.files[0]; const urlColumnInput = document.getElementById('url_column').value.toUpperCase(); const urlColumnIndex = urlColumnInput.charCodeAt(0) - 'A'.charCodeAt(0); if (urlColumnIndex < 0 || urlColumnIndex > 25) { log(`Lỗi: Cột "${urlColumnInput}" không hợp lệ. Vui lòng nhập A-Z.`); return; } const reader = new FileReader(); reader.onload = (e) => { const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: 'array' }); const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; const json = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); for (let i = 1; i < json.length; i++) { const row = json[i]; if (row && row.length > urlColumnIndex) { const permalink = row[urlColumnIndex]; if (permalink && typeof permalink === 'string' && (permalink.includes('waze.com/editor') || permalink.includes('waze.com/ul'))) { permalinks.push(permalink); } } } if (permalinks.length > 0) { currentIndex = 0; updateUIState(); processCurrentLink(); } else { log(`Không tìm thấy URL hợp lệ trong cột ${urlColumnInput} của file.`); updateUIState(); } updateUIState(); }; reader.readAsArrayBuffer(file); } function updateUIState() { const hasLinks = permalinks.length > 0; const navIndexInput = document.getElementById('nav_index_input'); const navTotalCount = document.getElementById('nav_total_count'); document.getElementById('prev_btn').disabled = !hasLinks || currentIndex <= 0; document.getElementById('next_btn').disabled = !hasLinks || currentIndex >= permalinks.length - 1; document.getElementById('reselect_btn').disabled = !hasLinks; document.getElementById('update_city_btn').disabled = !hasLinks; navIndexInput.disabled = !hasLinks; navIndexInput.max = permalinks.length; if (hasLinks) { navIndexInput.value = currentIndex + 1; navTotalCount.textContent = ` / ${permalinks.length}`; document.getElementById('status_info').textContent = `Đã tải ${permalinks.length} URL.`; } else { navIndexInput.value = ''; navTotalCount.textContent = '/ N/A'; document.getElementById('status_info').textContent = 'Chưa có file nào được tải.'; } } function registerHotkeys() { document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return; } switch (e.key) { case 'ArrowRight': // Mũi tên phải: Next e.preventDefault(); document.getElementById('next_btn').click(); break; case 'ArrowLeft': // Mũi tên trái: Previous e.preventDefault(); document.getElementById('prev_btn').click(); break; case 'ArrowUp': // Mũi tên lên: Reselect e.preventDefault(); document.getElementById('reselect_btn').click(); break; case 'ArrowDown': // Mũi tên xuống: Update City e.preventDefault(); document.getElementById('update_city_btn').click(); break; } }); } function navigate(direction, targetIndex = null) { if (permalinks.length === 0) { log('Chưa có URL nào được tải.'); return; } let newIndex; if (targetIndex !== null) { newIndex = targetIndex; } else { newIndex = currentIndex + direction; } if (newIndex >= 0 && newIndex < permalinks.length) { currentIndex = newIndex; updateUIState(); processCurrentLink(); } else { log('Đã ở đầu hoặc cuối danh sách, hoặc chỉ số không hợp lệ.'); updateUIState(); } } function processCurrentLink() { if (currentIndex < 0 || currentIndex >= permalinks.length) return log('Vị trí không hợp lệ.'); const url = permalinks[currentIndex]; parseWazeUrlAndNavigate(url); } async function parseWazeUrlAndNavigate(url) { try { const params = new URL(url).searchParams; const lon = parseFloat(params.get('lon')); const lat = parseFloat(params.get('lat')); const zoom = parseInt(params.get('zoomLevel') || params.get('zoom'), 10) + 2; const segmentIDs = (params.get('segments') || '').split(',').filter(id => id); const venueIDs = (params.get('venues') || '').split(',').filter(id => id); if (!lon || !lat) { log('Lỗi: URL không chứa tọa độ (lon/lat).'); return; } W.map.setCenter(WazeWrap.Geometry.ConvertTo900913(lon, lat), zoom); await delay(100); WazeWrap.Model.onModelReady(() => { (async () => { await delay(500); let objectsToSelect = []; if (segmentIDs.length > 0) { const segments = segmentIDs.map(id => W.model.segments.getObjectById(id)).filter(Boolean); if (segments.length === 0) { log(`Cảnh báo: Không tìm thấy segment nào trên bản đồ từ ID ${segmentIDs.join(',')} sau khi tải.`); } else { objectsToSelect.push(...segments); } } if (venueIDs.length > 0) { const venues = venueIDs.map(id => W.model.venues.getObjectById(id)).filter(Boolean); if (venues.length === 0) { log(`Cảnh báo: Không tìm thấy venue nào trên bản đồ từ ID ${venueIDs.join(',')} sau khi tải.`); } else { objectsToSelect.push(...venues); } } if (objectsToSelect.length > 0) { W.selectionManager.setSelectedModels(objectsToSelect); } else { log('Cảnh báo: Không tìm thấy đối tượng nào trên bản đồ từ ID trong URL.'); } })(); }, true); } catch (error) { log(`Lỗi khi xử lý URL: ${error.message}`); console.error(error); } } async function updateCityForSelection() { const newCityName = document.getElementById('new_city_name').value.trim(); if (!newCityName) { alert('Vui lòng nhập tên thành phố mới!'); return; } const objectType = document.querySelector('input[name="object_type"]:checked').value; try { if (objectType === 'street') { const previewSpanSelector = document.querySelector("#segment-edit-general > div:nth-child(1) > div > div > div.preview > wz-card > div > span") if (previewSpanSelector && previewSpanSelector.textContent.includes('Multiple streets')) { log('Phát hiện nhiều đối tượng đang được chọn (Multiple). Không thể cập nhật thành phố.'); alert('Không thể cập nhật thành phố khi có nhiều đối tượng được chọn (Multiple).'); return; } (async () => { try { const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); await document.querySelectorAll('.w-icon.w-icon-pencil-fill.edit-button').forEach(btn => btn.click()); await delay(100); const input = await document.querySelector("#segment-edit-general > div:nth-child(1) > div > div > wz-card > form > div:nth-child(3) > wz-autocomplete").shadowRoot.querySelector("#text-input"); await delay(100); input.value = newCityName; input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); await delay(100); await document.querySelector("#segment-edit-general > div:nth-child(1) > div > div > wz-card > form > div:nth-child(3) > wz-autocomplete").shadowRoot.querySelector("div > wz-menu > wz-menu-item:nth-child(2) > div > div > div > wz-autocomplete-item-text > div").click() await delay(100); await document.querySelectorAll(".save-button").forEach(btn => btn.click()); } catch (err) { console.error("❌ Lỗi:", err); } })(); } else { (async () => { try { const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); await document.querySelectorAll('.w-icon.w-icon-pencil-fill.edit-button').forEach(btn => btn.click()); await delay(100); const input = await document.querySelector("#venue-edit-general > div:nth-child(1) > div > div.address-edit-view > wz-card > form > div:nth-child(4) > wz-autocomplete").shadowRoot.querySelector("#text-input"); await delay(100); input.value = newCityName; input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); await delay(100); await document.querySelector("#venue-edit-general > div:nth-child(1) > div > div > wz-card > form > div:nth-child(4) > wz-autocomplete").shadowRoot.querySelector("div > wz-menu > wz-menu-item:nth-child(2) > div > div > div > wz-autocomplete-item-text > div").click() await delay(100); await document.querySelectorAll(".save-button").forEach(btn => btn.click()); } catch (err) { console.error("❌ Lỗi:", err); } })(); } } catch (err) { console.error("❌ Lỗi:", err); } } bootstrap(); })();