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