WME Switch Uturns
選択したノードまたはセグメントの U ターンを切り替えます。
「WME ノードから Uturn を追加」スクリプト
https://greasyfork.org/ja/scripts/38084-wme-switch-uturns
WAZEPT Segments
https://greasyfork.org/ja/scripts/406000-wazept-segments
単線の道路を2分割にしてくれるScriptです
分割したい道路を選択して下さい。
スケールを使って分割したい距離を測定し、必要に応じて変更してください。
次に「Split the segments」をクリックします。
道路が画像のように分割されます。
注意:日本仕様のスクリプトではないため、分割後の進行方向が逆になります。進行方向を手動で変更してください。
注意
レーン案内が設定されている場合、道路を分割することはできません。分割する前に、レーン案内を解除してください。

Coordinate Input to Jartic Map2
https://greasyfork.org/en/scripts/526803-coordinate-input-to-jartic-map2
使用方法
- Wazeエディターの右上にある🔗をクリックし、コーディネートの座標をコピーします。
- 「ペースト」ボタンをクリックすると、基準となる座標が貼り付けられます。
- 「JARTIC」ボタンをクリックすると、交通規制情報マップが開きます。
※本スクリプトは日本国内のみ対応しています。
※すでに座標が入力されていても、新しい座標をペーストできます。
交通規制マップ→Wazeエディター2
座標を事前に設定し、リロード後に新しいタブでWazeエディターを開くボタンを追加します。
https://greasyfork.org/en/scripts/527874-%E4%BA%A4%E9%80%9A%E8%A6%8F%E5%88%B6%E3%83%9E%E3%83%83%E3%83%97-waze%E3%82%A8%E3%83%87%E3%82%A3%E3%82%BF%E3%83%BC%EF%BC%92
交通規制マップ→Wazeエディター2 ユーザースクリプト 説明書
概要
本スクリプトは、特定の交通規制マップ(http://hotmist.ddo.jp/)のURLから座標情報を取得し、 ページリロード後にWazeエディターを新しいタブで開くボタンを追加するものです。
動作概要
- 交通規制マップのURLから座標(緯度・経度)を取得
- 座標が適切な範囲内かチェック
- Wazeエディターの編集画面のURLを生成
- 「リロード後にWazeへ」ボタンをページに追加
- ボタンをクリックすると、ページをリロードし、リロード後にWazeエディターを新しいタブで開く
インストール方法
- Tampermonkeyなどのユーザースクリプト管理拡張機能をインストール
- 本スクリプトを新しいユーザースクリプトとして登録
- 交通規制マップのページ(http://hotmist.ddo.jp/)を開いて動作を確認
仕様詳細
- 対応サイト:
http://hotmist.ddo.jp/* - スクリプトの動作:
- URL内に
@緯度,経度の形式で座標が含まれている場合、その値を取得 - 取得した座標をWazeエディターのURLに組み込み、リロード後に自動で開く
- 無効な座標の場合は警告メッセージを表示
- ボタンの位置は画面左下(固定)
- URL内に
注意事項
- 交通規制マップのURLに座標が含まれていない場合、スクリプトは動作しません。
- 取得した座標が無効な場合、Wazeエディターは開きません。
- スクリプトの動作にはJavaScriptが有効になっている必要があります。
更新履歴
- v1.6(最新): Wazeエディターを開く処理を最適化、UIの改良
免責事項
本スクリプトの使用によって発生したいかなる損害についても、開発者は責任を負いません。 自己責任のもとご利用ください。
WME
E50 POIデータ取得
日本語対応に変更しました
https://greasyfork.org/ja/scripts/531209-wme-e50-poi%E3%83%87%E3%83%BC%E3%82%BF%E5%8F%96%E5%BE%97
Tamperモンキーの代用品で使えます
OrangeMonkey
https://chromewebstore.google.com/detail/orangemonkey/ekmeppjgajofkpiofbebgcbohbmfldaf
WME EZRoad Mod 説明
WME EZRoad Modは、Waze Map Editor(WME)用のユーザースクリプトで、道路セグメントの編集を簡単かつ効率的に行うためのツールです。このスクリプトは、道路タイプ、ロックレベル、制限速度、舗装状態、道路名や市区町村の設定などを迅速に更新する機能を提供します。特に、Wazeの編集作業を効率化したいエディター向けに設計されており、ショートカットキーや自動保存機能、接続セグメントからの属性コピーなどの便利な機能が含まれています。
主な機能
- 道路タイプの簡単な変更
- 高速道路、国道、県道、一般道路、歩道など、16種類の道路タイプを簡単に選択・適用可能。
- 各道路タイプには専用のショートカットキー(例:S+1で高速道路、A+4で歩道)が割り当てられており、キーボード操作で迅速に切り替え可能。
- ロックレベルの設定
- 選択した道路タイプに応じて、ロックレベル(L1~L6またはHRCS)を設定可能。
- HRCS(Highest Rank Connected Segment)は、接続セグメントの中で最も高いロックレベルを自動的に適用。
- 制限速度の設定
- 道路タイプごとに制限速度を設定可能。速度を未設定(-1または0)にもできる。
- 設定した速度は前方・後方両方向に適用。
- 舗装/未舗装の切り替え
- セグメントを舗装または未舗装に設定可能。歩行者系道路(歩道、歩行者エリア、階段)は自動的に舗装状態に設定。
- UIのコンパクトモードと非コンパクトモードの両方に対応。
- 道路名と市区町村の管理
- 道路名を未設定にする:道路名を空にし、代替道路名も削除。
- 市区町村を未設定にする:市区町村を空に設定。
- 接続セグメントから名称をコピー:接続されたセグメントの道路名や市区町村をコピー。
- 接続セグメントから属性をコピー:速度、道路タイプ、ロックレベル、舗装状態、道路名、市区町村を一括でコピー。
- 自動保存
- 更新後に自動的に変更を保存するオプション。手動保存の手間を省略。
- 設定のエクスポート/インポート
- ロックレベルや制限速度の設定をJSON形式でエクスポート・インポート可能。設定の共有やバックアップが簡単。
- ショートカットキー
- デフォルトのショートカットキー(G)で「クイック更新」を実行。
- 各道路タイプに個別のショートカットキーを設定可能。
- ショートカットキーの競合が発生した場合、キーを無効化してWMEのUIで設定変更可能。
- UIの統合
- WMEのサイドバーに専用の「EZRoads Mod」タブを追加し、道路タイプ、ロックレベル、制限速度、追加オプションを直感的に設定可能。
- チェックボックスやラジオボタンでオプションを簡単に切り替え。
- ツールチップで各オプションの説明を表示。
- 歩行者系道路の特別対応
- 歩道、歩行者エリア、階段とその他の道路タイプ間での切り替え時に確認ダイアログを表示。
- セグメントを削除して再作成するプロセスを採用し、名称や属性のコピーもサポート。
GoogleマップからWazeエディターへ飛べるスクリプトです
tamperモンキーかオレンジモンキーを使用して使ってください。
// ==UserScript==
// @name Google Maps + Waze + OpenStreetMap Button + Address Search
// @namespace https://www.google.com/maps/place/
// @version 1.4.0
// @description GoogleマップからWaze(ライブマップ/エディター)およびOpenStreetMapで開くボタン&選択テキストをGoogleマップ検索(日本語対応、SPA対応、DMS座標対応)
// @match https://www.google.com/maps/*
// @match https://www.google.co.jp/maps/*
// @match https://maps.google.com/*
// @match https://maps.google.co.jp/*
// @icon https://pngimg.com/uploads/waze/waze_PNG21.png
// @grant none
// @author 碧いうさぎ
// ==/UserScript==
(function () {
'use strict';
// ==========================
// Googleマップ → Waze/OpenStreetMap 機能
// ==========================
function convertZoom(googleZoom, isMeter = false) {
let baseZoom;
if (isMeter) {
const meters = parseFloat(googleZoom);
if (meters <= 50) baseZoom = 19;
else if (meters <= 100) baseZoom = 18;
else if (meters <= 200) baseZoom = 17;
else if (meters <= 500) baseZoom = 16;
else if (meters <= 1000) baseZoom = 15;
else if (meters <= 2000) baseZoom = 14;
else if (meters <= 5000) baseZoom = 13;
else if (meters <= 10000) baseZoom = 12;
else if (meters <= 20000) baseZoom = 11;
else baseZoom = 10;
} else {
const googleZoomLevel = parseFloat(googleZoom);
if (googleZoomLevel >= 20) baseZoom = 19;
else if (googleZoomLevel >= 18) baseZoom = 18;
else if (googleZoomLevel >= 16) baseZoom = 17;
else if (googleZoomLevel >= 14) baseZoom = 16;
else if (googleZoomLevel >= 12) baseZoom = 15;
else if (googleZoomLevel >= 10) baseZoom = 14;
else if (googleZoomLevel >= 8) baseZoom = 13;
else if (googleZoomLevel >= 6) baseZoom = 12;
else if (googleZoomLevel >= 4) baseZoom = 11;
else baseZoom = 10;
}
return Math.min(baseZoom + 2, 19);
}
function dmsToDecimal(dms) {
const dmsRegex = /(\d+)°(\d+)'(\d+\.?\d*)"(N|S|E|W)/;
const match = dms.match(dmsRegex);
if (!match) return null;
const degrees = parseFloat(match[1]);
const minutes = parseFloat(match[2]);
const seconds = parseFloat(match[3]);
const direction = match[4];
let decimal = degrees + (minutes / 60) + (seconds / 3600);
if (direction === 'S' || direction === 'W') {
decimal = -decimal;
}
return decimal.toFixed(6);
}
function extractCoordinatesAndZoom() {
try {
const url = decodeURIComponent(window.location.href);
const viewRegex = /@(-?\d+\.\d+),(-?\d+\.\d+),(\d+\.?\d*[mz])/;
const placeRegex = /!3m1!1e3!4m\d+!3m\d+!1s0x[0-9a-f]+:0x[0-9a-f]+!8m2!3d(-?\d+\.\d+)!4d(-?\d+\.\d+)/;
const pinRegex = /!3d(-?\d+\.\d+)!4d(-?\d+\.\d+)/;
const dmsRegex = /\/place\/(\d+%C2%B0\d+'[\d.]+%22[N|S]\+?\d+%C2%B0\d+'[\d.]+%22[E|W])/;
const queryRegex = /[?&]q=([^&]+)/;
let lat, lon, zoom;
const viewMatch = url.match(viewRegex);
const placeMatch = url.match(placeRegex);
const pinMatch = url.match(pinRegex);
const dmsMatch = url.match(dmsRegex);
const queryMatch = url.match(queryRegex);
if (dmsMatch) {
const dmsParts = decodeURIComponent(dmsMatch[1]).split('+');
if (dmsParts.length === 2) {
lat = dmsToDecimal(dmsParts[0]);
lon = dmsToDecimal(dmsParts[1]);
const zoomMatch = url.match(/@(-?\d+\.\d+),(-?\d+\.\d+),(\d+\.?\d*[mz])/);
zoom = zoomMatch ? convertZoom(zoomMatch[3].replace(/[mz]/, ''), zoomMatch[3].endsWith('m')) : 19;
}
} else if (placeMatch) {
lat = placeMatch[1];
lon = placeMatch[2];
const zoomMatch = url.match(/@(-?\d+\.\d+),(-?\d+\.\d+),(\d+\.?\d*[m])/);
zoom = zoomMatch ? convertZoom(zoomMatch[3].replace('m', ''), true) : 19;
} else if (viewMatch) {
lat = viewMatch[1];
lon = viewMatch[2];
const zoomValue = viewMatch[3];
const isMeter = zoomValue.endsWith('m');
zoom = convertZoom(zoomValue.replace(/[mz]/, ''), isMeter);
} else if (pinMatch) {
lat = pinMatch[1];
lon = pinMatch[2];
zoom = 19;
} else {
if (queryMatch) {
return { query: queryMatch[1] };
}
return null;
}
return { lat, lon, zoom };
} catch (e) {
console.error('座標抽出エラー:', e);
return null;
}
}
function openWaze(isEditor = false) {
const coords = extractCoordinatesAndZoom();
if (coords && coords.lat && coords.lon) {
const wazeUrl = isEditor
? `https://waze.com/ja/editor?env=row&lat=${coords.lat}&lon=${coords.lon}&zoomLevel=${coords.zoom}`
: `https://www.waze.com/ja/livemap?lat=${coords.lat}&lon=${coords.lon}&zoom=${coords.zoom}`;
window.open(wazeUrl, '_blank');
} else {
alert('座標を取得できませんでした。OpenStreetMapを開きますか?');
openOpenStreetMap();
}
}
function openOpenStreetMap() {
const coords = extractCoordinatesAndZoom();
let osmUrl;
if (coords && coords.lat && coords.lon) {
osmUrl = `https://www.openstreetmap.org/#map=${coords.zoom}/${coords.lat}/${coords.lon}`;
} else if (coords && coords.query) {
osmUrl = `https://www.openstreetmap.org/search?query=${encodeURIComponent(coords.query)}`;
} else {
osmUrl = `https://www.openstreetmap.org`;
}
window.open(osmUrl, '_blank');
}
function openInGoogleMapsSameCoords() {
const coords = extractCoordinatesAndZoom();
if (coords && coords.lat && coords.lon && coords.zoom) {
const googleMapsUrl = `https://www.google.com/maps/@${coords.lat},${coords.lon},${coords.zoom}z`;
window.open(googleMapsUrl, '_blank');
} else {
alert('座標を取得できませんでした。');
}
}
function createWazeButton() {
if (!/google\.(com|co\.jp)\/maps/.test(window.location.href)) return;
const existing = document.querySelector('#waze-button-container');
if (existing) return;
const container = document.createElement('div');
container.id = 'waze-button-container';
container.style.position = 'fixed';
container.style.zIndex = '9999';
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.backgroundColor = '#f0f0f0';
container.style.borderRadius = '5px';
container.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
container.style.userSelect = 'none';
container.style.cursor = 'move';
container.style.padding = '5px';
let savedTop = localStorage.getItem('wazeButtonTop');
let savedLeft = localStorage.getItem('wazeButtonLeft');
if (!savedTop || !savedLeft) {
const w = window.innerWidth, h = window.innerHeight;
savedLeft = `${(w - 450) / 2}px`; // ボタン3つ分の幅を考慮
savedTop = `${(h - 40) / 2}px`;
}
container.style.top = savedTop;
container.style.left = savedLeft;
let isDragging = false, offsetX, offsetY;
container.addEventListener('mousedown', (e) => {
e.preventDefault();
isDragging = true;
const rect = container.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
function onMouseMove(e) {
if (!isDragging) return;
container.style.left = `${e.clientX - offsetX}px`;
container.style.top = `${e.clientY - offsetY}px`;
}
function onMouseUp() {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
localStorage.setItem('wazeButtonLeft', container.style.left);
localStorage.setItem('wazeButtonTop', container.style.top);
}
const btnWazeEditor = document.createElement('button');
btnWazeEditor.textContent = 'Wazeエディター';
btnWazeEditor.style = 'padding:5px 10px; background:#007bff; color:white; border:none; font-family:sans-serif; cursor:pointer; margin-right:5px;';
btnWazeEditor.onclick = () => openWaze(true);
const btnWazeLive = document.createElement('button');
btnWazeLive.textContent = 'Wazeライブマップ';
btnWazeLive.style = 'padding:5px 10px; background:#17a2b8; color:white; border:none; font-family:sans-serif; cursor:pointer; margin-right:5px;';
btnWazeLive.onclick = () => openWaze(false);
const btnOSM = document.createElement('button');
btnOSM.textContent = 'OpenStreetMap';
btnOSM.style = 'padding:5px 10px; background:#6c757d; color:white; border:none; font-family:sans-serif; cursor:pointer;';
btnOSM.onclick = (e) => {
if (e.shiftKey) {
openInGoogleMapsSameCoords();
} else {
openOpenStreetMap();
}
};
container.appendChild(btnWazeEditor);
container.appendChild(btnWazeLive);
container.appendChild(btnOSM);
document.body.appendChild(container);
}
function observeSPA() {
let lastUrl = location.href;
const observer = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
createWazeButton();
}
});
const targetNode = document.querySelector('#pane') || document.body;
observer.observe(targetNode, { subtree: true, childList: true });
}
// ===============================
// テキスト選択→Googleマップ機能
// ===============================
function openInGoogleMaps() {
let selectedText = window.getSelection().toString().trim();
if (selectedText) {
let url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(selectedText)}`;
window.open(url, '_blank');
} else {
alert('住所またはテキストを選択してください!');
}
}
document.addEventListener('keydown', function (e) {
if (e.ctrlKey && e.key.toLowerCase() === 'm') {
openInGoogleMaps();
}
});
document.addEventListener('contextmenu', function () {
setTimeout(() => {
let text = window.getSelection().toString().trim();
if (text) {
if (confirm(`"${text}" をGoogleマップで開きますか?`)) {
let url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(text)}`;
window.open(url, '_blank');
}
}
}, 100);
});
// 初期化
function initializeScript() {
if (document.body) {
createWazeButton();
observeSPA();
} else {
setTimeout(initializeScript, 500);
}
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
initializeScript();
} else {
window.addEventListener('load', initializeScript);
}
})();
GoogleマップからWazeエディター、Wazeライブマップ、OpenStreetMapに移動できるScriptです。
// ==UserScript==
// @name Google Maps + Waze + OpenStreetMap Button + Address Search
// @namespace https://www.google.com/maps/place/
// @version 1.4.0
// @description GoogleマップからWaze(ライブマップ/エディター)およびOpenStreetMapで開くボタン&選択テキストをGoogleマップ検索(日本語対応、SPA対応、DMS座標対応)
// @match https://www.google.com/maps/*
// @match https://www.google.co.jp/maps/*
// @match https://maps.google.com/*
// @match https://maps.google.co.jp/*
// @icon https://pngimg.com/uploads/waze/waze_PNG21.png
// @grant none
// @author 碧いうさぎ
// ==/UserScript==
(function () {
'use strict';
// ==========================
// Googleマップ → Waze/OpenStreetMap 機能
// ==========================
function convertZoom(googleZoom, isMeter = false) {
let baseZoom;
if (isMeter) {
const meters = parseFloat(googleZoom);
if (meters <= 50) baseZoom = 19;
else if (meters <= 100) baseZoom = 18;
else if (meters <= 200) baseZoom = 17;
else if (meters <= 500) baseZoom = 16;
else if (meters <= 1000) baseZoom = 15;
else if (meters <= 2000) baseZoom = 14;
else if (meters <= 5000) baseZoom = 13;
else if (meters <= 10000) baseZoom = 12;
else if (meters <= 20000) baseZoom = 11;
else baseZoom = 10;
} else {
const googleZoomLevel = parseFloat(googleZoom);
if (googleZoomLevel >= 20) baseZoom = 19;
else if (googleZoomLevel >= 18) baseZoom = 18;
else if (googleZoomLevel >= 16) baseZoom = 17;
else if (googleZoomLevel >= 14) baseZoom = 16;
else if (googleZoomLevel >= 12) baseZoom = 15;
else if (googleZoomLevel >= 10) baseZoom = 14;
else if (googleZoomLevel >= 8) baseZoom = 13;
else if (googleZoomLevel >= 6) baseZoom = 12;
else if (googleZoomLevel >= 4) baseZoom = 11;
else baseZoom = 10;
}
return Math.min(baseZoom + 2, 19);
}
function dmsToDecimal(dms) {
const dmsRegex = /(\d+)°(\d+)'(\d+\.?\d*)"(N|S|E|W)/;
const match = dms.match(dmsRegex);
if (!match) return null;
const degrees = parseFloat(match[1]);
const minutes = parseFloat(match[2]);
const seconds = parseFloat(match[3]);
const direction = match[4];
let decimal = degrees + (minutes / 60) + (seconds / 3600);
if (direction === 'S' || direction === 'W') {
decimal = -decimal;
}
return decimal.toFixed(6);
}
function extractCoordinatesAndZoom() {
try {
const url = decodeURIComponent(window.location.href);
const viewRegex = /@(-?\d+\.\d+),(-?\d+\.\d+),(\d+\.?\d*[mz])/;
const placeRegex = /!3m1!1e3!4m\d+!3m\d+!1s0x[0-9a-f]+:0x[0-9a-f]+!8m2!3d(-?\d+\.\d+)!4d(-?\d+\.\d+)/;
const pinRegex = /!3d(-?\d+\.\d+)!4d(-?\d+\.\d+)/;
const dmsRegex = /\/place\/(\d+%C2%B0\d+'[\d.]+%22[N|S]\+?\d+%C2%B0\d+'[\d.]+%22[E|W])/;
const queryRegex = /[?&]q=([^&]+)/;
let lat, lon, zoom;
const viewMatch = url.match(viewRegex);
const placeMatch = url.match(placeRegex);
const pinMatch = url.match(pinRegex);
const dmsMatch = url.match(dmsRegex);
const queryMatch = url.match(queryRegex);
if (dmsMatch) {
const dmsParts = decodeURIComponent(dmsMatch[1]).split('+');
if (dmsParts.length === 2) {
lat = dmsToDecimal(dmsParts[0]);
lon = dmsToDecimal(dmsParts[1]);
const zoomMatch = url.match(/@(-?\d+\.\d+),(-?\d+\.\d+),(\d+\.?\d*[mz])/);
zoom = zoomMatch ? convertZoom(zoomMatch[3].replace(/[mz]/, ''), zoomMatch[3].endsWith('m')) : 19;
}
} else if (placeMatch) {
lat = placeMatch[1];
lon = placeMatch[2];
const zoomMatch = url.match(/@(-?\d+\.\d+),(-?\d+\.\d+),(\d+\.?\d*[m])/);
zoom = zoomMatch ? convertZoom(zoomMatch[3].replace('m', ''), true) : 19;
} else if (viewMatch) {
lat = viewMatch[1];
lon = viewMatch[2];
const zoomValue = viewMatch[3];
const isMeter = zoomValue.endsWith('m');
zoom = convertZoom(zoomValue.replace(/[mz]/, ''), isMeter);
} else if (pinMatch) {
lat = pinMatch[1];
lon = pinMatch[2];
zoom = 19;
} else {
if (queryMatch) {
return { query: queryMatch[1] };
}
return null;
}
return { lat, lon, zoom };
} catch (e) {
console.error('座標抽出エラー:', e);
return null;
}
}
function openWaze(isEditor = false) {
const coords = extractCoordinatesAndZoom();
if (coords && coords.lat && coords.lon) {
const wazeUrl = isEditor
? `https://waze.com/ja/editor?env=row&lat=${coords.lat}&lon=${coords.lon}&zoomLevel=${coords.zoom}`
: `https://www.waze.com/ja/livemap?lat=${coords.lat}&lon=${coords.lon}&zoom=${coords.zoom}`;
window.open(wazeUrl, '_blank');
} else {
alert('座標を取得できませんでした。OpenStreetMapを開きますか?');
openOpenStreetMap();
}
}
function openOpenStreetMap() {
const coords = extractCoordinatesAndZoom();
let osmUrl;
if (coords && coords.lat && coords.lon) {
osmUrl = `https://www.openstreetmap.org/#map=${coords.zoom}/${coords.lat}/${coords.lon}`;
} else if (coords && coords.query) {
osmUrl = `https://www.openstreetmap.org/search?query=${encodeURIComponent(coords.query)}`;
} else {
osmUrl = `https://www.openstreetmap.org`;
}
window.open(osmUrl, '_blank');
}
function openInGoogleMapsSameCoords() {
const coords = extractCoordinatesAndZoom();
if (coords && coords.lat && coords.lon && coords.zoom) {
const googleMapsUrl = `https://www.google.com/maps/@${coords.lat},${coords.lon},${coords.zoom}z`;
window.open(googleMapsUrl, '_blank');
} else {
alert('座標を取得できませんでした。');
}
}
function createWazeButton() {
if (!/google\.(com|co\.jp)\/maps/.test(window.location.href)) return;
const existing = document.querySelector('#waze-button-container');
if (existing) return;
const container = document.createElement('div');
container.id = 'waze-button-container';
container.style.position = 'fixed';
container.style.zIndex = '9999';
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.backgroundColor = '#f0f0f0';
container.style.borderRadius = '5px';
container.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
container.style.userSelect = 'none';
container.style.cursor = 'move';
container.style.padding = '5px';
let savedTop = localStorage.getItem('wazeButtonTop');
let savedLeft = localStorage.getItem('wazeButtonLeft');
if (!savedTop || !savedLeft) {
const w = window.innerWidth, h = window.innerHeight;
savedLeft = `${(w - 450) / 2}px`; // ボタン3つ分の幅を考慮
savedTop = `${(h - 40) / 2}px`;
}
container.style.top = savedTop;
container.style.left = savedLeft;
let isDragging = false, offsetX, offsetY;
container.addEventListener('mousedown', (e) => {
e.preventDefault();
isDragging = true;
const rect = container.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
function onMouseMove(e) {
if (!isDragging) return;
container.style.left = `${e.clientX - offsetX}px`;
container.style.top = `${e.clientY - offsetY}px`;
}
function onMouseUp() {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
localStorage.setItem('wazeButtonLeft', container.style.left);
localStorage.setItem('wazeButtonTop', container.style.top);
}
const btnWazeEditor = document.createElement('button');
btnWazeEditor.textContent = 'Wazeエディター';
btnWazeEditor.style = 'padding:5px 10px; background:#007bff; color:white; border:none; font-family:sans-serif; cursor:pointer; margin-right:5px;';
btnWazeEditor.onclick = () => openWaze(true);
const btnWazeLive = document.createElement('button');
btnWazeLive.textContent = 'Wazeライブマップ';
btnWazeLive.style = 'padding:5px 10px; background:#17a2b8; color:white; border:none; font-family:sans-serif; cursor:pointer; margin-right:5px;';
btnWazeLive.onclick = () => openWaze(false);
const btnOSM = document.createElement('button');
btnOSM.textContent = 'OpenStreetMap';
btnOSM.style = 'padding:5px 10px; background:#6c757d; color:white; border:none; font-family:sans-serif; cursor:pointer;';
btnOSM.onclick = (e) => {
if (e.shiftKey) {
openInGoogleMapsSameCoords();
} else {
openOpenStreetMap();
}
};
container.appendChild(btnWazeEditor);
container.appendChild(btnWazeLive);
container.appendChild(btnOSM);
document.body.appendChild(container);
}
function observeSPA() {
let lastUrl = location.href;
const observer = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
createWazeButton();
}
});
const targetNode = document.querySelector('#pane') || document.body;
observer.observe(targetNode, { subtree: true, childList: true });
}
// ===============================
// テキスト選択→Googleマップ機能
// ===============================
function openInGoogleMaps() {
let selectedText = window.getSelection().toString().trim();
if (selectedText) {
let url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(selectedText)}`;
window.open(url, '_blank');
} else {
alert('住所またはテキストを選択してください!');
}
}
document.addEventListener('keydown', function (e) {
if (e.ctrlKey && e.key.toLowerCase() === 'm') {
openInGoogleMaps();
}
});
document.addEventListener('contextmenu', function () {
setTimeout(() => {
let text = window.getSelection().toString().trim();
if (text) {
if (confirm(`"${text}" をGoogleマップで開きますか?`)) {
let url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(text)}`;
window.open(url, '_blank');
}
}
}, 100);
});
// 初期化
function initializeScript() {
if (document.body) {
createWazeButton();
observeSPA();
} else {
setTimeout(initializeScript, 500);
}
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
initializeScript();
} else {
window.addEventListener('load', initializeScript);
}
})();
WME RTC Manager
https://greasyfork.org/ja/scripts/549529-wme-rtc-manager
「WME RTC Manager」は、Waze Map Editor(WME)で道路閉鎖(RTC: Road Traffic Closure)を効率的に管理するためのユーザースクリプトです。主な機能は以下の通り:
- 一括選択: 同じ閉鎖情報(開始日、終了日、説明など)を持つ複数のセグメントを「Select all」ボタンで一気に選択可能。
- 編集効率化: 選択したセグメントの閉鎖情報をまとめて編集・確認でき、作業時間を短縮。
- 設定カスタマイズ: 専用の「RTC Manager」タブでスクリプトの設定を調整可能。
- 更新通知: スクリプト更新時に変更履歴をポップアップで表示。
使いどころ: イベントや工事で複数のセグメントにまたがる閉鎖情報を管理する際に便利。WME編集者、特にコミュニティで閉鎖情報を頻繁に扱う人に最適。インストールはGreasy Forkから行い、WMEの編集パネルで「Select all」ボタンを使って操作します。
説明書です。
Waze 便利ツール(簡単説明)2つの便利機能が1つに!1. 右のボタン(全ページ)位置: 右側
動かし方: ドラッグで好きな場所へ
ボタン:Wazeイベント
国交省道路情報
下のURLバー(イベント・ライブマップのみ)使い方:URLをコピー
「貼り付け」クリック
「Editorで開く」か「Live Mapで開く」をクリック
エディター画面では出ません(邪魔にならない)
インストールTampermonkey を入れる
→ https://www.tampermonkey.net/
スクリプトコードを全部コピー
Tampermonkey で「新規作成」→ 貼り付け → 保存
Wazeページをリロード
名前Waze リンクポッド + URLランチャー (Editor非表示)以上! これだけでOK!作った人: Aoi
更新: 2025年11月9日
![]()
- 正方形のボタン:ゆがんだ Place を自動で直角に補正してくれることがあります。
- スティックのボタン:Place が歪んでいて保存できない場合に使ってください。クリックすると、保存できるようになることがあります。
WME E50 Fetch POI Data
名前:WME E50 Fetch POI Data(バージョン 0.12.4)
使用方法:
- WMEでPOI(エリアまたはポイント)を選択する
- 編集パネル(またはモーダルウィンドウ)に「Information
」が表示される - リストされた住所・施設情報を確認
- 適用したい項目をクリック → 確認ダイアログで「OK」を押すと、名称・住所・番地が自動入力される
- マウスを項目に合わせると地図上に距離線が表示される
以上!これだけで使えます。
// ==UserScript==
// @name WME E50 Fetch POI Data
// @name:ja WME E50 POIデータ取得
// @name:uk WME 🇺🇦 E50 Fetch POI Data
// @name:ru WME 🇺🇦 E50 Fetch POI Data
// @version 0.12.4
// @description Fetch information about the POI from external sources
// @description:ja 外部ソースからPOIの情報を取得します
// @license MIT License
// @author Anton Shevchuk
// @namespace https://greasyfork.org/users/227648-anton-shevchuk
// @supportURL https://github.com/AntonShevchuk/wme-e50/issues
// @match https://www.waze.com/ja/editor?env=row&lon=*&lat=*&zoom=*&venues=*&tab=feature_editor
// @match https://www.waze.com/ja/editor?env=*=*
// @icon data:image/png;base64,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ICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICB4bWxuczpHSU1QPSJodHRwOi8vd3d3LmdpbXAub3JnL3htcC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgeG1wTU06RG9jdW1lbnRJRD0iZ2ltcDpkb2NpZDpnaW1wOmE5OWYwNzEyLTc5ZDctNDY5Ni05MWUyLTE5YmEzNjM2NTIzYiIKICAgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpjZGIzMGE2ZS00M2RkLTQ0MzktYTQ0Ny05ODc4YTc0MWZhZWYiCiAgIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpiMjBiMWUzNS1mZjE3LTRjZjgtODk5My0wMTYwNTc0OGY5MDkiCiAgIGRjOkZvcm1hdD0iaW1hZ2UvcG5nIgogICBHSU1QOkFQST0iMi4wIgogICBHSU1QOlBsYXRmb3JtPSJNYWMgT1MiCiAgIEdJTVA6VGltZVN0YW1wPSIxNjczNDMzNzgwNTU3NzE0IgogICBHSU1QOlZlcnNpb249IjIuMTAuMzIiCiAgIHRpZmY6T3JpZW50YXRpb249IjEiCiAgIHhtcDpDcmVhdG9yVG9vbD0iR0lNUCAyLjEwIgogICB4bXA6TWV0YWRhdGFEYXRlPSIyMDIzOjAxOjExVDExOjQzOjAwKzAxOjAwIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAyMzowMToxMVQxMTo0MzowMCswMTowMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InNhdmVkIgogICAgICBzdEV2dDpjaGFuZ2VkPSIvIgogICAgICBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmI4N2M2YmI2LTJmZGItNDdlMi05MjYzLWE3ZjNmMjY0ZDYyOSIKICAgICAgc3RFdnQ6c29mdHdhcmVBZ2VudD0iR2ltcCAyLjEwIChNYWMgT1MpIgogICAgICBzdEV2dDp3aGVuPSIyMDIzLTAxLTExVDExOjQzOjAwKzAxOjAwIi8+CiAgICA8L3JkZjpTZXE+CiAgIDwveG1wTU06SGlzdG9yeT4KICA8L3JkZjpEZXNjcmlwdGlvbj4KIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0idyI/PgpaVBgAAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfnAQsKKwBWE1eUAAAGfklEQVR42u2ae0zTVxTHv7R1yuRVymgrdMYhGh9oYDQBCzPMByYm4hAfU1E0IjjjIBKVxEqJIz7+ULFRJEq0hGCFzdDEaEBNAGkZERcnL3EMUXC0gIUCE9BSuj+wP/qzrZSHSy2/T0L4nXvPuXDP795z7rn5ARQUFBQUFBQUFBQUUxEH4ikFNDjgJwCbAIRAb9RnHzPVA5ADyIcDMpCKoZFOEbgQQSG5K9Er1Uq9vaJUK/WSuxI9RFBABK6xA+SV9ZX6qUJlfaUeIsiHF4YIcdcE1zJjVsUAAMprypFTkYPMfzLtagfEe8UjOigayxYvAwBI7kmwS7ErzgEilCp/Vn7HcedAUa1AyM0Quw568g1yCPwEUHWqwBVzH9DooIdy3DkAgCx5lt1HfcMcOe4c0EEPpXHpXCLaS9okdu8A4zly6VwH2lQ/BzCsUYpkRmKhx8Jx/YGetz0QN4sJeb3beiz+arHV9vdb7qNioMJsn9BXiOC5wfB088SgbhC1zbXI+jPLov64HbAtYBsiQyPH5YDW160QXxhxwL7QfVj97WrrB8gFKhrIEwp2DEbO9hz4ePmQ2oMWBmFr2FaIb4mRXJNs1fD/+xZwd3KfkD0NNOTtyjOZvAHH6Y5I+iEJQl+hbTpgotxcdRM8T97HlzWdgYS1CZO3BT5E3a1Gk6rJKt2O7o6P9h/LPTZqDDBm+ZLlJLmosghlz8rAnMlE7OpYuMx0AQB4uHngtN9pHKk+MvkOaFI1gS/lj+sNGv5BA2kNaVbbJs5OBNOZSci1TbVYc3sNIQ/ph3Ao8hAhhy4IBaptbAu4OI44YODdwJhsw+aHkeTy+nKSfLjqMPrf9hPyPK95th0DNP9qxqTvxfIiyXWqOtPt2aMmnlmuLLjR3GzMAca3DPrhX18zvobQVwihrxC7ObstmnJZXJKc/jLdREfVqSLJ+7/ZP/kxYCI4OTqR5BthN7AuaB0cpzsSbSe6TuDy3ctIeZpCflu0sb8vOp0++Q5gu7GRF5Y3+hJ/o0HcwziLDuCwONi8fLPp+Ew2hJuF4BZxEft7LNHu6uRKCnjmGBoity/gLgCeTbIDeGweeGzeqHoqtcrEAdbvFAfsXLETd+rvoKCrYPiQ88XIKul602XWrq2njbwCHOi2EwMczFwzage1qGqswqNnj6DuVpP6pjGmIen7JLNj6bQ6s+1arXbyi6HJYovnFpI88G4AybnJOP/yPNH2JPoJlvgsIWTjZ9Kbo5t/d3QG/dM7oKW9BRVPR6+4Plym0nYppCKp+YzwHtEtEQoSCwjZ+UtnRHtGI6c9BwPaAcyYNgMA4OZkPr2xXdjkmGAhVkzIAW1dbdhUvGniadAMMo0MKrUKHBaHaJvtOhtoBzS9Ghhurxg0hsU6gHRWUNZ9fsVQR4/5+kE3pBvzWDqd7vNzANOJaTGrGHOAd8BsCjXmXOM523FAtiAblT9W4nnccygPKCGaLzKrZ5zvAeBVzysAQHNHM6l9EXeRia2Hqwepau3T99mOA2axZiFwfiDmcOeAw+IgIjDCROci/yKcHZ0Jube/l7jILPmrhKQb6BNIkk8uOkk6UdY1132aLMBmspEflm+1/vXH1yHTyFD6tBQrA1YS7f6+/ihcW4i04jTI++TICs7CjhU7SLbVjSP1rLhZjJTuFLBcWQCApb5Lke6fjsTHiYhiRSFmRQzJtri2+NM4gOfJG/VWxpiqV1WQaWRIa0jD3va9JNtwfjjC+eHmDzWDWlwovUBqK6kuwYaQDUQmSIhIQEJEgtlULaoX2V45fPb2WWgHrTut5T/Ih7RDSj5M3d+C1tetH7XTDmqRUZRhm3eC6S/TceK3E+js7bSo0/+uH1eLrmL7g+0mfYMYxNFfj5qUvcaTPyM7g1ONpybvJCj/Wz6uHGygpqOGJKfWp0LaKMVB/4MI8AkAx50DBo0BZacSDa0NuPTwEkrelFgcT9ImgUQswZWgK+DP5YPNZKO7rxsvVC+QqciETCOz/lzmfdxb33KsZVhIta9vIiyhTx2+ieH9wgNNqVPqDR0x7Bi7n7zxHJU6pZ6mg67MsJ/2hOyxewcY5qjqVEEHXRkNQG7hH4UAAIGfAIooBeK944eLFjv6ifeOhyJKAYGfAADwfs6572vQqfuJjCENbuRL+eXZ97Itphd7QNWpQva9bPCl/HIAG8nV+VT6TE6PDBzHECgoKCgoKCgoKCgopiz/AYtz5BmJvxgCAAAAAElFTkSuQmCC
// @connect revgeocode.search.hereapi.com
// @connect api.visicom.ua
// @connect nominatim.openstreetmap.org
// @connect dev.virtualearth.net
// @connect maps.googleapis.com
// @connect stat.waze.com.ua
// @grant GM.xmlHttpRequest
// @grant GM.setClipboard
// @require https://update.greasyfork.org/scripts/389765/1090053/CommonUtils.js
// @require https://update.greasyfork.org/scripts/450160/1704233/WME-Bootstrap.js
// @require https://update.greasyfork.org/scripts/450221/1691071/WME-Base.js
// @require https://update.greasyfork.org/scripts/450320/1688694/WME-UI.js
// @require https://cdn.jsdelivr.net/npm/@turf/turf@7.2.0/turf.min.js
// @downloadURL https://update.greasyfork.org/scripts/389143/WME%20E50%20Fetch%20POI%20Data.user.js
// @updateURL https://update.greasyfork.org/scripts/389143/WME%20E50%20Fetch%20POI%20Data.meta.js
// ==/UserScript==
/* jshint esversion: 8 */
/* global require */
/* global $, jQuery */
/* global I18n */
/* global WMEBase, WMEUI, WMEUIHelper, WMEUIHelperFieldset */
/* global Container, Settings, SimpleCache, Tools */
/* global Node$1, Segment, Venue, VenueAddress, WmeSDK */
/* global turf */
(function () {
'use strict'
const NAME = 'E50'
// translation structure
const TRANSLATION = {
'en': {
title: 'Information 📍',
notFound: 'Not found',
options: {
title: 'Options',
modal: 'Use modal window',
transparent: 'Transparent modal window',
entryPoint: 'Create Entry Point if not exists',
externalProvider: 'Show pointer to linked place',
copyData: 'Copy POI data to clipboard on click',
lock: 'Lock POI to 2 level',
keys: 'API keys',
},
ranges: {
title: 'Additional',
radius: 'Radius for search',
collapse: 'Collapse the lists longer than',
},
providers: {
title: 'Providers',
magic: 'Closest Segments',
osm: 'Open Street Map',
bing: 'Bing',
here: 'HERE',
google: 'Google',
visicom: 'Visicom',
ua: 'UA Addresses',
},
questions: {
changeName: 'Are you sure to change the name?',
changeCity: 'Are you sure to change the city?',
changeStreet: 'Are you sure to change the street name?',
changeNumber: 'Are you sure to change the house number?',
notFoundCity: 'City not found in the current location, are you sure to create a new one?',
notFoundStreet: 'Street not found in the current location, are you sure to create a new one?'
}
},
'uk': {
title: 'Інформація 📍',
notFound: 'Нічого не знайдено',
options: {
title: 'Налаштування',
modal: 'Використовувати окрему панель',
transparent: 'Напівпрозора панель',
entryPoint: 'Створювати точку в\'їзду, якщо відсутня',
externalProvider: 'Відображати пов\'язане місце',
copyData: 'При виборі, копіювати до буферу обміну назву та адресу POI',
lock: 'Блокувати POI 2-м рівнем',
keys: 'Ключі до API',
},
ranges: {
title: 'Додаткові',
radius: 'Радіус для пошуку',
collapse: 'Складати перелік, більший за',
},
providers: {
title: 'Джерела',
magic: 'Сегменти поруч',
osm: 'Open Street Map',
bing: 'Bing',
here: 'HERE',
google: 'Google',
visicom: 'Візіком',
ua: 'UA Адреси',
},
questions: {
changeName: 'Ви впевненні що хочете змінити им\'я?',
changeCity: 'Ви впевненні що хочете змінити місто?',
changeStreet: 'Ви впевненні що хочете змінити вулицю?',
changeNumber: 'Ви впевненні що хочете змінити номер дома?',
notFoundCity: 'Ми не знайшли такого міста у поточному місці, ви впевнені, що треба його додати?',
notFoundStreet: 'Ми не знайшли таку вулицю у поточному місці, ви впевнені, що треба її додати?',
}
},
'ru': {
title: 'Информация 📍',
notFound: 'Ничего не найдено',
options: {
title: 'Настройки',
modal: 'Использовать отдельную панель',
transparent: 'Полупрозрачная панель',
entryPoint: 'Создавать точку въезда если отсутствует',
externalProvider: 'Показывать связанное место',
copyData: 'При виборе, копировать в буфер обмена название и адрес POI',
lock: 'Блокировать POI 2-м уровнем',
keys: 'Ключи к API',
},
ranges: {
title: 'Дополнительно',
radius: 'Радиус поиска',
collapse: 'Складывать списки, которые больше',
},
providers: {
title: 'Источники',
magic: 'Ближайшие сегменты',
osm: 'Open Street Map',
bing: 'Bing',
here: 'HERE',
google: 'Google',
visicom: 'Визиком',
ua: 'UA Адреса',
},
questions: {
changeName: 'Ви уверены, что хотите изменить имя?',
changeCity: 'Ви уверены, что хотите изменить город?',
changeStreet: 'Ви уверены, что хотите изменить улицу?',
changeNumber: 'Ви уверены, что хотите изменить номер дома?',
notFoundCity: 'Мы не нашли такого города в данной локации, вы уверены что нужно его добавить?',
notFoundStreet: 'Мы не нашли такую улицу в данной локации, вы уверены что нужно её добавить?',
}
},
'fr': {
title: 'Informations 📍',
notFound: 'Lieu inconnu',
options: {
title: 'Réglages',
modal: 'Activer la fenêtre',
transparent: 'Fenêtre transparente',
entryPoint: 'Créer le point d\'entrée s\'il n\'existe pas',
copyData: 'Copier les informations du POI en cliquant',
lock: 'Verrouiller le POI au niveau 2',
keys: 'API keys',
},
ranges: {
title: 'Supplémentaire',
radius: 'Rayon de recherche',
collapse: 'Réduire les listes plus grandes que',
},
providers: {
title: 'Sources',
magic: 'Au plus proche du segment',
osm: 'Open Street Map',
bing: 'Bing',
here: 'HERE',
google: 'Google',
visicom: 'Visicom',
ua: 'UA Addresses',
},
questions: {
changeName: 'Êtes-vous sûr de changer le nom ?',
changeCity: 'Êtes-vous sûr de changer la ville ?',
changeStreet: 'Êtes-vous sûr de changer la rue ?',
changeNumber: 'Êtes-vous sûr de changer le numéro de rue ?',
notFoundCity: 'City not found in the current location, are you sure to create a new one?',
notFoundStreet: 'Street not found in the current location, are you sure to create a new one?'
}
},
'ja': {
title: '情報 📍',
notFound: '見つかりません',
options: {
title: '設定',
modal: 'モーダルウィンドウを使用',
transparent: '半透明のモーダルウィンドウ',
entryPoint: 'エントリーポイントがなければ作成する',
externalProvider: '関連する場所を表示',
copyData: 'クリックでPOIデータをクリップボードにコピー',
lock: 'POIをレベル2にロックする',
keys: 'APIキー',
},
ranges: {
title: '追加設定',
radius: '検索半径',
collapse: '要素数が次より多い場合に折りたたむ',
},
providers: {
title: '提供元',
magic: '最寄りのセグメント',
osm: 'Open Street Map',
bing: 'Bing',
here: 'HERE',
google: 'Google',
visicom: 'Visicom',
ua: 'UAアドレス',
},
questions: {
changeName: '名前を変更してもよろしいですか?',
changeCity: '都市を変更してもよろしいですか?',
changeStreet: '通りの名前を変更してもよろしいですか?',
changeNumber: '番地を変更してもよろしいですか?',
notFoundCity: '現在の場所にこの都市が見つかりません。新しく作成してもよろしいですか?',
notFoundStreet: '現在の場所にこの通りが見つかりません。新しく作成してもよろしいですか?'
}
}
}
const SETTINGS = {
options: {
modal: true,
transparent: false,
entryPoint: true,
externalProvider: false,
copyData: true,
lock: true,
},
ranges: {
radius: 200,
collapse: 3,
},
providers: {
magic: true,
osm: false,
// bing: false,
here: false,
google: true,
visicom: false,
ua: false,
},
keys: {
// Russian warship, go f*ck yourself!
visicom: '',
here: '',
// bing: '',
google: 'AIzaSyBWB3' + 'jiUm1dkFwvJWy4w4ZmO7K' + 'PyF4oUa0', // extracted from WME
ua: 'E50'
}
}
const LOCALE = {
// Ukraine
232: {
country: 'uk',
language: 'ua',
locale: 'uk_UA'
}
}
// Road Types
// I18n.translations.uk.segment.road_types
// I18n.translations.en.segment.road_types
const TYPES = {
street: 1,
primary: 2,
freeway: 3,
ramp: 4,
trail: 5,
major: 6,
minor: 7,
offroad: 8,
walkway: 9,
boardwalk: 10,
ferry: 15,
stairway: 16,
private: 17,
railroad: 18,
runway: 19,
parking: 20,
narrow: 22
}
WMEUI.addTranslation(NAME, TRANSLATION)
const STYLE =
'.form-group.e50 .header h5 { padding: 16px 16px 0; font-size: 16px }' +
'.form-group.e50 .body { overflow-x: auto; max-height: 420px; padding: 4px 0; }' +
'#venue-edit-general .e50 fieldset { border: 0; padding: 0; margin: 0; }' +
'#venue-edit-general .e50 legend { width: 100%; text-align: left; }' +
'#venue-edit-general .e50 fieldset legend, .wme-ui-panel.e50 fieldset legend { cursor:pointer; font-size: 12px; font-weight: bold; margin: 0; padding: 0 8px; background-color: #f6f7f7; border: 1px solid #e5e5e5 }' +
'#venue-edit-general .e50 fieldset legend::after, .wme-ui-panel.e50 fieldset legend::after { display: inline-block; text-rendering: auto; content: "↑"; float: right; font-size: 10px; line-height: inherit; position: relative; right: 3px; } ' +
'#venue-edit-general .e50 fieldset legend span, .wme-ui-panel.e50 fieldset legend span { font-weight: bold; background-color: #fff; border-radius: 5px; color: #ed503b; display: inline-block; font-size: 12px; line-height: 14px; max-width: 30px; padding: 1px 5px; text-align: center; } ' +
'#venue-edit-general .e50 fieldset ul, .wme-ui-panel.e50 fieldset ul { border: 1px solid #ddd; } ' +
'#venue-edit-general .e50 fieldset.collapsed ul, .wme-ui-panel.e50 fieldset.collapsed ul { display: none } ' +
'#venue-edit-general .e50 fieldset.collapsed legend::after, .wme-ui-panel.e50 fieldset.collapsed legend::after { content: "↓" }' +
'#venue-edit-general .e50 ul, .wme-ui-panel.e50 ul { padding: 8px; margin: 0 }' +
'#venue-edit-general .e50 li, .wme-ui-panel.e50 li { padding: 0; margin: 0; list-style: none; margin-bottom: 2px }' +
'#venue-edit-general .e50 li a, .wme-ui-panel.e50 li a { display: block; padding: 2px 4px; text-decoration: none; border: 1px solid #e4e4e4; }' +
'#venue-edit-general .e50 li a:hover, .wme-ui-panel.e50 li a:hover { background: rgba(255, 255, 200, 1) }' +
'#venue-edit-general .e50 li a.nonumber, .wme-ui-panel.e50 li a.nonumber { background: rgba(250, 250, 200, 0.5) }' +
'#venue-edit-general .e50 li a.nonumber:hover, .wme-ui-panel.e50 li a.nonumber:hover { background: rgba(250, 250, 200, 1) }' +
'#venue-edit-general .e50 li a.noaddress, .wme-ui-panel.e50 li a.noaddress { background: rgba(250, 200, 100, 0.5) }' +
'#venue-edit-general .e50 li a.noaddress:hover, .wme-ui-panel.e50 li a.noaddress:hover { background: rgba(250, 200, 100, 1) }' +
'.form-group.e50 legend { cursor:pointer; font-size: 12px; font-weight: bold; width: auto; text-align: right; border: 0; margin: 0; padding: 0 8px; }' +
'.form-group.e50 fieldset { border: 1px solid #ddd; padding: 8px; }' +
'.form-group.e50 div.controls { padding: 8px; }' +
'.form-group.e50 div.controls:empty, #panel-container .archive-panel .body:empty { min-height: 20px; }' +
'.form-group.e50 div.controls:empty::after, #panel-container .archive-panel .body:empty::after { color: #ccc; padding: 0 8px; content: "' + I18n.t(NAME).notFound + '" }' +
'.form-group.e50 div.controls label { white-space: normal; font-weight: normal; margin-top: 5px; line-height: 18px; font-size: 13px; }' +
'.form-group.e50 div.controls input[type="text"] { float:right; }' +
'.form-group.e50 div.controls input[type="number"] { float:right; width: 60px; text-align:right; }' +
'.distance-over-200 { background-color: #f08a24; }' +
'.distance-over-1000 { background-color: #ed503b; }' +
'.external-operational a.url { border: 4px solid #009900; border-radius: 50% }' +
'.external-closed-temporarily a.url { border: 4px solid #ff7300; border-radius: 50% }' +
'.external-closed-permanently a.url { border: 4px solid #ff0000; border-radius: 50% }' +
'p.e50-info { border-top: 1px solid #ccc; color: #777; font-size: x-small; margin-top: 15px; padding-top: 10px; text-align: center; }' +
'#sidebar p.e50-blue { background-color:#0057B8;color:white;height:32px;text-align:center;line-height:32px;font-size:24px;margin:0; }' +
'#sidebar p.e50-yellow { background-color:#FFDD00;color:black;height:32px;text-align:center;line-height:32px;font-size:24px;margin:0; }'
WMEUI.addStyle(STYLE)
const layerConfig = {
defaultRule: {
styleContext: {
label: (context) => {
const style = context?.feature?.properties?.style;
if (!style)
return style;
return style?.label;
},
},
styleRules: [
{
predicate: (properties) => properties.styleName === "styleNode",
style: {
pointRadius: 8,
fillOpacity: 0.5,
fillColor: '#fff',
strokeColor: '#fff',
strokeWidth: 2,
strokeLinecap: 'round',
graphicZIndex: 9999,
},
},
{
predicate: (properties) => properties.styleName === "styleLine",
style: {
strokeWidth: 4,
strokeColor: '#fff',
strokeLinecap: 'round',
strokeDashstyle: 'dash',
label: "${label}",
labelOutlineColor: '#000',
labelOutlineWidth: 3,
labelAlign: 'cm',
fontColor: '#fff',
fontSize: '24px',
fontFamily: 'Courier New, monospace',
fontWeight: 'bold',
labelYOffset: 24,
graphicZIndex: 9999,
}
}
],
},
};
let E50Instance, E50Cache
class E50 extends WMEBase {
constructor (name, settings) {
super(name, settings)
this.initHelper()
this.initTab()
this.initLayer()
}
initHelper () {
this.helper = new WMEUIHelper(this.name)
this.modal = this.helper.createModal(I18n.t(this.name).title)
this.panel = this.helper.createPanel(I18n.t(this.name).title)
}
initTab () {
let tab = this.helper.createTab(
I18n.t(this.name).title,
{
sidebar: this.wmeSDK.Sidebar,
image: GM_info.script.icon
}
)
// Setup options
/** @type {WMEUIHelperFieldset} */
let fsOptions = this.helper.createFieldset(I18n.t(this.name).options.title)
let options = this.settings.get('options')
for (let item in options) {
if (options.hasOwnProperty(item)) {
fsOptions.addCheckbox(
item,
I18n.t(this.name).options[item],
(event) => this.settings.set(['options', item], event.target.checked),
this.settings.get('options', item)
)
}
}
tab.addElement(fsOptions)
// Setup ranges
/** @type {WMEUIHelperFieldset} */
let fsRanges = this.helper.createFieldset(I18n.t(this.name).ranges.title)
let ranges = this.settings.get('ranges')
for (let item in ranges) {
if (ranges.hasOwnProperty(item)) {
fsRanges.addNumber(
'settings-ranges-' + item,
I18n.t(NAME).ranges[item],
event => this.settings.set(['ranges', item], event.target.value),
this.settings.get('ranges', item),
(item === 'radius') ? 100 : 0,
(item === 'radius') ? 1000 : 10,
(item === 'radius') ? 50 : 1
)
}
}
tab.addElement(fsRanges)
// Setup providers settings
/** @type {WMEUIHelperFieldset} */
let fsProviders = this.helper.createFieldset(I18n.t(this.name).providers.title)
let providers = this.settings.get('providers')
for (let item in providers) {
if (providers.hasOwnProperty(item) && SETTINGS.providers.hasOwnProperty(item)) {
fsProviders.addCheckbox(
item,
I18n.t(this.name).providers[item],
(event) => this.settings.set(['providers', item], event.target.checked),
this.settings.get('providers', item)
)
}
}
tab.addElement(fsProviders)
// Setup providers key's
/** @type {WMEUIHelperFieldset} */
let fsKeys = this.helper.createFieldset(I18n.t(this.name).options.keys)
let keys = this.settings.get('keys')
for (let item in keys) {
if (keys.hasOwnProperty(item) && SETTINGS.keys.hasOwnProperty(item)) {
fsKeys.addInput(
'key-' + item,
I18n.t(this.name).providers[item],
(event) => this.settings.set(['keys', item], event.target.value),
this.settings.get('keys', item)
)
}
}
tab.addElement(fsKeys)
tab.addText(
'info',
'<a href="' + GM_info.scriptUpdateURL + '">' + GM_info.script.name + '</a> ' + GM_info.script.version
)
tab.addText('blue', 'made in')
tab.addText('yellow', 'Ukraine')
tab.inject()
}
initLayer () {
this.wmeSDK.Map.addLayer({
layerName: this.name,
styleRules: layerConfig.defaultRule.styleRules,
styleContext: layerConfig.defaultRule.styleContext
});
// this.wmeSDK.LayerSwitcher.addLayerCheckbox({ name: this.name });
this.wmeSDK.Map.setLayerZIndex({ layerName: this.name, zIndex: 9999 });
this.wmeSDK.Map.setLayerVisibility({ layerName: this.name, visibility: false });
}
/**
* Create the vector from the center of the selected POI to point by lon and lat
* @param {Number} lon
* @param {Number} lat
*/
createVector (lon, lat) {
let poi = this.getSelectedPOI()
if (!poi) {
return
}
const from = turf.centroid(poi.geometry)
const to = turf.point([lon, lat], { styleName: "styleNode" }, { id: `node_${lon}_${lat}` });
this.wmeSDK.Map.addFeatureToLayer({ layerName: this.name, feature: to });
const lineCoordinates = [
from.geometry.coordinates,
to.geometry.coordinates,
];
const distance = Math.round( turf.distance(to, from) * 1000)
const label = (distance > 2000)
? (distance / 1000).toFixed(1) + 'km'
: distance + 'm'
// https://www.waze.com/editor/sdk/interfaces/index.SDK.FeatureStyle.html
const line = turf.lineString(lineCoordinates, {
styleName: "styleLine",
style: {
label: label,
},
}, { id: `line_${lon}_${lat}` });
this.wmeSDK.Map.addFeatureToLayer({ layerName: this.name, feature: line });
}
/**
* Remove all vectors from the layer
*/
removeVectors () {
this.wmeSDK.Map.removeAllFeaturesFromLayer({ layerName: this.name });
}
/**
* Show the Layer
*/
showLayer () {
this.wmeSDK.Map.setLayerVisibility({ layerName: this.name, visibility: true });
}
/**
* Hide the Layer
*/
hideLayer () {
this.wmeSDK.Map.setLayerVisibility({ layerName: this.name, visibility: false });
}
/**
* Handler for `none.wme` event
* @param {jQuery.Event} event
* @return {Null}
*/
onNone (event) {
if (this.settings.get('options', 'modal')) {
this.modal.html().remove()
}
}
/**
* Handler for `venue.wme` event
* - create and fill the modal panel
*
* @param {jQuery.Event} event
* @param {HTMLElement} element
* @param {Venue} model
* @return {null|void}
*/
onVenue (event, element, model) {
let container, parent
if (this.settings.get('options', 'modal')) {
parent = this.modal.html()
container = parent.querySelector('.wme-ui-body')
} else {
parent = this.panel.html()
container = parent.querySelector('.controls')
}
// Clear container
try {
if (container)
while (container.hasChildNodes()) {
container.removeChild(container.lastChild)
}
} catch (e) {
console.error(e)
}
if (!model) {
return
}
let feature = turf.centroid(model.geometry)
let [lon, lat] = feature.geometry.coordinates;
let providers = []
let country = this.wmeSDK.DataModel.Countries.getTopCountry()?.id || 232
let settings = LOCALE[country]
this.group(
'📍' + lon + ' ' + lat
)
let radius = this.settings.get('ranges', 'radius')
if (this.settings.get('providers', 'magic')) {
let Magic = new MagicProvider(container, settings)
let providerPromise = Magic
.search(lon, lat, radius)
.then(() => Magic.render())
.catch(() => this.log(':('))
providers.push(providerPromise)
}
if (this.settings.get('providers', 'ua')) {
let UaAddresses = new UaAddressesProvider(container, settings, this.settings.get('keys', 'ua'))
let providerPromise = UaAddresses
.search(lon, lat, radius)
.then(() => UaAddresses.render())
.catch(() => this.log(':('))
providers.push(providerPromise)
}
if (this.settings.get('providers', 'osm')) {
let Osm = new OsmProvider(container, settings)
let providerPromise = Osm
.search(lon, lat, radius)
.then(() => Osm.render())
.catch(() => this.log(':('))
providers.push(providerPromise)
}
if (this.settings.get('providers', 'visicom')) {
let Visicom = new VisicomProvider(container, settings, this.settings.get('keys', 'visicom'))
let providerPromise = Visicom
.search(lon, lat, radius)
.then(() => Visicom.render())
.catch(() => this.log(':('))
providers.push(providerPromise)
}
if (this.settings.get('providers', 'here')) {
let Here = new HereProvider(container, settings, this.settings.get('keys', 'here'))
let providerPromise = Here
.search(lon, lat, radius)
.then(() => Here.render())
.catch(() => this.log(':('))
providers.push(providerPromise)
}
if (this.settings.get('providers', 'bing')) {
let Bing = new BingProvider(container, settings, this.settings.get('keys', 'bing'))
let providerPromise = Bing
.search(lon, lat, radius)
.then(() => Bing.render())
.catch(() => this.log(':('))
providers.push(providerPromise)
}
if (this.settings.get('providers', 'google')) {
let Google = new GoogleProvider(container, settings, this.settings.get('keys', 'google'))
let providerPromise = Google
.search(lon, lat, radius)
.then(() => Google.render())
.catch(() => this.log(':('))
providers.push(providerPromise)
}
if (this.settings.get('options', 'externalProvider')) {
if (model.externalProviderIds?.length) {
let items = element.querySelectorAll('.external-providers-control .external-provider')
for (let i = 0; i < model.externalProviderIds.length; i++) {
let externalProviderId = model.externalProviderIds[i]
let item = items[i]
GoogleProvider
.makeDetailsRequest(externalProviderId)
.then(details => {
let extLat = details.geometry.location.lat()
let extLng = details.geometry.location.lng()
let distance = turf.distance(
turf.point([lon, lat]),
turf.point([extLng, extLat]),
{
units: 'meters'
}
)
item.dataset.distance = Math.round(distance)
item.dataset.lat = extLat
item.dataset.lon = extLng
if (details.business_status === 'OPERATIONAL') {
item.classList.add('external-operational')
} else if (details.business_status === 'CLOSED_TEMPORARILY') {
item.classList.add('external-closed-temporarily')
} else if (details.business_status === 'CLOSED_PERMANENTLY') {
item.classList.add('external-closed-permanently')
}
item.classList.add(this.name + '-external')
if (distance > 1000) {
item.classList.add('distance-over-1000')
} else if (distance > 200) {
item.classList.add('distance-over-200')
}
})
.catch(() => { this.log(':(') })
}
}
}
Promise
.all(providers)
.then(() => this.groupEnd())
if (this.settings.get('options', 'modal')) {
if (this.settings.get('options', 'transparent')) {
parent.style.opacity = '0.6'
parent.onmouseover = () => (parent.style.opacity = '1')
parent.onmouseout = () => (parent.style.opacity = '0.6')
}
this.modal.container().append(parent)
} else {
element.prepend(parent)
}
}
/**
* Get Selected Venue if it not the NATURAL_FEATURES
* @return {null|Object}
*/
getSelectedPOI () {
let venue = this.getSelectedVenues().shift()
if (!venue) {
return null
}
let except = ['NATURAL_FEATURES']
if (except.indexOf(venue.categories[0]) === -1) {
return venue
}
return null
}
/**
* Apply data to the current selected place
* @param {Object} data
*/
applyData (data) {
let venue = this.getSelectedPOI()
if (!this.wmeSDK.DataModel.Venues.hasPermissions({ venueId: venue.id })) {
this.log('You don\'t have permissions to edit this venue')
return
}
let address = this.wmeSDK.DataModel.Venues.getAddress({ venueId: venue.id })
let lat = parseFloat(data.lat)
let lon = parseFloat(data.lon)
if (isNaN(lat) || isNaN(lon)) {
this.log('Invalid coordinates')
return
}
this.group('Apply data to selected Venue ↓')
let name = data.name ? data.name.trim() : ''
let cityId = isNaN(parseInt(data.cityId)) ? null : parseInt(data.cityId)
let cityName = data.cityName ? data.cityName.trim() : ''
let streetId = isNaN(parseInt(data.streetId)) ? null : parseInt(data.streetId)
let streetName = data.streetName ? data.streetName.trim() : ''
let number = data.number ? data.number.trim() : ''
if (this.settings.get('options', 'copyData')) {
toClipboard([name, number, streetName, cityName].filter(x => !!x).join(' '))
}
// Apply new Name
let newName
// If exists, ask the user to replace it or not
// If not exists - use name or house number as name
if (venue.name) {
this.log('The Venue has a Name «' + venue.name + '»' )
if (name && name !== venue.name) {
this.log('Replace a Venue Name with a new one?' )
if (window.confirm(I18n.t(NAME).questions.changeName + '\n«' + venue.name + '» ⟶ «' + name + '»?')) {
newName = name
this.log(' — Yes, a new Venue Name is «' + newName + '»' )
} else {
newName = venue.name
this.log(' — No, use a old Venue Name «' + newName + '»' )
}
} else if (number && number !== venue.name) {
this.log('Replace the Venue Name with a number?' )
if (window.confirm(I18n.t(NAME).questions.changeName + '\n«' + venue.name + '» ⟶ «' + number + '»?')) {
newName = number
this.log(' — Yes, a new Venue Name is «' + newName + '»' )
} else {
newName = venue.name
this.log(' — No, use a old Venue Name «' + newName + '»' )
}
}
} else if (name) {
newName = name
this.log('Use a new Venue Name «' + newName + '»' )
} else if (number) {
newName = number
this.log('Use a new Venue Name «' + newName + '»' )
// Update alias for korpus
if ((new RegExp('[0-9]+[а-яі]?к[0-9]+', 'i')).test(number)) {
let alias = number.replace('к', ' корпус ')
let aliases = venue.aliases?.slice() || []
if (aliases.indexOf(alias) === -1) {
aliases.push(alias)
this.log('Apply a new Venue Alias «' + alias + '»' )
this.wmeSDK.DataModel.Venues.updateVenue({
venueId: venue.id,
aliases: aliases
})
}
}
}
// Set only really new name
if (newName && newName !== venue.name) {
this.log('Apply a new Venue Name «' + newName + '»' )
this.wmeSDK.DataModel.Venues.updateVenue({
venueId: venue.id,
name: newName
})
}
// Apply a City name
if (!cityId && cityName) {
this.log('We don\'t find a City with name «' + cityName + '», create a new one?' )
// Ask to create a new City
if (window.confirm(I18n.t(NAME).questions.notFoundCity + '\n«' + cityName + '»?')) {
cityId = this.getCity(cityName).id
this.log(' — Yes, create new City «' + cityName + '»' )
} else {
cityId = this.getCity().id
this.log(' — No, use the empty City with ID «' + cityId + '»' )
}
} else if (!cityId && !cityName) {
cityId = this.getCity().id
this.log('We don\'t find a City and use the empty City with ID «' + cityId + '»' )
}
let city = this.getCityById(cityId)
let newStreetId
// Apply a new Street
if (streetId && address.street
&& streetId !== address.street.id
&& '' !== address.street.name) {
this.log('Replace the Street with a new one?')
if (window.confirm(I18n.t(NAME).questions.changeStreet + '\n«' + address.street.name + '» ⟶ «' + streetName + '»?')) {
newStreetId = streetId
this.log(' — Yes, use a new Street Name «' + streetName + '»')
} else {
this.log(' — No, use a old Street Name «' + address.street.name + '»')
}
} else if (streetId) {
newStreetId = streetId
this.log('Use a new Street with ID «' + newStreetId + '»')
} else if (!streetId) {
let street
if (streetName) {
this.log('We don\'t find the street «' + streetName + '»')
this.log('Create a new Street?')
if (window.confirm(I18n.t(NAME).questions.notFoundStreet + '\n«' + streetName + '»?')) {
street = this.getStreet(city.id, streetName)
this.log(' — Yes, create a new Street «' + streetName + '»')
} else if ('' !== address.street?.name) {
street = this.wmeSDK.DataModel.Streets.getById( { streetId: address.street.id } )
this.log(' — No, use the current Street «' + street.name + '»')
} else {
street = this.getStreet(city.id, '')
this.log(' — No, use the empty Street with ID «' + street.id + '»')
}
} else {
this.log('We don\'t find the street')
street = this.getStreet(city.id, '')
this.log('Use the empty Street with ID «' + street.id + '»')
}
if (street.id !== address.street?.id && '' !== address.street?.name) {
this.log('Replace the Street with new one?')
if (window.confirm(I18n.t(NAME).questions.changeStreet + '\n«' + address.street.name + '» ⟶ «' + streetName + '»?')) {
newStreetId = street.id
this.log(' — Yes, use a new Street Name «' + streetName + '»')
} else {
this.log(' — No, use the current Street Name «' + address.street.name + '»')
}
} else {
newStreetId = street.id
}
}
if (newStreetId && newStreetId !== address.street?.id) {
this.log('Apply a new Street ID «' + newStreetId + '»' )
this.wmeSDK.DataModel.Venues.updateAddress({
venueId: venue.id,
streetId: newStreetId
})
}
let newHouseNumber
// Apply a House Number
if (number) {
if (address.houseNumber) {
this.log('Replace the House Number with a new one?')
if (address.houseNumber !== number &&
window.confirm(I18n.t(NAME).questions.changeNumber + '\n«' + address.houseNumber + '» ⟶ «' + number + '»?')) {
newHouseNumber = number
this.log(' — Yes, use a new House Number «' + number + '»')
} else {
this.log(' — No, use the current House Number «' + address.houseNumber + '»')
}
} else {
newHouseNumber = number
this.log('Use a new House Number «' + number + '»')
}
}
if (newHouseNumber) {
this.log('Apply a new House Number «' + newHouseNumber + '»' )
this.wmeSDK.DataModel.Venues.updateAddress({
venueId: venue.id,
houseNumber: newHouseNumber
})
}
// Lock to level 2
if (this.settings.get('options', 'lock')
&& venue.lockRank < 1
&& this.wmeSDK.State.getUserInfo().rank > 0) {
this.log('Apply a new Lock Rank «' + (1+1) + '»' )
this.wmeSDK.DataModel.Venues.updateVenue({
venueId: venue.id,
lockRank: 1
})
}
// If no an entry point, we would create it
if (this.settings.get('options', 'entryPoint')
&& venue.navigationPoints?.length === 0) {
this.log('Create a Navigation Point')
let point = turf.point([lon, lat])
if (venue.geometry.type === 'Point') {
this.log('Use the coordinates for new Navigation Point for Point')
} else if (turf.pointsWithinPolygon(point, venue.geometry).features?.length > 0) {
this.log('Use the coordinates for new Navigation Point inside Polygon')
} else {
// point is outside the venue geometry
this.log('Use the intersection of Polygon and vector to coordinates as new Navigation Point')
let centroid = turf.centroid(venue.geometry);
let line = turf.lineString([
centroid.geometry.coordinates,
point.geometry.coordinates,
]);
let featureCollection = turf.lineIntersect(venue.geometry, line);
point = featureCollection.features?.pop()
}
// create a navigation point
let navigationPoint = {
isEntry: true,
isExit: false,
isPrimary: true,
name: "",
point: point.geometry
}
this.log('Apply a new Navigation Point')
this.wmeSDK.DataModel.Venues.replaceNavigationPoints({
venueId: venue.id,
navigationPoints: [navigationPoint]
})
}
this.groupEnd()
}
getCityById (cityID) {
if (!cityID || isNaN(parseInt(cityID))) {
return null
}
return this.wmeSDK.DataModel.Cities.getById({
cityId: cityID
})
}
getCity (cityName = '') {
return this.wmeSDK.DataModel.Cities.getCity({
countryId: this.wmeSDK.DataModel.Countries.getTopCountry().id,
cityName: cityName
})
|| this.wmeSDK.DataModel.Cities.addCity({
countryId: this.wmeSDK.DataModel.Countries.getTopCountry().id,
cityName: cityName
})
}
getStreet (cityId, streetName = '') {
return this.wmeSDK.DataModel.Streets.getStreet({
cityId: cityId,
streetName: streetName,
})
|| this.wmeSDK.DataModel.Streets.addStreet({
cityId: cityId,
streetName: streetName
})
}
}
/**
* Basic Provider class
*/
class Provider {
constructor (uid, container, settings) {
this.uid = uid.trim().toLowerCase().replace(/\s/g, '-')
this.name = uid
this.response = []
this.settings = settings
// prepare DOM
this.panel = this._panel()
this.container = container
this.container.append(this.panel)
}
/**
* @param {String} url
* @param {Object} data
* @returns {Promise<unknown>}
*/
async makeRequest (url, data) {
let query = new URLSearchParams(data).toString()
if (query.length) {
url = url + '?' + query
}
// console.log(url)
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
responseType: 'json',
url: url,
onload: response => response && response.response && resolve(response.response) || reject(response),
onabort: response => reject(response),
onerror: response => reject(response),
ontimeout: response => reject(response),
})
})
}
/**
* @param {Number} lon
* @param {Number} lat
* @param {Number} radius
* @return {Promise<array>}
*/
async request (lon, lat, radius) {
throw new Error('Abstract method')
}
/**
* @param {Number} lon
* @param {Number} lat
* @param {Number} radius
* @return {Promise<void>}
*/
async search (lon, lat, radius = 1000) {
let key = this.uid + ':' + lon + ',' + lat
if (E50Cache.has(key)) {
this.response = E50Cache.get(key)
} else {
this.response = await this.request(lon, lat, radius).catch(e => console.error(this.uid, 'search return error', e))
E50Cache.set(key, this.response)
}
return new Promise((resolve, reject) => {
if (this.response) {
resolve()
} else {
reject()
}
})
}
/**
* @param {Array} res
* @return {Array}
*/
collection (res) {
let result = []
for (let i = 0; i < res.length; i++) {
result.push(this.item(res[i]))
}
result = result.filter(x => x)
return result
}
/**
* Should return {Object}
* @param {Object} res
* @return {Object}
*/
item (res) {
throw new Error('Abstract method')
}
/**
* @param {Number} lon
* @param {Number} lat
* @param {String} city
* @param {String} street
* @param {String} number
* @param {String} name
* @param {String} reference
* @return {{number: *, cityId: Number, cityName: *, streetId: Number, streetName: *, name: *, raw: *, lon: *, title: *, lat: *}}
*/
element (lon, lat, city, street, number, name = '', reference = '') {
// Raw data from provider
let raw = [city, street, number, name].filter(x => !!x).join(', ')
{
city = normalizeCity(city)
street = normalizeStreet(street)
number = normalizeNumber(number)
name = normalizeName(name)
}
let [cityId, cityName] = detectCity(city)
let [streetId, streetName] = detectStreet(cityId, street)
if (!cityId && streetId) {
let streetModel = E50Instance.wmeSDK.DataModel.Streets.getById( { streetId: streetId } )
let cityModel = E50Instance.wmeSDK.DataModel.Cities.getById( { cityId: streetModel.cityId } )
cityId = cityModel.id
cityName = cityModel.name
}
let title = [street, number, name].filter(x => !!x).join(', ')
return {
lat: lat,
lon: lon,
cityId: cityId,
cityName: cityName,
streetId: streetId,
streetName: streetName,
number: number,
name: name,
title: title,
raw: raw,
reference: reference
}
}
/**
* Render result to target element
*/
render () {
if (this.response.length === 0) {
// remove empty panel
this.panel.remove()
return
}
this.panel.append(this._fieldset())
}
/**
* Create div for all items
* @return {HTMLDivElement}
* @private
*/
_panel () {
let div = document.createElement('div')
div.id = NAME.toLowerCase() + '-' + this.name
div.className = NAME.toLowerCase()
return div
}
/**
* Build fieldset with the list of the response items
* @return {HTMLFieldSetElement}
* @protected
*/
_fieldset () {
let fieldset = document.createElement('fieldset')
let list = document.createElement('ul')
let collapse = parseInt(E50Instance.settings.get('ranges', 'collapse'))
if (collapse && this.response.length > collapse) {
fieldset.className = 'collapsed'
} else {
fieldset.className = ''
}
for (let i = 0; i < this.response.length; i++) {
let item = document.createElement('li')
item.append(this._link(this.response[i]))
list.append(item)
}
let legend = document.createElement('legend')
legend.innerHTML = this.name + ' <span>' + this.response.length + '</span>'
legend.onclick = function () {
this.parentElement.classList.toggle("collapsed")
return false
}
fieldset.append(legend, list)
return fieldset
}
/**
* Build link by {Object}
* @param {Object} item
* @return {HTMLAnchorElement}
* @protected
*/
_link (item) {
let a = document.createElement('a')
a.href = '#'
a.dataset.lat = item.lat
a.dataset.lon = item.lon
a.dataset.cityId = item.cityId || ''
a.dataset.cityName = item.cityName || ''
a.dataset.streetId = item.streetId || ''
a.dataset.streetName = item.streetName || ''
a.dataset.number = item.number
a.dataset.name = item.name
a.dataset.reference = item.reference || ''
a.innerText = item.title || item.raw
a.title = item.raw
a.className = NAME + '-link'
if (!item.cityId || !item.streetId) {
a.className += ' noaddress'
}
if (!item.number) {
a.className += ' nonumber'
}
return a
}
}
/**
* Based on the closest segment and city
*/
class MagicProvider extends Provider {
constructor (container, settings) {
super(I18n.t(NAME).providers.magic, container, settings)
}
async request (lon, lat, radius) {
let segments = E50Instance.getAllSegments(
[TYPES.boardwalk, TYPES.stairway, TYPES.railroad, TYPES.runway, TYPES.parking]
)
let streets = {}
console.groupCollapsed(this.uid)
for (let key in segments) {
let segment = segments[key]
let address = E50Instance.wmeSDK.DataModel.Segments.getAddress({ segmentId: segment.id })
if (address.street.name === '') {
continue
}
let distance = turf.pointToLineDistance(
turf.point([lon, lat]),
segment.geometry,
{
units: 'meters'
}
)
if (!streets[address.street.id]
|| distance < streets[address.street.id].distance) {
let nearestPointOnLine = turf.nearestPointOnLine(
segment.geometry,
turf.point([lon, lat])
)
streets[address.street.id] = {
lon: nearestPointOnLine.geometry.coordinates[0],
lat: nearestPointOnLine.geometry.coordinates[1],
streetId: address.street.id,
streetName: address.street.name,
cityId: address.city.id,
cityName: address.city.name,
number: '',
name: '',
title: address.street.name,
raw: address.city.name + ', ' + address.street.name,
distance: distance,
}
}
}
let result = []
for (let key in streets) {
if (streets.hasOwnProperty(key) && streets[key].distance <= radius) {
result.push(streets[key])
}
}
result.sort((a, b) => {
if (a.distance < b.distance) {
return -1;
}
if (a.distance > b.distance) {
return 1;
}
return 0;
})
console.log(result.length + ' streets found.')
console.groupEnd(this.uid)
return result
}
}
/**
* US Addresses
*/
class UaAddressesProvider extends Provider {
constructor (container, settings, key) {
super(I18n.t(NAME).providers.ua, container, settings)
this.key = key
}
async request (lon, lat, radius) {
let result = []
let url = 'https://stat.waze.com.ua/address_map/address_map.php'
let data = {
lon: lon,
lat: lat,
radius: radius,
limit: 20,
script: this.key
}
let response = await this.makeRequest(url, data).catch(e => console.error(this.uid, 'return error', e))
console.groupCollapsed(this.uid)
if (response?.result && response.result === 'success') {
result = this.collection(response.data.polygons.Default)
} else {
console.info('No response returned')
}
console.groupEnd(this.uid)
return result
}
item (res) {
let data = res.name.split(",")
data = data.map(part => part.trim())
let number = data.length ? data.pop() : null
let street = data.length ? data.pop() : null
let city = data.length ? data.pop() : null
// https://cdn.jsdelivr.net/npm/wellknown@0.5.0/wellknown.min.js
// let element = wellknown.parse(res.polygon);
// let center = turf.centroid(element)
// center.geometry.coordinates[0],
// center.geometry.coordinates[1],
let [lat, lon] = res.center.split(';')
return this.element(
lon,
lat,
city,
street,
number
)
}
}
/**
* visicom.ua
*/
class VisicomProvider extends Provider {
constructor (container, settings, key) {
super(I18n.t(NAME).providers.visicom, container, settings)
this.key = key
}
async request (lon, lat, radius) {
let result = []
let url = 'https://api.visicom.ua/data-api/5.0/uk/geocode.json'
let data = {
near: lon + ',' + lat,
categories: 'adr_address',
order: 'distance',
radius: radius,
limit: 10,
key: this.key,
}
let response = await this.makeRequest(url, data).catch(e => console.error(this.uid, 'return error', e))
console.groupCollapsed(this.uid)
if (response?.features?.length > 0) {
result = this.collection(response.features)
} else {
console.info('No response returned')
if (response?.status) {
console.info('Status:', response.status)
}
}
console.groupEnd(this.uid)
return result
}
item (res) {
let city = ''
let street = ''
let number = ''
if (res.properties.settlement) {
city = res.properties.settlement
}
if (res.properties.street) {
street = res.properties.street_type + ' ' + res.properties.street
}
if (res.properties.name) {
number = res.properties.name
}
return this.element(res.geo_centroid.coordinates[0], res.geo_centroid.coordinates[1], city, street, number)
}
}
/**
* OpenStreetMap
*/
class OsmProvider extends Provider {
constructor (container, settings) {
super(I18n.t(NAME).providers.osm, container, settings)
}
async request (lon, lat, radius) {
let result = []
let url = 'https://nominatim.openstreetmap.org/reverse'
let data = {
lon: lon,
lat: lat,
zoom: 18,
addressdetails: 1,
countrycodes: this.settings.language,
'accept-language': this.settings.locale,
format: 'json',
}
let response = await this.makeRequest(url, data).catch(e => console.error(this.uid, 'return error', e))
console.groupCollapsed(this.uid)
if (response?.address) {
result = [this.item(response)]
} else {
console.info('No response returned')
}
console.groupEnd(this.uid)
return result
}
item (res) {
let city = ''
let street = ''
let number = ''
if (res.address.city) {
city = res.address.city
} else if (res.address.town) {
city = res.address.town
}
if (res.address.road) {
street = res.address.road
}
if (res.address.house_number) {
number = res.address.house_number
}
return this.element(res.lon, res.lat, city, street, number)
}
}
/**
* Here Maps
* @link https://developer.here.com/documentation/geocoder/topics/quick-start-geocode.html
* @link https://www.here.com/docs/bundle/geocoder-api-developer-guide/page/topics/resource-reverse-geocode.html
*/
class HereProvider extends Provider {
constructor (container, settings, key) {
super(I18n.t(NAME).providers.here, container, settings)
this.key = key
}
async request (lon, lat, radius) {
let result = []
let url = 'https://revgeocode.search.hereapi.com/v1/revgeocode'
let data = {
apiKey: this.key,
at: lat + ',' + lon,
types: 'address',
limit: 20
}
let response = await this.makeRequest(url, data).catch(e => console.error(this.uid, 'return error', e))
console.groupCollapsed(this.uid)
if (response?.items?.length) {
result = this.collection(
response.items.filter(x => x.resultType === 'houseNumber')
)
} else {
console.info('No response returned')
}
console.groupEnd(this.uid)
return result
}
item (res) {
console.log(res)
return this.element(
res.position.lng,
res.position.lat,
res.address.city,
res.address.street,
res.address.houseNumber
)
}
}
/**
* Bing Mapі DISABLED
* @link https://docs.microsoft.com/en-us/bingmaps/rest-services/locations/find-a-location-by-point
* http://dev.virtualearth.net/REST/v1/Locations/50.03539,36.34732?o=xml&key=AuBfUY8Y1Nzf3sRgceOYxaIg7obOSaqvs0k5dhXWfZyFpT9ArotYNRK7DQ_qZqZw&c=uk
* http://dev.virtualearth.net/REST/v1/Locations/50.03539,36.34732?o=xml&key=AuBfUY8Y1Nzf3sRgceOYxaIg7obOSaqvs0k5dhXWfZyFpT9ArotYNRK7DQ_qZqZw&c=uk&includeEntityTypes=Address
*/
class BingProvider extends Provider {
constructor (container, settings, key) {
super(I18n.t(NAME).providers.bing, container, settings)
this.key = key
}
async request (lon, lat, radius) {
let result = []
let url = 'https://dev.virtualearth.net/REST/v1/Locations/' + lat + ',' + lon
let data = {
includeEntityTypes: 'Address',
c: this.settings.country,
key: this.key,
}
let response = await this.makeRequest(url, data).catch(e => console.error(this.uid, 'return error', e))
console.groupCollapsed(this.uid)
if (response?.resourceSets?.[0]?.resources?.length) {
result = this.collection(
response.resourceSets[0].resources.filter(
el => el.address?.addressLine?.includes(',')
)
);
} else {
console.info('No response returned')
}
console.groupEnd(this.uid)
return result
}
item (res) {
let address = res.address.addressLine.split(',')
return this.element(
res.point.coordinates[1],
res.point.coordinates[0],
res.address.locality,
address[0],
address[1]
)
}
}
/**
* Google Place
* @link https://developers.google.com/places/web-service/search
*/
class GoogleProvider extends Provider {
constructor (container, settings, key) {
super(I18n.t(NAME).providers.google, container, settings)
this.key = key
}
async request (lon, lat, radius) {
let result = []
let response = await this.makeAPIRequest(lat, lon, radius)
.catch(e => null)
//.catch(e => console.error(this.uid, 'return error', e))
console.groupCollapsed(this.uid)
if (response?.length) {
result = this.collection(response)
} else {
console.info('No response returned')
}
console.groupEnd(this.uid)
return result
}
async makeAPIRequest (lat, lon, radius) {
let center = new google.maps.LatLng(lat, lon)
let map = new google.maps.Map(document.createElement('div'), { center: center })
let request = {
location: center,
radius: radius,
type: 'point_of_interest',
// doesn't work
// fields: ['name', 'address_component', 'geometry'],
// language: this.settings.country,
}
let service = new google.maps.places.PlacesService(map)
return new Promise((resolve, reject) => {
service.nearbySearch(request, (results, status) => {
if (status === google.maps.places.PlacesServiceStatus.OK) {
resolve(results)
} else {
reject(status)
}
})
})
}
item (res) {
let address = res.vicinity.split(',')
address = address.map(str => str.trim())
// looks like hell
let street = address[0] && address[0].length > 4 ? address[0] : ''
let number = address[1] && address[1].length < 13 ? address[1] : ''
let city = address[2] ? address[2] : ''
return this.element(
res.geometry.location.lng(),
res.geometry.location.lat(),
city,
street,
number,
res.name,
res.reference
)
}
/**
* Details about a specific object or entity.
*
* This variable is used to encapsulate information or attributes
* related to a particular subject. The structure and type of the
* details may vary depending on the specific application or use-case.
*/
static async makeDetailsRequest(reference) {
// We need a map instance to initialize the service (even a dummy one)
let map = new google.maps.Map(document.createElement('div'))
let service = new google.maps.places.PlacesService(map)
let request = {
placeId: reference, // Google now uses placeId instead of reference
// Specifying fields is cheaper and faster
fields: ['business_status', 'geometry', 'name', 'place_id', 'vicinity']
}
return new Promise((resolve, reject) => {
service.getDetails(request, (place, status) => {
if (status === google.maps.places.PlacesServiceStatus.OK) {
resolve(place)
} else {
reject(status)
}
})
})
}
}
$(document)
.on('bootstrap.wme', ready)
.on('click', '.' + NAME + '-link', applyData)
.on('mouseenter', '.' + NAME + '-link', showLayer)
.on('mouseleave', '.' + NAME + '-link', hideLayer)
.on('mouseenter', '.' + NAME + '-external', showLayer)
.on('mouseleave', '.' + NAME + '-external', hideLayer)
.on('none.wme', hideLayer)
/**
* Initializes the `E50Instance` and `E50Cache` objects with predefined configurations.
*
* @return {void} This function does not return a value.
*/
function ready () {
E50Instance = new E50(NAME, SETTINGS)
E50Cache = new SimpleCache()
}
/**
* Apply data to the current selected POI
* @param event
*/
function applyData (event) {
event.preventDefault()
E50Instance.applyData(event.target.dataset)
}
/**
* Create the vector from the center of the selected POI to point by lon and lat
*/
function showLayer (event) {
const lon = parseFloat(event.target.dataset.lon)
const lat = parseFloat(event.target.dataset.lat)
E50Instance.createVector(lon, lat)
E50Instance.showLayer()
}
/**
* Remove all vectors and hide the layer
*/
function hideLayer () {
E50Instance.removeVectors()
E50Instance.hideLayer()
}
/**
* Copy to clipboard
* @param text
*/
function toClipboard (text) {
// normalize
text = normalizeString(text)
text = text.replace(/'/g, '')
GM.setClipboard(text)
console.log(
'%c' + NAME + ': %cCopied «' + text + '» to the clipboard',
'color: #0DAD8D; font-weight: bold',
'color: dimgray; font-weight: normal'
)
}
/**
* Normalize the string:
* - remove the double quotes
* - remove double space
* @param {String} str
* @returns {String}
*/
function normalizeString (str) {
// Clear space symbols and double quotes
str = str.trim()
.replace(/["“”]/g, '')
.replace(/\s{2,}/g, ' ')
// Clear accents/diacritics, but "\u0306" needed for "й"
// str = str.normalize('NFD').replace(/[\u0300-\u0305\u0309-\u036f]/g, '');
return str
}
/**
* Normalize the name:
* - remove № and # chars
* - remove dots
* @param {String} name
* @return {String}
*/
function normalizeName (name) {
name = normalizeString(name)
name = name.replace(/[№#]/g, '')
name = name.replace(/\.$/, '')
return name
}
/**
* Normalize the city name
* @param {String} city
* @return {String}
*/
function normalizeCity (city) {
return normalizeString(city)
}
/**
* Search the city name from available in the editor area
* @param {String} city
* @return {[Number,String]}
*/
function detectCity(city) {
// Get the list of all available cities
let cities = E50Instance.wmeSDK.DataModel.Cities.getAll()
.filter(city => city.name)
console.log("Total found " + cities.length + " cities.")
// More than one city, use city with best matching score
// Remove text in the "()"; Waze puts the region name to the pair brackets
let best = findBestMatch(city, cities.map(city => city.name.replace(/( ?\(.*\))/gi, '')))
if (best > -1) {
console.info("✅ City detected")
return [cities[best]['id'], cities[best]['name']]
/*} else if (cities.length === 1) {
console.info("❎ City doesn't found, uses default city")
return [cities[0]['id'], cities[0]['name']]*/
} else {
console.info("❌ City doesn't found")
return [null, city]
}
}
/**
* Normalize the street name by UA rules
* @param {String} street
* @return {String}
*/
function normalizeStreet (street) {
street = normalizeString(street)
if (street === '') {
return ''
}
// Prepare street name
street = street.replace(/[’']/, '\'')
// Remove text in the "()", OSM puts alternative name to the pair brackets
street = street.replace(/( ?\(.*\))/gi, '')
// Normalize title
let regs = {
'(^| )бульвар( |$)': '$1б-р$2', // normalize
'(^| )вїзд( |$)': '$1в\'їзд$2', // fix mistakes
'(^| )в\'ізд( |$)': '$1в\'їзд$2', // fix mistakes
'(^|.+?) ?вулиця ?(.+|$)': '$1$2', // remove type 'вулиця'
'(^|.+?) ?улица ?(.+|$)': '$1$2', // remove type 'улица'
'^(.+) в?ул\\.?$': '$1', // remove 'вул' suffix
'^в?ул\\.? (.+)$': '$1', // remove leading 'вул'
'(^| )дорога( |$)': '$1дор.$2', // normalize
'(^| )мікрорайон( |$)': '$1мкрн.$2', // normalize
'(^| )набережна( |$)': '$1наб.$2', // normalize
'(^| )площадь( |$)': '$1площа$2', // translate
'(^| )провулок провулок( |$)': '$1пров.$2', // O_o
'(^| )провулок( |$)': '$1пров.$2', // normalize
//'(^| )проїзд( |$)': '$1пр.$2', // normalize
'(^| )проспект( |$)': '$1просп.$2', // normalize
'(^| )район( |$)': '$1р-н$2', // normalize
'(^| )станція( |$)': '$1ст.$2', // normalize
}
for (let key in regs) {
let re = new RegExp(key, 'gi')
if (re.test(street)) {
street = street.replace(re, regs[key])
break
}
}
return street
}
/**
* Search the street name from available in the editor area
* Normalize the street name by UA rules
* @param {Number} cityId
* @param {String} street
* @return {[Number,String]}
*/
function detectStreet (cityId, street) {
// It can be empty
if (street.trim() === '') {
return [null, null]
}
// Get all streets
let streets = E50Instance.wmeSDK.DataModel.Streets.getAll()
.filter(street => street.cityId === cityId)
.filter(street => street.name)
// Get type and create RegExp for filter streets
let reTypes = new RegExp('(алея|б-р|в\'їзд|вул\\.|дор\\.|мкрн|наб\\.|площа|пров\\.|проїзд|просп\\.|р-н|ст\\.|тракт|траса|тупик|узвіз|шосе)', 'gi')
let matches = [...street.matchAll(reTypes)]
let types = []
// Detect type(s) — do not add 'вул.' by default; use detected types if present
types = matches.map(match => match[0].toLowerCase())
// Filter streets by detected type(s); if none detected, consider all streets
let filteredStreets = types.length > 0 ? streets.filter(street => types.some(type => street.name.indexOf(type) > -1)) : streets
// Matching names without type(s)
let best = findBestMatch(
street.replace(reTypes, '').toLowerCase().trim(),
filteredStreets.map(street => street.name.replace(reTypes, '').toLowerCase().trim())
)
if (best > -1) {
return [filteredStreets[best]['id'], filteredStreets[best]['name']]
} else {
return [null, street]
}
}
/**
* Normalize the number by UA rules
* @param {String} number
* @return {String}
*/
function normalizeNumber (number) {
// invalid data as a number
if (number?.trim().length > 16) {
return ''
}
// process "д."
number = number.replace(/^д\. ?/i, '')
// process "дом"
number = number.replace(/^дом ?/i, '')
// process "буд."
number = number.replace(/^буд\. ?/i, '')
// remove spaces
number = number.trim().replace(/\s/g, '')
number = number.toUpperCase()
// process Latin to Cyrillic
number = number.replace('A', 'А')
number = number.replace('B', 'В')
number = number.replace('E', 'Е')
number = number.replace('I', 'І')
number = number.replace('K', 'К')
number = number.replace('M', 'М')
number = number.replace('H', 'Н')
number = number.replace('О', 'О')
number = number.replace('P', 'Р')
number = number.replace('C', 'С')
number = number.replace('T', 'Т')
number = number.replace('Y', 'У')
// process і, з, о
number = number.replace('І', 'і')
number = number.replace('З', 'з')
number = number.replace('О', 'о')
// process "корпус" to "к"
number = number.replace(/(.*)к(?:орп|орпус)?(\d+)/gi, '$1к$2')
// process "N-M" or "N/M" to "NM"
number = number.replace(/(.*)[-/]([а-яі])/gi, '$1$2')
// valid number format
// 123А 123А/321 123А/321Б 123к1 123Ак2
/*if (!number.match(/^\d+[а-яі]?([/к]\d+[а-яі]?)?$/gi)) {
return ''
}*/
return number
}
/**
* @link https://github.com/aceakash/string-similarity
* @param {String} first
* @param {String} second
* @return {Number}
*/
function compareTwoStrings (first, second) {
first = first.replace(/\s+/g, '')
second = second.replace(/\s+/g, '')
if (!first.length && !second.length) return 1 // if both are empty strings
if (!first.length || !second.length) return 0 // if only one is empty string
if (first === second) return 1 // identical
if (first.length === 1 && second.length === 1) return 0 // both are 1-letter strings
if (first.length < 2 || second.length < 2) return 0 // if either is a 1-letter string
let firstBigrams = new Map()
for (let i = 0; i < first.length - 1; i++) {
const bigram = first.substring(i, i + 2)
const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) + 1 : 1
firstBigrams.set(bigram, count)
}
let intersectionSize = 0
for (let i = 0; i < second.length - 1; i++) {
const bigram = second.substring(i, i + 2)
const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) : 0
if (count > 0) {
firstBigrams.set(bigram, count - 1)
intersectionSize++
}
}
return (2.0 * intersectionSize) / (first.length + second.length - 2)
}
/**
* @param {String} mainString
* @param {String[]} targetStrings
* @return {Number}
*/
function findBestMatch (mainString, targetStrings) {
let bestMatch = ''
let bestMatchRating = 0
let bestMatchIndex = -1
for (let i = 0; i < targetStrings.length; i++) {
let rating = compareTwoStrings(mainString, targetStrings[i])
if (rating > bestMatchRating) {
bestMatch = targetStrings[i]
bestMatchRating = rating
bestMatchIndex = i
}
}
if (bestMatch === '' || bestMatchRating < 0.35) {
console.log('❌', mainString, '🆚', targetStrings)
return -1
} else {
console.log('✅', mainString, '🆚', bestMatch, ':', bestMatchRating)
return bestMatchIndex
}
}
})()

