// ==UserScript==
// @name WME Map Nav History
// @name:de WME Karten-Navigations-Historie
// @name:es WME Historial de Navegación del Mapa
// @name:fr WME Historique de Navigation de Carte
// @name:it WME Cronologia di Navigazione Mappa
// @description Navigate through map history using Alt + arrow keys, mouse control, clickable history sidebar, and location highlighting
// @description:de Ermöglicht die Navigation durch die Kartenhistorie mit Alt + Pfeiltasten, Maussteuerung, klickbarer Historie-Sidebar und Ortsmarkierungen
// @description:es Permite navegar por el historial del mapa usando Alt + teclas de flecha, control del ratón, barra lateral de historial clicable y resaltado de ubicaciones
// @description:fr Permet de naviguer dans l'historique de la carte en utilisant Alt + touches fléchées, contrôle de la souris, barre latérale d'historique cliquable et mise en évidence des emplacements
// @description:it Consente di navigare nella cronologia della mappa utilizzando Alt + tasti freccia, controllo del mouse, barra laterale della cronologia cliccabile e evidenziazione delle posizioni
// @namespace https://greasyfork.dpdns.org/de/users/863740-horst-wittlich
// @version 2025.06.04
// @author hiwi234
// @include https://www.waze.com/editor*
// @include https://www.waze.com/*/editor*
// @include https://beta.waze.com/*
// @exclude https://www.waze.com/user/*editor/*
// @exclude https://www.waze.com/*/user/*editor/*
// @grant none
// @license MIT
// ==/UserScript==
/* global W, OpenLayers */
(function() {
'use strict';
let navigationHistory = [];
let currentIndex = -1;
let isInitialized = false;
let lastSaveTime = 0;
let currentLanguage = (navigator.language || navigator.userLanguage).substring(0, 2);
let highlightedPositions = {};
const translations = {
en: {
scriptActive: "Script active",
back: "Back",
forward: "Forward",
controls: "Controls",
previousPosition: "Previous position",
nextPosition: "Next position",
backButton: "Back button",
forwardButton: "Forward button",
hints: "Hints",
historyLimit: "History stores up to 100 positions",
autoSave: "New positions are automatically saved when moving the map",
history: "History",
clearHistory: "Clear History",
noHistory: "No history available",
currentPosition: "Current Position",
markPosition: "Mark Position",
unmarkPosition: "Unmark Position",
markedPositions: "Marked Positions",
clickToNavigate: "Click on history entry for direct jump",
autoSaveDescription: "Automatic save and loading on next start",
confirmClearHistory: "Are you sure you want to delete the entire history?",
locationName: "Location",
zoomLevel: "Zoom Level"
},
de: {
scriptActive: "Script aktiv",
back: "Zurück",
forward: "Vorwärts",
controls: "Steuerung",
previousPosition: "Vorherige Position",
nextPosition: "Nächste Position",
backButton: "Zurück-Button",
forwardButton: "Vorwärts-Button",
hints: "Hinweise",
historyLimit: "Die Historie speichert bis zu 100 Positionen",
autoSave: "Neue Positionen werden beim Verschieben der Karte automatisch gespeichert",
history: "Verlauf",
clearHistory: "Verlauf löschen",
noHistory: "Kein Verlauf verfügbar",
currentPosition: "Aktuelle Position",
markPosition: "Position markieren",
unmarkPosition: "Position entfernen",
markedPositions: "Markierte Positionen",
clickToNavigate: "Klick auf Historie-Eintrag für direkten Sprung",
autoSaveDescription: "Verlauf wird automatisch gespeichert und beim nächsten Start geladen",
confirmClearHistory: "Sind Sie sicher, dass Sie den gesamten Verlauf löschen möchten?",
locationName: "Ort",
zoomLevel: "Zoom-Stufe"
},
es: {
scriptActive: "Script activo",
back: "Atrás",
forward: "Adelante",
controls: "Controles",
previousPosition: "Posición anterior",
nextPosition: "Siguiente posición",
backButton: "Botón atrás",
forwardButton: "Botón adelante",
hints: "Consejos",
historyLimit: "El historial almacena hasta 100 posiciones",
autoSave: "Las nuevas posiciones se guardan automáticamente al mover el mapa",
history: "Historial",
clearHistory: "Limpiar Historial",
noHistory: "Sin historial disponible",
currentPosition: "Posición Actual",
markPosition: "Marcar Posición",
unmarkPosition: "Desmarcar Posición",
markedPositions: "Posiciones Marcadas",
clickToNavigate: "Haz clic en la entrada del historial para saltar directamente",
autoSaveDescription: "Guardado automático y carga en el próximo inicio",
confirmClearHistory: "¿Estás seguro de que quieres eliminar todo el historial?",
locationName: "Ubicación",
zoomLevel: "Nivel de Zoom"
},
fr: {
scriptActive: "Script actif",
back: "Retour",
forward: "Avancer",
controls: "Contrôles",
previousPosition: "Position précédente",
nextPosition: "Position suivante",
backButton: "Bouton retour",
forwardButton: "Bouton avancer",
hints: "Conseils",
historyLimit: "L'historique stocke jusqu'à 100 positions",
autoSave: "Les nouvelles positions sont automatiquement sauvegardées lors du déplacement de la carte",
history: "Historique",
clearHistory: "Effacer l'Historique",
noHistory: "Aucun historique disponible",
currentPosition: "Position Actuelle",
markPosition: "Marquer la Position",
unmarkPosition: "Démarquer la Position",
markedPositions: "Positions Marquées",
clickToNavigate: "Cliquez sur l'entrée d'historique pour un saut direct",
autoSaveDescription: "Sauvegarde automatique et chargement au prochain démarrage",
confirmClearHistory: "Êtes-vous sûr de vouloir supprimer tout l'historique?",
locationName: "Emplacement",
zoomLevel: "Niveau de Zoom"
},
it: {
scriptActive: "Script attivo",
back: "Indietro",
forward: "Avanti",
controls: "Controlli",
previousPosition: "Posizione precedente",
nextPosition: "Posizione successiva",
backButton: "Pulsante indietro",
forwardButton: "Pulsante avanti",
hints: "Suggerimenti",
historyLimit: "La cronologia memorizza fino a 100 posizioni",
autoSave: "Le nuove posizioni vengono salvate automaticamente quando si sposta la mappa",
history: "Cronologia",
clearHistory: "Cancella Cronologia",
noHistory: "Nessuna cronologia disponibile",
currentPosition: "Posizione Attuale",
markPosition: "Marca Posizione",
unmarkPosition: "Rimuovi Marcatura",
markedPositions: "Posizioni Marcate",
clickToNavigate: "Clicca sulla voce della cronologia per il salto diretto",
autoSaveDescription: "Salvataggio automatico e caricamento al prossimo avvio",
confirmClearHistory: "Sei sicuro di voler eliminare l'intera cronologia?",
locationName: "Posizione",
zoomLevel: "Livello di Zoom"
}
};
const t = (key) => {
return (translations[currentLanguage] && translations[currentLanguage][key]) || translations.en[key];
};
// Hilfsfunktion um Koordinaten zu formatieren
function formatCoords(lat, lon) {
return `${lat.toFixed(5)}, ${lon.toFixed(5)}`;
}
// Hilfsfunktion um Zeitstempel zu formatieren
function formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString();
}
// Hilfsfunktion um Ortsname zu generieren (vereinfacht)
function generateLocationName(lat, lon, zoom) {
return `${t('zoomLevel')} ${zoom} - ${formatCoords(lat, lon)}`;
}
// Markierte Position hinzufügen/entfernen
function togglePositionHighlight(index) {
const position = navigationHistory[index];
if (!position) return;
if (highlightedPositions[index]) {
// Hervorhebung entfernen
delete highlightedPositions[index];
position.marked = false;
} else {
// Hervorhebung hinzufügen
highlightedPositions[index] = true;
position.marked = true;
}
updateHistoryList();
saveToLocalStorage();
}
function saveCurrentPosition() {
if (!isInitialized || !W.map) return;
const now = Date.now();
// Mindestens 2 Sekunden zwischen den Speicherungen
if (now - lastSaveTime < 2000) return;
const center = W.map.getCenter();
const zoom = W.map.getZoom();
const lastEntry = navigationHistory[currentIndex];
if (lastEntry &&
Math.abs(lastEntry.lat - center.lat) < 0.00001 &&
Math.abs(lastEntry.lon - center.lon) < 0.00001 &&
lastEntry.zoom === zoom) {
return;
}
// Entferne alle Einträge nach dem aktuellen Index (für Verzweigungen)
navigationHistory = navigationHistory.slice(0, currentIndex + 1);
navigationHistory.push({
lat: center.lat,
lon: center.lon,
zoom: zoom,
timestamp: now,
name: generateLocationName(center.lat, center.lon, zoom),
marked: false
});
currentIndex++;
lastSaveTime = now;
// Begrenze auf 100 Einträge
if (navigationHistory.length > 100) {
const removedEntry = navigationHistory.shift();
currentIndex--;
// Entferne Hervorhebung des gelöschten Eintrags
if (removedEntry.marked && highlightedPositions[0]) {
delete highlightedPositions[0];
}
// Aktualisiere Hervorhebungs-Indizes
const newHighlightedPositions = {};
Object.keys(highlightedPositions).forEach(oldIndex => {
const newIndex = parseInt(oldIndex) - 1;
if (newIndex >= 0) {
newHighlightedPositions[newIndex] = highlightedPositions[oldIndex];
}
});
highlightedPositions = newHighlightedPositions;
}
updateNavigationButtons();
updateHistoryList();
saveToLocalStorage();
}
function navigateToPosition(position, index) {
if (!position || !isInitialized || !W.map) return;
try {
const lonlat = new OpenLayers.LonLat(position.lon, position.lat);
W.map.setCenter(lonlat, position.zoom);
if (typeof index !== 'undefined') {
currentIndex = index;
updateNavigationButtons();
updateHistoryList();
}
} catch (error) {
console.error('WME Map Nav History: Navigation error:', error);
}
}
function handleKeyDown(e) {
if (!isInitialized) return;
if (e.altKey && e.keyCode === 37) { // Alt + Left
navigateBack();
e.preventDefault();
}
else if (e.altKey && e.keyCode === 39) { // Alt + Right
navigateForward();
e.preventDefault();
}
}
function navigateBack() {
if (currentIndex > 0) {
currentIndex--;
navigateToPosition(navigationHistory[currentIndex]);
updateNavigationButtons();
updateHistoryList();
}
}
function navigateForward() {
if (currentIndex < navigationHistory.length - 1) {
currentIndex++;
navigateToPosition(navigationHistory[currentIndex]);
updateNavigationButtons();
updateHistoryList();
}
}
function updateNavigationButtons() {
const backBtn = document.getElementById('nav-history-back');
const forwardBtn = document.getElementById('nav-history-forward');
if (backBtn) {
backBtn.disabled = currentIndex <= 0;
backBtn.style.opacity = currentIndex <= 0 ? '0.5' : '1';
}
if (forwardBtn) {
forwardBtn.disabled = currentIndex >= navigationHistory.length - 1;
forwardBtn.style.opacity = currentIndex >= navigationHistory.length - 1 ? '0.5' : '1';
}
}
function updateHistoryList() {
const historyContainer = document.getElementById('nav-history-list');
if (!historyContainer) return;
if (navigationHistory.length === 0) {
historyContainer.innerHTML = `<div class="no-history">${t('noHistory')}</div>`;
return;
}
let html = '';
navigationHistory.forEach((entry, index) => {
const isCurrent = index === currentIndex;
const isMarked = entry.marked || false;
const time = formatTime(entry.timestamp);
html += `
<div class="history-item ${isCurrent ? 'current' : ''} ${isMarked ? 'marked' : ''}" data-index="${index}">
<div class="history-item-header">
<span class="history-item-time">${time}</span>
<div class="history-item-controls">
${isCurrent ? `<span class="current-marker">${t('currentPosition')}</span>` : ''}
<button class="mark-button ${isMarked ? 'marked' : ''}" data-index="${index}" title="${isMarked ? t('unmarkPosition') : t('markPosition')}">
${isMarked ? '★' : '☆'}
</button>
</div>
</div>
<div class="history-item-location">
${entry.name}
</div>
<div class="history-item-coords">
${formatCoords(entry.lat, entry.lon)}
</div>
</div>
`;
});
historyContainer.innerHTML = html;
// Event Listener für Klicks auf Historie-Einträge
historyContainer.querySelectorAll('.history-item').forEach(item => {
item.addEventListener('click', function(e) {
if (e.target.classList.contains('mark-button')) {
return; // Lass den Button-Handler das übernehmen
}
const index = parseInt(this.dataset.index);
navigateToPosition(navigationHistory[index], index);
});
});
// Event Listener für Marker-Buttons
historyContainer.querySelectorAll('.mark-button').forEach(button => {
button.addEventListener('click', function(e) {
e.stopPropagation();
const index = parseInt(this.dataset.index);
togglePositionHighlight(index);
});
});
}
function clearHistory() {
// Alle Hervorhebungen entfernen
highlightedPositions = {};
navigationHistory = [];
currentIndex = -1;
updateNavigationButtons();
updateHistoryList();
saveToLocalStorage();
}
function saveToLocalStorage() {
try {
const data = {
history: navigationHistory,
currentIndex: currentIndex,
highlightedPositions: highlightedPositions
};
localStorage.setItem('wme-nav-history', JSON.stringify(data));
} catch (error) {
console.error('WME Map Nav History: Error saving to localStorage:', error);
}
}
function loadFromLocalStorage() {
try {
const data = localStorage.getItem('wme-nav-history');
if (data) {
const parsed = JSON.parse(data);
navigationHistory = parsed.history || [];
currentIndex = parsed.currentIndex || -1;
highlightedPositions = parsed.highlightedPositions || {};
// Validiere den currentIndex
if (currentIndex >= navigationHistory.length) {
currentIndex = navigationHistory.length - 1;
}
}
} catch (error) {
console.error('WME Map Nav History: Error loading from localStorage:', error);
navigationHistory = [];
currentIndex = -1;
highlightedPositions = {};
}
}
async function createSidebarTab() {
try {
const { tabLabel, tabPane } = W.userscripts.registerSidebarTab("wme-nav-history");
tabLabel.innerText = 'NAV';
tabLabel.title = '<h4>Map Navigation History</h4>';
await W.userscripts.waitForElementConnected(tabPane);
const styleSheet = document.createElement("style");
styleSheet.textContent = `
.nav-history-button {
padding: 8px 15px;
cursor: pointer;
background: #4a89dc;
color: white;
border: none;
border-radius: 4px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
min-width: 100px;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.nav-history-button:hover {
background: #5d9cec;
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
transform: translateY(-1px);
}
.nav-history-button:active {
transform: translateY(1px);
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.nav-history-button:disabled {
background: #b5b5b5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.nav-history-container {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin: 10px 0;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
}
.clear-history-button {
background: #dc3545;
padding: 6px 12px;
font-size: 12px;
min-width: auto;
}
.clear-history-button:hover {
background: #c82333;
}
.history-section {
margin-top: 20px;
background: #fff;
border-radius: 6px;
border: 1px solid #e1e4e8;
}
.history-header {
padding: 15px;
border-bottom: 1px solid #e1e4e8;
display: flex;
justify-content: space-between;
align-items: center;
background: #f8f9fa;
border-radius: 6px 6px 0 0;
}
.history-controls {
display: flex;
gap: 10px;
align-items: center;
}
.history-list {
max-height: 300px;
overflow-y: auto;
}
.history-item {
padding: 12px 15px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.2s ease;
position: relative;
}
.history-item:hover {
background-color: #f8f9fa;
}
.history-item.current {
background-color: #e8f4fd;
border-left: 4px solid #4a89dc;
}
.history-item.marked {
border-left: 4px solid #ffd700;
background-color: #fffbf0;
}
.history-item.current.marked {
border-left: 4px solid #4a89dc;
background: linear-gradient(to right, #e8f4fd, #fffbf0);
}
.history-item:last-child {
border-bottom: none;
}
.history-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.history-item-controls {
display: flex;
align-items: center;
gap: 8px;
}
.history-item-time {
font-size: 12px;
color: #666;
font-weight: 600;
}
.current-marker {
font-size: 10px;
background: #4a89dc;
color: white;
padding: 2px 6px;
border-radius: 10px;
font-weight: 600;
}
.mark-button {
background: none;
border: none;
font-size: 16px;
cursor: pointer;
padding: 2px 4px;
border-radius: 3px;
transition: all 0.2s ease;
color: #ccc;
}
.mark-button:hover {
background: rgba(0,0,0,0.1);
color: #ffd700;
}
.mark-button.marked {
color: #ffd700;
}
.history-item-location {
font-weight: 600;
color: #2c3e50;
margin-bottom: 3px;
font-size: 13px;
}
.history-item-coords {
font-size: 11px;
color: #7f8c8d;
font-family: monospace;
}
.no-history {
padding: 20px;
text-align: center;
color: #666;
font-style: italic;
}
`;
document.head.appendChild(styleSheet);
tabPane.innerHTML = `
<div class="nav-history-container">
<p style="margin-top: 0; color: #2c3e50;"><h4>WME Map Nav History</h4></p>
<p style="color: #4CAF50; display: flex; align-items: center; gap: 5px;">
<span style="font-size: 18px;">✓</span> ${t('scriptActive')}
</p>
<div style="display: flex; gap: 15px; margin: 20px 0;">
<button id="nav-history-back" class="nav-history-button">
<span style="font-size: 16px;">⬅️</span> ${t('back')}
</button>
<button id="nav-history-forward" class="nav-history-button">
${t('forward')} <span style="font-size: 16px;">➡️</span>
</button>
</div>
<div class="history-section">
<div class="history-header">
<strong>${t('history')}</strong>
<div class="history-controls">
<button id="clear-history" class="nav-history-button clear-history-button">
${t('clearHistory')}
</button>
</div>
</div>
<div id="nav-history-list" class="history-list">
<div class="no-history">${t('noHistory')}</div>
</div>
</div>
<hr style="border: none; height: 1px; background: #e1e4e8; margin: 15px 0;">
<div style="background: #fff; padding: 15px; border-radius: 6px; border: 1px solid #e1e4e8;">
<b style="margin-top: 0; color: #2c3e50;">${t('controls')}:</b>
<ul style="padding-left: 20px; color: #4a5568;">
<li><strong>Alt + ←</strong> ${t('previousPosition')}</li>
<li><strong>Alt + →</strong> ${t('nextPosition')}</li>
<li><strong>Click</strong> auf Historie-Eintrag für direkten Sprung</li>
<li><strong>★ Button</strong> ${t('markPosition')}</li>
</ul>
<b style="margin-top: 15px; color: #2c3e50;">${t('hints')}:</b>
<ul style="padding-left: 20px; color: #4a5568;">
<li>${t('historyLimit')}</li>
<li>${t('autoSave')}</li>
<li>${t('autoSaveDescription')}</li>
<li>Markierte Positionen werden in der Liste hervorgehoben</li>
<li>Hervorhebungen bleiben auch nach Neustart erhalten</li>
</ul>
</div>
</div>
`;
const backBtn = tabPane.querySelector('#nav-history-back');
const forwardBtn = tabPane.querySelector('#nav-history-forward');
const clearBtn = tabPane.querySelector('#clear-history');
backBtn.addEventListener('click', navigateBack);
forwardBtn.addEventListener('click', navigateForward);
clearBtn.addEventListener('click', () => {
if (confirm(t('confirmClearHistory'))) {
clearHistory();
}
});
updateNavigationButtons();
updateHistoryList();
return true;
} catch (error) {
console.error('WME Map Nav History: Error creating sidebar tab:', error);
return false;
}
}
function initializeScript() {
if (isInitialized) return;
try {
// Lade gespeicherten Verlauf
loadFromLocalStorage();
W.map.events.register('moveend', null, saveCurrentPosition);
document.addEventListener('keydown', handleKeyDown);
createSidebarTab();
isInitialized = true;
saveCurrentPosition();
console.log('WME Map Nav History Extended: Successfully initialized');
} catch (error) {
console.error('WME Map Nav History: Initialization error:', error);
isInitialized = false;
}
}
if (W?.userscripts?.state.isInitialized) {
initializeScript();
} else {
document.addEventListener("wme-initialized", initializeScript, {
once: true,
});
}
})();