WME City Name Update

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.

当前为 2025-07-18 提交的版本,查看 最新版本

// ==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();
})();