Waze右下の緯度経度表示を高精度に検出してGoogleマップへ。Googleマップ側からはWazeライブマップへ同座標で戻す
// ==UserScript==
// @name Waze ⇄ Google Maps Jump
// @namespace aoi-tools
// @version 5.0
// @description Waze右下の緯度経度表示を高精度に検出してGoogleマップへ。Googleマップ側からはWazeライブマップへ同座標で戻す
// @author Rakuten AI
// @match https://www.waze.com/ja/live-map/*
// @match https://www.waze.com/live-map/*
// @match https://www.google.co.jp/maps/*
// @match https://www.google.com/maps/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
let currentLat = null;
let currentLon = null;
/******************************
* ボタン作成 (位置記憶・ドラッグ可能)
******************************/
function createBtn(id, label, color, onClick) {
const existing = document.getElementById(id);
if (existing) return existing;
const btn = document.createElement('button');
btn.id = id;
btn.textContent = label;
const saved = JSON.parse(localStorage.getItem(id) || '{"x":100,"y":100}');
Object.assign(btn.style, {
position: 'fixed',
left: saved.x + 'px',
top: saved.y + 'px',
zIndex: 999999,
padding: '10px 15px',
background: color,
color: 'white',
border: '2px solid white',
borderRadius: '20px',
cursor: 'move',
fontSize: '13px',
fontWeight: 'bold',
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
opacity: '0.70',
userSelect: 'none'
});
// ドラッグ誤判定対策:一定距離以上動いたらドラッグ扱い
let startClientX = 0, startClientY = 0;
let startLeft = 0, startTop = 0;
let dragging = false;
const DRAG_THRESHOLD_PX = 6;
const onMouseMove = (ev) => {
const dx = ev.clientX - startClientX;
const dy = ev.clientY - startClientY;
if (!dragging) {
if (Math.hypot(dx, dy) < DRAG_THRESHOLD_PX) return;
dragging = true;
}
btn.style.left = (startLeft + dx) + 'px';
btn.style.top = (startTop + dy) + 'px';
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
localStorage.setItem(id, JSON.stringify({
x: parseInt(btn.style.left, 10),
y: parseInt(btn.style.top, 10)
}));
// mouseup直後のclickで誤発火しないように少し遅延で解除
setTimeout(() => { dragging = false; }, 0);
};
btn.addEventListener('mousedown', (e) => {
startClientX = e.clientX;
startClientY = e.clientY;
startLeft = btn.offsetLeft;
startTop = btn.offsetTop;
dragging = false;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
btn.addEventListener('click', () => {
if (!dragging) onClick();
});
document.body.appendChild(btn);
return btn;
}
/******************************
* 座標パース("lat | lon" / "lat, lon" / "lat lon" など対応)
******************************/
function parseLatLonFromText(text) {
if (!text) return null;
const t = String(text).replace(/\u00A0/g, ' ').trim(); // NBSP対策
// lat: -90..90, lon: -180..180 の小数
const re = /(-?\d{1,2}\.\d+)\s*(?:\||,|\s)\s*(-?\d{1,3}\.\d+)/;
const m = t.match(re);
if (!m) return null;
const lat = parseFloat(m[1]);
const lon = parseFloat(m[2]);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
if (lat < -90 || lat > 90) return null;
if (lon < -180 || lon > 180) return null;
return { lat, lon };
}
/******************************
* Waze: 右下表示を「画面から」検出(誤検出・負荷対策)
* - 右下エリア中心に限定
* - 可視性/サイズ/テキスト長で絞り込み
******************************/
function findCoordsOnScreenPreferBottomRight() {
const vw = window.innerWidth;
const vh = window.innerHeight;
// 右下エリアに限定(負荷&誤検出対策)
const regionLeft = vw * 0.55;
const regionTop = vh * 0.55;
const candidates = [];
const nodes = document.querySelectorAll('div, span, p, small, button, a, li');
for (const el of nodes) {
const rect = el.getBoundingClientRect();
// 右下エリア外はスキップ
if (rect.right < regionLeft || rect.bottom < regionTop) continue;
// 画面外/小さすぎる要素はスキップ
if (rect.width < 30 || rect.height < 10) continue;
if (rect.bottom < 0 || rect.right < 0 || rect.top > vh || rect.left > vw) continue;
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden') continue;
if (parseFloat(style.opacity || '1') === 0) continue;
const text = (el.innerText || el.textContent || '').trim();
if (!text) continue;
if (text.length > 60) continue; // 長文は誤検出しやすい
const parsed = parseLatLonFromText(text);
if (!parsed) continue;
// 右下に近いほど優先
const dist = Math.hypot(vw - rect.right, vh - rect.bottom);
// 追加のスコア:座標表示っぽい短さを少し優遇
const lenPenalty = Math.max(0, text.length - 25) * 2;
candidates.push({
lat: parsed.lat,
lon: parsed.lon,
dist: dist + lenPenalty,
raw: text
});
}
if (candidates.length === 0) return null;
candidates.sort((a, b) => a.dist - b.dist);
return candidates[0];
}
/******************************
* Google Maps: URLから座標抽出(複数形式対応)
******************************/
function getLatLonFromGoogleUrl(url) {
// 1) /@lat,lon,zoom
let m = url.match(/@(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)/);
if (m) return { lat: parseFloat(m[1]), lon: parseFloat(m[2]) };
// 2) ?q=lat,lon
m = url.match(/[?&]q=(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)/);
if (m) return { lat: parseFloat(m[1]), lon: parseFloat(m[2]) };
// 3) ?query=lat,lon (search api)
m = url.match(/[?&]query=(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)/);
if (m) return { lat: parseFloat(m[1]), lon: parseFloat(m[2]) };
// 4) Place/ルート系: !3dlat!4dlon
m = url.match(/!3d(-?\d+(?:\.\d+)?)!4d(-?\d+(?:\.\d+)?)/);
if (m) return { lat: parseFloat(m[1]), lon: parseFloat(m[2]) };
return null;
}
/******************************
* Waze側
******************************/
if (location.host.includes('waze.com')) {
const jumpBtn = createBtn('jump-to-gmaps', '→ Google Maps', '#4285F4', () => {
if (currentLat != null && currentLon != null) {
// zoom安定:@lat,lon,17z
const url = `https://www.google.com/maps/@${currentLat},${currentLon},17z`;
window.location.href = url;
} else {
alert('右下の座標が読み取れていません。地図を少し動かして座標表示が出た状態で押してください。');
}
});
let lastKey = '';
let scheduled = false;
const update = () => {
scheduled = false;
const found = findCoordsOnScreenPreferBottomRight();
if (!found) return;
const key = `${found.lat},${found.lon}`;
if (key === lastKey) return;
lastKey = key;
currentLat = found.lat;
currentLon = found.lon;
jumpBtn.style.opacity = '1';
jumpBtn.textContent = `→ Google Maps (${currentLat.toFixed(5)}, ${currentLon.toFixed(5)})`;
};
// MutationObserverはデバウンスして負荷を抑える
const requestUpdate = () => {
if (scheduled) return;
scheduled = true;
setTimeout(update, 120);
};
const obs = new MutationObserver(() => requestUpdate());
obs.observe(document.documentElement, { childList: true, subtree: true, characterData: true });
// 保険の定期更新(低頻度)
setInterval(() => requestUpdate(), 2500);
requestUpdate();
}
/******************************
* Google Maps側
******************************/
if (location.host.includes('google.')) {
createBtn('jump-to-waze', '→ Waze Live Map', '#33CCFF', () => {
const ll = getLatLonFromGoogleUrl(window.location.href);
if (!ll) {
alert('URLに座標が含まれていません。地図を少し動かしてURLが更新されてから押してください。');
return;
}
// ライブマップで同座標を表示(アプリが開く場合あり)
const url = `https://www.waze.com/ul?ll=${ll.lat},${ll.lon}&navigate=no`;
window.location.href = url;
});
}
})();