WME Mapillary Viewer

Zeigt Mapillary Viewer, Traffic Signs, Map Features und Street-Level Imagery im Waze Map Editor

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         WME Mapillary Viewer
// @namespace    https://greasyfork.org/de/users/863740-horst-wittlich
// @version      2026.01.12
// @author       Hiwi234
// @description  Zeigt Mapillary Viewer, Traffic Signs, Map Features und Street-Level Imagery im Waze Map Editor
// @match        https://www.waze.com/editor*
// @match        https://www.waze.com/*/editor*
// @match        https://beta.waze.com/editor*
// @match        https://beta.waze.com/*/editor*
// @exclude      https://www.waze.com/user/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @connect      graph.mapillary.com
// @connect      www.mapillary.com
// @connect      thumbnails.mapillary.com
// @connect      *
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

/* global W, OpenLayers, GM_xmlhttpRequest, GM_addStyle, GM_setClipboard */

(function () {
    'use strict';

    if (window._wmeMapillaryV8) return;
    window._wmeMapillaryV8 = true;

    const SCRIPT_ID = 'wme-mapillary-v8';
    const SCRIPT_NAME = 'Mapillary';
    const SCRIPT_VERSION = '2026.01.11.12';
    const ACCESS_TOKEN = 'MLY|9425779667550855|8ed93f7c998fd15d39e3a693638e2ac8';

    // ============ I18N ============
    const I18N = {
        de: {
            // UI Labels
            enabled: 'Aktiviert',
            display: 'Anzeige',
            images: 'Bilder',
            panoramas: '360° Panoramas',
            trafficSigns: 'Verkehrsschilder',
            objects: 'Objekte (Ampeln, etc.)',
            sequenceLines: 'Sequenz-Linien',
            signFilters: 'Schilder-Filter',
            speed: 'Geschwindigkeit',
            stopYield: 'Stop/Vorfahrt',
            turnDirection: 'Abbiegen/Richtung',
            prohibitions: 'Verbote',
            parking: 'Parken',
            pedestrianCyclist: 'Fußgänger/Radfahrer',
            warningSigns: 'Warnschilder',
            infoSigns: 'Hinweisschilder',
            all: 'Alle',
            none: 'Keine',
            appearance: 'Darstellung',
            opacity: 'Deckkraft',
            performance: 'Performance',
            maxImages: 'Max. Bilder',
            maxSigns: 'Max. Schilder',
            fast: 'Schnell',
            complete: 'Vollständig',
            dateFilter: 'Datumsfilter',
            filterByDate: 'Nach Datum filtern',
            from: 'Von',
            to: 'Bis',
            noLimit: 'Leer lassen = kein Limit',
            refresh: 'Daten neu laden',
            clearCache: 'Cache leeren',
            shortcuts: 'Tastenkürzel',
            shortcutToggle: 'M = Ein/Aus',
            shortcutClose: 'ESC = Viewer schließen',
            // Status
            ready: 'Bereit',
            loading: 'Lade...',
            disabled: 'Deaktiviert',
            zoomIn: 'Zoom {zoom}+ für Daten',
            zoomMore: 'Weiter reinzoomen',
            cacheCleared: 'Cache geleert',
            linkCopied: 'Link kopiert!',
            noImageFound: 'Kein Bild gefunden',
            // Viewer
            viewer: 'Mapillary Viewer',
            date: 'Datum',
            creator: 'Ersteller',
            camera: 'Kamera',
            resolution: 'Auflösung',
            firstSeen: 'Zuerst',
            lastSeen: 'Zuletzt',
            // Stats
            statsImages: 'Bilder',
            statsSigns: 'Schilder',
            statsObjects: 'Objekte',
            sequenceLoaded: 'Sequenz: {count} Bilder',
            prevImage: 'Vorheriges Bild',
            nextImage: 'Nächstes Bild'
        },
        en: {
            enabled: 'Enabled',
            display: 'Display',
            images: 'Images',
            panoramas: '360° Panoramas',
            trafficSigns: 'Traffic Signs',
            objects: 'Objects (Traffic lights, etc.)',
            sequenceLines: 'Sequence Lines',
            signFilters: 'Sign Filters',
            speed: 'Speed',
            stopYield: 'Stop/Yield',
            turnDirection: 'Turn/Direction',
            prohibitions: 'Prohibitions',
            parking: 'Parking',
            pedestrianCyclist: 'Pedestrian/Cyclist',
            warningSigns: 'Warning Signs',
            infoSigns: 'Info Signs',
            all: 'All',
            none: 'None',
            appearance: 'Appearance',
            opacity: 'Opacity',
            performance: 'Performance',
            maxImages: 'Max. Images',
            maxSigns: 'Max. Signs',
            fast: 'Fast',
            complete: 'Complete',
            dateFilter: 'Date Filter',
            filterByDate: 'Filter by date',
            from: 'From',
            to: 'To',
            noLimit: 'Leave empty = no limit',
            refresh: 'Reload data',
            clearCache: 'Clear cache',
            shortcuts: 'Shortcuts',
            shortcutToggle: 'M = Toggle',
            shortcutClose: 'ESC = Close viewer',
            ready: 'Ready',
            loading: 'Loading...',
            disabled: 'Disabled',
            zoomIn: 'Zoom {zoom}+ for data',
            zoomMore: 'Zoom in more',
            cacheCleared: 'Cache cleared',
            linkCopied: 'Link copied!',
            noImageFound: 'No image found',
            viewer: 'Mapillary Viewer',
            date: 'Date',
            creator: 'Creator',
            camera: 'Camera',
            resolution: 'Resolution',
            firstSeen: 'First seen',
            lastSeen: 'Last seen',
            statsImages: 'Images',
            statsSigns: 'Signs',
            statsObjects: 'Objects',
            sequenceLoaded: 'Sequence: {count} images',
            prevImage: 'Previous image',
            nextImage: 'Next image'
        },
        fr: {
            enabled: 'Activé',
            display: 'Affichage',
            images: 'Images',
            panoramas: 'Panoramas 360°',
            trafficSigns: 'Panneaux de signalisation',
            objects: 'Objets (Feux, etc.)',
            sequenceLines: 'Lignes de séquence',
            signFilters: 'Filtres de panneaux',
            speed: 'Vitesse',
            stopYield: 'Stop/Cédez',
            turnDirection: 'Tourner/Direction',
            prohibitions: 'Interdictions',
            parking: 'Stationnement',
            pedestrianCyclist: 'Piétons/Cyclistes',
            warningSigns: 'Panneaux d\'avertissement',
            infoSigns: 'Panneaux d\'information',
            all: 'Tous',
            none: 'Aucun',
            appearance: 'Apparence',
            opacity: 'Opacité',
            performance: 'Performance',
            maxImages: 'Max. Images',
            maxSigns: 'Max. Panneaux',
            fast: 'Rapide',
            complete: 'Complet',
            dateFilter: 'Filtre de date',
            filterByDate: 'Filtrer par date',
            from: 'De',
            to: 'À',
            noLimit: 'Vide = pas de limite',
            refresh: 'Recharger les données',
            clearCache: 'Vider le cache',
            shortcuts: 'Raccourcis',
            shortcutToggle: 'M = Activer/Désactiver',
            shortcutClose: 'ESC = Fermer la visionneuse',
            ready: 'Prêt',
            loading: 'Chargement...',
            disabled: 'Désactivé',
            zoomIn: 'Zoom {zoom}+ pour les données',
            zoomMore: 'Zoomer plus',
            cacheCleared: 'Cache vidé',
            linkCopied: 'Lien copié!',
            noImageFound: 'Aucune image trouvée',
            viewer: 'Visionneuse Mapillary',
            date: 'Date',
            creator: 'Créateur',
            camera: 'Caméra',
            resolution: 'Résolution',
            firstSeen: 'Première fois',
            lastSeen: 'Dernière fois',
            statsImages: 'Images',
            statsSigns: 'Panneaux',
            statsObjects: 'Objets',
            sequenceLoaded: 'Séquence: {count} images',
            prevImage: 'Image précédente',
            nextImage: 'Image suivante'
        },
        es: {
            enabled: 'Activado',
            display: 'Visualización',
            images: 'Imágenes',
            panoramas: 'Panoramas 360°',
            trafficSigns: 'Señales de tráfico',
            objects: 'Objetos (Semáforos, etc.)',
            sequenceLines: 'Líneas de secuencia',
            signFilters: 'Filtros de señales',
            speed: 'Velocidad',
            stopYield: 'Stop/Ceda',
            turnDirection: 'Giro/Dirección',
            prohibitions: 'Prohibiciones',
            parking: 'Aparcamiento',
            pedestrianCyclist: 'Peatones/Ciclistas',
            warningSigns: 'Señales de advertencia',
            infoSigns: 'Señales informativas',
            all: 'Todos',
            none: 'Ninguno',
            appearance: 'Apariencia',
            opacity: 'Opacidad',
            performance: 'Rendimiento',
            maxImages: 'Máx. Imágenes',
            maxSigns: 'Máx. Señales',
            fast: 'Rápido',
            complete: 'Completo',
            dateFilter: 'Filtro de fecha',
            filterByDate: 'Filtrar por fecha',
            from: 'Desde',
            to: 'Hasta',
            noLimit: 'Vacío = sin límite',
            refresh: 'Recargar datos',
            clearCache: 'Limpiar caché',
            shortcuts: 'Atajos',
            shortcutToggle: 'M = Activar/Desactivar',
            shortcutClose: 'ESC = Cerrar visor',
            ready: 'Listo',
            loading: 'Cargando...',
            disabled: 'Desactivado',
            zoomIn: 'Zoom {zoom}+ para datos',
            zoomMore: 'Acercar más',
            cacheCleared: 'Caché limpiado',
            linkCopied: '¡Enlace copiado!',
            noImageFound: 'No se encontró imagen',
            viewer: 'Visor Mapillary',
            date: 'Fecha',
            creator: 'Creador',
            camera: 'Cámara',
            resolution: 'Resolución',
            firstSeen: 'Primera vez',
            lastSeen: 'Última vez',
            statsImages: 'Imágenes',
            statsSigns: 'Señales',
            statsObjects: 'Objetos',
            sequenceLoaded: 'Secuencia: {count} imágenes',
            prevImage: 'Imagen anterior',
            nextImage: 'Imagen siguiente'
        },
        it: {
            enabled: 'Attivato',
            display: 'Visualizzazione',
            images: 'Immagini',
            panoramas: 'Panorami 360°',
            trafficSigns: 'Segnali stradali',
            objects: 'Oggetti (Semafori, ecc.)',
            sequenceLines: 'Linee di sequenza',
            signFilters: 'Filtri segnali',
            speed: 'Velocità',
            stopYield: 'Stop/Precedenza',
            turnDirection: 'Svolta/Direzione',
            prohibitions: 'Divieti',
            parking: 'Parcheggio',
            pedestrianCyclist: 'Pedoni/Ciclisti',
            warningSigns: 'Segnali di avvertimento',
            infoSigns: 'Segnali informativi',
            all: 'Tutti',
            none: 'Nessuno',
            appearance: 'Aspetto',
            opacity: 'Opacità',
            performance: 'Prestazioni',
            maxImages: 'Max. Immagini',
            maxSigns: 'Max. Segnali',
            fast: 'Veloce',
            complete: 'Completo',
            dateFilter: 'Filtro data',
            filterByDate: 'Filtra per data',
            from: 'Da',
            to: 'A',
            noLimit: 'Vuoto = nessun limite',
            refresh: 'Ricarica dati',
            clearCache: 'Svuota cache',
            shortcuts: 'Scorciatoie',
            shortcutToggle: 'M = Attiva/Disattiva',
            shortcutClose: 'ESC = Chiudi visualizzatore',
            ready: 'Pronto',
            loading: 'Caricamento...',
            disabled: 'Disattivato',
            zoomIn: 'Zoom {zoom}+ per i dati',
            zoomMore: 'Ingrandisci di più',
            cacheCleared: 'Cache svuotata',
            linkCopied: 'Link copiato!',
            noImageFound: 'Nessuna immagine trovata',
            viewer: 'Visualizzatore Mapillary',
            date: 'Data',
            creator: 'Creatore',
            camera: 'Fotocamera',
            resolution: 'Risoluzione',
            firstSeen: 'Prima volta',
            lastSeen: 'Ultima volta',
            statsImages: 'Immagini',
            statsSigns: 'Segnali',
            statsObjects: 'Oggetti',
            sequenceLoaded: 'Sequenza: {count} immagini',
            prevImage: 'Immagine precedente',
            nextImage: 'Immagine successiva'
        },
        nl: {
            enabled: 'Ingeschakeld',
            display: 'Weergave',
            images: 'Afbeeldingen',
            panoramas: '360° Panorama\'s',
            trafficSigns: 'Verkeersborden',
            objects: 'Objecten (Verkeerslichten, etc.)',
            sequenceLines: 'Sequentielijnen',
            signFilters: 'Bordenfilters',
            speed: 'Snelheid',
            stopYield: 'Stop/Voorrang',
            turnDirection: 'Afslaan/Richting',
            prohibitions: 'Verboden',
            parking: 'Parkeren',
            pedestrianCyclist: 'Voetgangers/Fietsers',
            warningSigns: 'Waarschuwingsborden',
            infoSigns: 'Informatieborden',
            all: 'Alle',
            none: 'Geen',
            appearance: 'Uiterlijk',
            opacity: 'Dekking',
            performance: 'Prestaties',
            maxImages: 'Max. Afbeeldingen',
            maxSigns: 'Max. Borden',
            fast: 'Snel',
            complete: 'Volledig',
            dateFilter: 'Datumfilter',
            filterByDate: 'Filteren op datum',
            from: 'Van',
            to: 'Tot',
            noLimit: 'Leeg = geen limiet',
            refresh: 'Gegevens herladen',
            clearCache: 'Cache wissen',
            shortcuts: 'Sneltoetsen',
            shortcutToggle: 'M = Aan/Uit',
            shortcutClose: 'ESC = Viewer sluiten',
            ready: 'Gereed',
            loading: 'Laden...',
            disabled: 'Uitgeschakeld',
            zoomIn: 'Zoom {zoom}+ voor gegevens',
            zoomMore: 'Verder inzoomen',
            cacheCleared: 'Cache gewist',
            linkCopied: 'Link gekopieerd!',
            noImageFound: 'Geen afbeelding gevonden',
            viewer: 'Mapillary Viewer',
            date: 'Datum',
            creator: 'Maker',
            camera: 'Camera',
            resolution: 'Resolutie',
            firstSeen: 'Eerste keer',
            lastSeen: 'Laatste keer',
            statsImages: 'Afbeeldingen',
            statsSigns: 'Borden',
            statsObjects: 'Objecten',
            sequenceLoaded: 'Sequentie: {count} afbeeldingen',
            prevImage: 'Vorige afbeelding',
            nextImage: 'Volgende afbeelding'
        },
        pl: {
            enabled: 'Włączony',
            display: 'Wyświetlanie',
            images: 'Zdjęcia',
            panoramas: 'Panoramy 360°',
            trafficSigns: 'Znaki drogowe',
            objects: 'Obiekty (Sygnalizacja, itp.)',
            sequenceLines: 'Linie sekwencji',
            signFilters: 'Filtry znaków',
            speed: 'Prędkość',
            stopYield: 'Stop/Ustąp',
            turnDirection: 'Skręt/Kierunek',
            prohibitions: 'Zakazy',
            parking: 'Parkowanie',
            pedestrianCyclist: 'Piesi/Rowerzyści',
            warningSigns: 'Znaki ostrzegawcze',
            infoSigns: 'Znaki informacyjne',
            all: 'Wszystkie',
            none: 'Żadne',
            appearance: 'Wygląd',
            opacity: 'Przezroczystość',
            performance: 'Wydajność',
            maxImages: 'Maks. Zdjęć',
            maxSigns: 'Maks. Znaków',
            fast: 'Szybko',
            complete: 'Kompletnie',
            dateFilter: 'Filtr daty',
            filterByDate: 'Filtruj po dacie',
            from: 'Od',
            to: 'Do',
            noLimit: 'Puste = bez limitu',
            refresh: 'Odśwież dane',
            clearCache: 'Wyczyść cache',
            shortcuts: 'Skróty',
            shortcutToggle: 'M = Włącz/Wyłącz',
            shortcutClose: 'ESC = Zamknij przeglądarkę',
            ready: 'Gotowy',
            loading: 'Ładowanie...',
            disabled: 'Wyłączony',
            zoomIn: 'Zoom {zoom}+ dla danych',
            zoomMore: 'Przybliż więcej',
            cacheCleared: 'Cache wyczyszczony',
            linkCopied: 'Link skopiowany!',
            noImageFound: 'Nie znaleziono zdjęcia',
            viewer: 'Przeglądarka Mapillary',
            date: 'Data',
            creator: 'Twórca',
            camera: 'Aparat',
            resolution: 'Rozdzielczość',
            firstSeen: 'Pierwszy raz',
            lastSeen: 'Ostatni raz',
            statsImages: 'Zdjęcia',
            statsSigns: 'Znaki',
            statsObjects: 'Obiekty',
            sequenceLoaded: 'Sekwencja: {count} zdjęć',
            prevImage: 'Poprzednie zdjęcie',
            nextImage: 'Następne zdjęcie'
        },
        pt: {
            enabled: 'Ativado',
            display: 'Exibição',
            images: 'Imagens',
            panoramas: 'Panoramas 360°',
            trafficSigns: 'Sinais de trânsito',
            objects: 'Objetos (Semáforos, etc.)',
            sequenceLines: 'Linhas de sequência',
            signFilters: 'Filtros de sinais',
            speed: 'Velocidade',
            stopYield: 'Pare/Dê preferência',
            turnDirection: 'Curva/Direção',
            prohibitions: 'Proibições',
            parking: 'Estacionamento',
            pedestrianCyclist: 'Pedestres/Ciclistas',
            warningSigns: 'Sinais de advertência',
            infoSigns: 'Sinais informativos',
            all: 'Todos',
            none: 'Nenhum',
            appearance: 'Aparência',
            opacity: 'Opacidade',
            performance: 'Desempenho',
            maxImages: 'Máx. Imagens',
            maxSigns: 'Máx. Sinais',
            fast: 'Rápido',
            complete: 'Completo',
            dateFilter: 'Filtro de data',
            filterByDate: 'Filtrar por data',
            from: 'De',
            to: 'Até',
            noLimit: 'Vazio = sem limite',
            refresh: 'Recarregar dados',
            clearCache: 'Limpar cache',
            shortcuts: 'Atalhos',
            shortcutToggle: 'M = Ativar/Desativar',
            shortcutClose: 'ESC = Fechar visualizador',
            ready: 'Pronto',
            loading: 'Carregando...',
            disabled: 'Desativado',
            zoomIn: 'Zoom {zoom}+ para dados',
            zoomMore: 'Aproximar mais',
            cacheCleared: 'Cache limpo',
            linkCopied: 'Link copiado!',
            noImageFound: 'Nenhuma imagem encontrada',
            viewer: 'Visualizador Mapillary',
            date: 'Data',
            creator: 'Criador',
            camera: 'Câmera',
            resolution: 'Resolução',
            firstSeen: 'Primeira vez',
            lastSeen: 'Última vez',
            statsImages: 'Imagens',
            statsSigns: 'Sinais',
            statsObjects: 'Objetos',
            sequenceLoaded: 'Sequência: {count} imagens',
            prevImage: 'Imagem anterior',
            nextImage: 'Próxima imagem'
        }
    };

    let currentLang = 'en';

    function detectLanguage() {
        try {
            if (window.I18n?.currentLocale) {
                const locale = window.I18n.currentLocale();
                const lang = locale.split('-')[0].toLowerCase();
                if (I18N[lang]) return lang;
            }
            const browserLang = navigator.language?.split('-')[0].toLowerCase();
            if (I18N[browserLang]) return browserLang;
        } catch (e) {}
        return 'en';
    }

    function t(key, params = {}) {
        let text = I18N[currentLang]?.[key] || I18N.en[key] || key;
        for (const [k, v] of Object.entries(params)) {
            text = text.replace(`{${k}}`, v);
        }
        return text;
    }

    // Schilder-Kategorien für Filter (mit I18N)
    function getSignFilterCategories() {
        return {
            speed: { name: t('speed'), pattern: 'regulatory--speed-limit*,regulatory--maximum-speed*,warning--speed*', icon: '🚗' },
            stop: { name: t('stopYield'), pattern: 'regulatory--stop*,regulatory--yield*,regulatory--give-way*', icon: '🛑' },
            turn: { name: t('turnDirection'), pattern: 'regulatory--turn*,regulatory--go-straight*,regulatory--one-way*,regulatory--keep*', icon: '↩️' },
            noEntry: { name: t('prohibitions'), pattern: 'regulatory--no-entry*,regulatory--no-*', icon: '⛔' },
            parking: { name: t('parking'), pattern: 'regulatory--parking*,regulatory--no-parking*,information--parking*', icon: '🅿️' },
            pedestrian: { name: t('pedestrianCyclist'), pattern: 'warning--pedestrians*,warning--cyclists*,regulatory--pedestrians*,regulatory--bicycles*', icon: '🚶' },
            warning: { name: t('warningSigns'), pattern: 'warning--*', icon: '⚠️' },
            info: { name: t('infoSigns'), pattern: 'information--*', icon: 'ℹ️' }
        };
    }

    // Legacy constant for pattern matching (without I18N names)
    const SIGN_FILTER_CATEGORIES = {
        speed: { pattern: 'regulatory--speed-limit*,regulatory--maximum-speed*,warning--speed*', icon: '🚗' },
        stop: { pattern: 'regulatory--stop*,regulatory--yield*,regulatory--give-way*', icon: '🛑' },
        turn: { pattern: 'regulatory--turn*,regulatory--go-straight*,regulatory--one-way*,regulatory--keep*', icon: '↩️' },
        noEntry: { pattern: 'regulatory--no-entry*,regulatory--no-*', icon: '⛔' },
        parking: { pattern: 'regulatory--parking*,regulatory--no-parking*,information--parking*', icon: '🅿️' },
        pedestrian: { pattern: 'warning--pedestrians*,warning--cyclists*,regulatory--pedestrians*,regulatory--bicycles*', icon: '🚶' },
        warning: { pattern: 'warning--*', icon: '⚠️' },
        info: { pattern: 'information--*', icon: 'ℹ️' }
    };

    let CONFIG = {
        enabled: true,
        opacity: 0.85,
        showImages: true,
        showPanos: true,
        showSigns: true,
        showFeatures: false,
        imageColor: '#05CB63',
        panoColor: '#9B59B6',
        signColor: '#E74C3C',
        featureColor: '#3498DB',
        filterByDate: false,
        dateFrom: '',
        dateTo: '',
        minZoom: 16,
        clusterFeatures: true,
        showSequenceLines: false,
        maxImages: 1000,
        maxSigns: 500,
        // Schilder-Filter (alle aktiv = alle anzeigen)
        signFilters: {
            speed: true,
            stop: true,
            turn: true,
            noEntry: true,
            parking: true,
            pedestrian: true,
            warning: true,
            info: true
        }
    };

    const STORAGE_KEY = 'wme_mapillary_v8';
    let imageLayer = null;
    let signLayer = null;
    let featureLayer = null;
    let sequenceLayer = null;
    let viewerDiv = null;
    let refreshTimer = null;
    let lastBbox = '';
    let cache = new Map();
    let featuresById = new Map();
    let currentSequence = null;
    let sequenceImages = [];
    let currentImageIndex = 0;

    const log = (msg) => console.log(`[${SCRIPT_NAME}] ${msg}`);

    // ============ CONFIG ============
    function loadConfig() {
        try {
            const s = localStorage.getItem(STORAGE_KEY);
            if (s) {
                const saved = JSON.parse(s);
                // Merge top-level
                Object.assign(CONFIG, saved);
                // Merge nested signFilters
                if (saved.signFilters) {
                    CONFIG.signFilters = { ...CONFIG.signFilters, ...saved.signFilters };
                }
            }
        } catch (e) {
            log('Config laden fehlgeschlagen: ' + e.message);
        }
    }

    function saveConfig() {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(CONFIG));
        } catch (e) {
            log('Config speichern fehlgeschlagen: ' + e.message);
        }
    }

    // ============ API ============
    const CACHE_MAX_SIZE = 200; // Größerer Cache
    const CACHE_TTL = 5 * 60 * 1000; // 5 Minuten TTL
    
    function api(url) {
        return new Promise((resolve) => {
            // Cache prüfen mit TTL
            const cached = cache.get(url);
            if (cached && (Date.now() - cached.time < CACHE_TTL)) {
                resolve(cached.data);
                return;
            }
            
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                timeout: 15000,
                onload: (r) => {
                    try {
                        const data = JSON.parse(r.responseText);
                        // Cache mit Zeitstempel
                        cache.set(url, { data, time: Date.now() });
                        // Cache-Größe begrenzen
                        if (cache.size > CACHE_MAX_SIZE) {
                            const firstKey = cache.keys().next().value;
                            cache.delete(firstKey);
                        }
                        resolve(data);
                    } catch (e) {
                        resolve({ data: [] });
                    }
                },
                onerror: () => resolve({ data: [] }),
                ontimeout: () => resolve({ data: [] })
            });
        });
    }

    async function loadImages(bbox) {
        let url = `https://graph.mapillary.com/images?access_token=${ACCESS_TOKEN}&fields=id,geometry,captured_at,is_pano,sequence,compass_angle,creator&bbox=${bbox}&limit=${CONFIG.maxImages}`;
        
        // Datumsfilter
        if (CONFIG.filterByDate) {
            if (CONFIG.dateFrom) url += `&start_captured_at=${CONFIG.dateFrom}T00:00:00Z`;
            if (CONFIG.dateTo) url += `&end_captured_at=${CONFIG.dateTo}T23:59:59Z`;
        }
        
        const data = await api(url);
        return data.data || [];
    }

    async function loadSigns(bbox) {
        // Baue object_values basierend auf aktiven Filtern
        const patterns = [];
        for (const [key, filter] of Object.entries(SIGN_FILTER_CATEGORIES)) {
            if (CONFIG.signFilters[key]) {
                patterns.push(...filter.pattern.split(','));
            }
        }
        
        // Wenn keine Filter aktiv, lade nichts
        if (patterns.length === 0) return [];
        
        // Deduplizieren und zu String
        const uniquePatterns = [...new Set(patterns)].join(',');
        
        const url = `https://graph.mapillary.com/map_features?access_token=${ACCESS_TOKEN}&fields=id,geometry,object_value,first_seen_at,last_seen_at,images&bbox=${bbox}&object_values=${uniquePatterns}&limit=${CONFIG.maxSigns}`;
        const data = await api(url);
        return data.data || [];
    }

    // Prüft ob ein Schild zu einer Kategorie gehört
    function matchesSignCategory(objectValue, categoryKey) {
        if (!objectValue || !SIGN_FILTER_CATEGORIES[categoryKey]) return false;
        const patterns = SIGN_FILTER_CATEGORIES[categoryKey].pattern.split(',');
        for (const pattern of patterns) {
            const regex = new RegExp('^' + pattern.replace(/\*/g, '.*'));
            if (regex.test(objectValue)) return true;
        }
        return false;
    }

    // Filtert Schilder nach aktiven Kategorien
    function filterSignsByCategory(signs) {
        return signs.filter(sign => {
            if (!sign.object_value) return false;
            for (const [key, enabled] of Object.entries(CONFIG.signFilters)) {
                if (enabled && matchesSignCategory(sign.object_value, key)) {
                    return true;
                }
            }
            return false;
        });
    }

    async function loadFeatures(bbox) {
        const url = `https://graph.mapillary.com/map_features?access_token=${ACCESS_TOKEN}&fields=id,geometry,object_value,first_seen_at,last_seen_at,images&bbox=${bbox}&object_values=object--*&limit=300`;
        const data = await api(url);
        return data.data || [];
    }

    async function loadSequenceImages(sequenceId) {
        const url = `https://graph.mapillary.com/image_ids?access_token=${ACCESS_TOKEN}&sequence_id=${sequenceId}`;
        const data = await api(url);
        return data.data || [];
    }

    async function getImageDetails(imageId) {
        const url = `https://graph.mapillary.com/${imageId}?access_token=${ACCESS_TOKEN}&fields=id,thumb_1024_url,thumb_2048_url,captured_at,creator,is_pano,compass_angle,computed_geometry,sequence,width,height,camera_type,make,model`;
        return await api(url);
    }

    async function getSignImage(signId) {
        const url = `https://graph.mapillary.com/${signId}?access_token=${ACCESS_TOKEN}&fields=images`;
        const data = await api(url);
        return data.images?.data?.[0]?.id || null;
    }

    // ============ SIGN CATEGORIES ============
    const SIGN_CATEGORIES = {
        'regulatory--stop': { icon: '🛑', name: 'Stop' },
        'regulatory--yield': { icon: '⚠️', name: 'Vorfahrt gewähren' },
        'regulatory--speed-limit': { icon: '🚗', name: 'Tempolimit' },
        'regulatory--no-entry': { icon: '⛔', name: 'Einfahrt verboten' },
        'regulatory--no-parking': { icon: '🅿️', name: 'Parkverbot' },
        'regulatory--one-way': { icon: '➡️', name: 'Einbahnstraße' },
        'regulatory--turn': { icon: '↩️', name: 'Abbiegen' },
        'warning--pedestrians': { icon: '🚶', name: 'Fußgänger' },
        'warning--children': { icon: '🚸', name: 'Kinder' },
        'warning--curve': { icon: '↪️', name: 'Kurve' },
        'warning--crossroads': { icon: '✚', name: 'Kreuzung' },
        'information--parking': { icon: '🅿️', name: 'Parken' },
        'information--highway': { icon: '🛣️', name: 'Autobahn' }
    };

    function getSignInfo(value) {
        if (!value) return { icon: '🚦', name: 'Schild' };
        for (const [key, info] of Object.entries(SIGN_CATEGORIES)) {
            if (value.includes(key)) return info;
        }
        const parts = value.split('--');
        return { 
            icon: value.startsWith('warning') ? '⚠️' : value.startsWith('regulatory') ? '🚫' : 'ℹ️',
            name: parts.length >= 2 ? parts[1].replace(/-/g, ' ') : value 
        };
    }

    // ============ FEATURE CATEGORIES ============
    const FEATURE_CATEGORIES = {
        'object--traffic-light': { icon: '🚦', name: 'Ampel' },
        'object--street-light': { icon: '💡', name: 'Straßenlaterne' },
        'object--fire-hydrant': { icon: '🧯', name: 'Hydrant' },
        'object--bench': { icon: '🪑', name: 'Bank' },
        'object--trash-can': { icon: '🗑️', name: 'Mülleimer' },
        'object--mailbox': { icon: '📮', name: 'Briefkasten' },
        'object--phone-booth': { icon: '📞', name: 'Telefonzelle' },
        'object--pole': { icon: '📍', name: 'Mast' },
        'object--manhole': { icon: '⭕', name: 'Kanaldeckel' },
        'object--utility-pole': { icon: '⚡', name: 'Strommast' }
    };

    function getFeatureInfo(value) {
        if (!value) return { icon: '📍', name: 'Objekt' };
        for (const [key, info] of Object.entries(FEATURE_CATEGORIES)) {
            if (value.includes(key)) return info;
        }
        const parts = value.split('--');
        return { icon: '📍', name: parts.length >= 2 ? parts[1].replace(/-/g, ' ') : value };
    }

    // ============ LAYERS ============
    function createLayers() {
        if (!W?.map) return;
        removeLayers();

        const ts = Date.now();

        // Sequence Layer (Linien)
        sequenceLayer = new OpenLayers.Layer.Vector('MLY_Seq_' + ts, {
            displayInLayerSwitcher: false,
            styleMap: new OpenLayers.StyleMap({
                'default': new OpenLayers.Style({
                    strokeColor: '#05CB63',
                    strokeWidth: 3,
                    strokeOpacity: 0.6
                })
            })
        });

        // Image Layer
        imageLayer = new OpenLayers.Layer.Vector('MLY_Img_' + ts, {
            displayInLayerSwitcher: false,
            styleMap: new OpenLayers.StyleMap({
                'default': new OpenLayers.Style({
                    pointRadius: 6,
                    fillColor: '${fillColor}',
                    fillOpacity: CONFIG.opacity,
                    strokeColor: '#fff',
                    strokeWidth: 1.5,
                    rotation: '${rotation}'
                })
            })
        });

        // Sign Layer
        signLayer = new OpenLayers.Layer.Vector('MLY_Sign_' + ts, {
            displayInLayerSwitcher: false,
            styleMap: new OpenLayers.StyleMap({
                'default': new OpenLayers.Style({
                    pointRadius: 8,
                    fillColor: CONFIG.signColor,
                    fillOpacity: CONFIG.opacity,
                    strokeColor: '#fff',
                    strokeWidth: 2,
                    graphicName: 'triangle'
                })
            })
        });

        // Feature Layer
        featureLayer = new OpenLayers.Layer.Vector('MLY_Feat_' + ts, {
            displayInLayerSwitcher: false,
            styleMap: new OpenLayers.StyleMap({
                'default': new OpenLayers.Style({
                    pointRadius: 6,
                    fillColor: CONFIG.featureColor,
                    fillOpacity: CONFIG.opacity,
                    strokeColor: '#fff',
                    strokeWidth: 1.5,
                    graphicName: 'square'
                })
            })
        });

        W.map.addLayer(sequenceLayer);
        W.map.addLayer(imageLayer);
        W.map.addLayer(signLayer);
        W.map.addLayer(featureLayer);

        W.map.setLayerIndex(sequenceLayer, 99);
        W.map.setLayerIndex(imageLayer, 100);
        W.map.setLayerIndex(signLayer, 101);
        W.map.setLayerIndex(featureLayer, 102);

        W.map.events.register('moveend', null, scheduleRefresh);

        log('Layers erstellt');
        refreshData();
    }

    function removeLayers() {
        if (!W?.map) return;
        W.map.events.unregister('moveend', null, scheduleRefresh);

        [sequenceLayer, imageLayer, signLayer, featureLayer].forEach(layer => {
            if (layer) {
                try { W.map.removeLayer(layer); layer.destroy(); } catch(e) {}
            }
        });
        sequenceLayer = imageLayer = signLayer = featureLayer = null;
        featuresById.clear();
    }

    function scheduleRefresh() {
        if (refreshTimer) clearTimeout(refreshTimer);
        // 2 Sekunden warten bevor neu geladen wird (Debounce beim Scrollen)
        refreshTimer = setTimeout(refreshData, 2000);
    }

    let isLoading = false;
    let pendingRefresh = false;

    async function refreshData() {
        if (!CONFIG.enabled || !imageLayer) return;

        // Verhindere parallele Ladevorgänge
        if (isLoading) {
            pendingRefresh = true;
            return;
        }

        const zoom = W.map.getZoom();
        if (zoom < CONFIG.minZoom) {
            setStatus(t('zoomIn', { zoom: CONFIG.minZoom }));
            [sequenceLayer, imageLayer, signLayer, featureLayer].forEach(l => l?.removeAllFeatures());
            return;
        }

        const bounds = W.map.getExtent();
        const proj4326 = new OpenLayers.Projection('EPSG:4326');
        const projMap = W.map.getProjectionObject();
        const b = bounds.clone().transform(projMap, proj4326);

        const bbox = `${b.left.toFixed(6)},${b.bottom.toFixed(6)},${b.right.toFixed(6)},${b.top.toFixed(6)}`;

        const area = (b.right - b.left) * (b.top - b.bottom);
        if (area > 0.01) {
            setStatus(t('zoomMore'));
            return;
        }

        if (bbox === lastBbox) return;
        lastBbox = bbox;

        isLoading = true;
        setStatus(t('loading'));

        try {
            // Parallel laden
            const promises = [];
            if (CONFIG.showImages || CONFIG.showPanos) promises.push(loadImages(bbox));
            else promises.push(Promise.resolve([]));
            
            if (CONFIG.showSigns) promises.push(loadSigns(bbox));
            else promises.push(Promise.resolve([]));
            
            if (CONFIG.showFeatures) promises.push(loadFeatures(bbox));
            else promises.push(Promise.resolve([]));

            const [images, signs, features] = await Promise.all(promises);

            // Prüfe ob sich die Ansicht während des Ladens geändert hat
            const currentBounds = W.map.getExtent();
            const cb = currentBounds.clone().transform(projMap, proj4326);
            const currentBbox = `${cb.left.toFixed(6)},${cb.bottom.toFixed(6)},${cb.right.toFixed(6)},${cb.top.toFixed(6)}`;
            
            if (currentBbox !== bbox) {
                // Ansicht hat sich geändert, verwerfe Ergebnisse
                return;
            }

            featuresById.clear();
            [sequenceLayer, imageLayer, signLayer, featureLayer].forEach(l => l?.removeAllFeatures());

            // Sequences sammeln
            const sequences = new Map();

            // Images
            const imgFeatures = [];
            for (const img of images) {
                if (!img.geometry?.coordinates) continue;
                const isPano = img.is_pano || false;
                if (isPano && !CONFIG.showPanos) continue;
                if (!isPano && !CONFIG.showImages) continue;

                const [lon, lat] = img.geometry.coordinates;
                const pt = new OpenLayers.Geometry.Point(lon, lat).transform(proj4326, projMap);
                const fid = 'img_' + img.id;

                const f = new OpenLayers.Feature.Vector(pt, {
                    fid: fid,
                    fillColor: isPano ? CONFIG.panoColor : CONFIG.imageColor,
                    rotation: img.compass_angle || 0
                });
                f.fid = fid;
                imgFeatures.push(f);

                featuresById.set(fid, { 
                    type: 'image', 
                    id: img.id, 
                    date: img.captured_at, 
                    isPano,
                    sequence: img.sequence,
                    creator: img.creator,
                    compass: img.compass_angle
                });

                // Sequence sammeln
                if (img.sequence && CONFIG.showSequenceLines) {
                    if (!sequences.has(img.sequence)) {
                        sequences.set(img.sequence, []);
                    }
                    sequences.get(img.sequence).push({ lon, lat, date: img.captured_at });
                }
            }
            imageLayer.addFeatures(imgFeatures);

            // Sequence Lines
            if (CONFIG.showSequenceLines) {
                const seqFeatures = [];
                for (const [seqId, points] of sequences) {
                    if (points.length < 2) continue;
                    points.sort((a, b) => a.date - b.date);
                    const linePoints = points.map(p => 
                        new OpenLayers.Geometry.Point(p.lon, p.lat).transform(proj4326, projMap)
                    );
                    const line = new OpenLayers.Geometry.LineString(linePoints);
                    seqFeatures.push(new OpenLayers.Feature.Vector(line));
                }
                sequenceLayer.addFeatures(seqFeatures);
            }

            // Signs
            const signFeatures = [];
            for (const sign of signs) {
                if (!sign.geometry?.coordinates) continue;
                const [lon, lat] = sign.geometry.coordinates;
                const pt = new OpenLayers.Geometry.Point(lon, lat).transform(proj4326, projMap);
                const fid = 'sign_' + sign.id;

                const f = new OpenLayers.Feature.Vector(pt, {});
                f.fid = fid;
                signFeatures.push(f);

                const info = getSignInfo(sign.object_value);
                featuresById.set(fid, { 
                    type: 'sign', 
                    id: sign.id, 
                    value: sign.object_value,
                    icon: info.icon,
                    name: info.name,
                    firstSeen: sign.first_seen_at,
                    lastSeen: sign.last_seen_at,
                    images: sign.images?.data || []
                });
            }
            signLayer.addFeatures(signFeatures);

            // Features
            const featFeatures = [];
            for (const feat of features) {
                if (!feat.geometry?.coordinates) continue;
                const [lon, lat] = feat.geometry.coordinates;
                const pt = new OpenLayers.Geometry.Point(lon, lat).transform(proj4326, projMap);
                const fid = 'feat_' + feat.id;

                const f = new OpenLayers.Feature.Vector(pt, {});
                f.fid = fid;
                featFeatures.push(f);

                const info = getFeatureInfo(feat.object_value);
                featuresById.set(fid, { 
                    type: 'feature', 
                    id: feat.id, 
                    value: feat.object_value,
                    icon: info.icon,
                    name: info.name,
                    firstSeen: feat.first_seen_at,
                    lastSeen: feat.last_seen_at,
                    images: feat.images?.data || []
                });
            }
            featureLayer.addFeatures(featFeatures);

            setStatus(`${imgFeatures.length} ${t('statsImages')} | ${signFeatures.length} ${t('statsSigns')} | ${featFeatures.length} ${t('statsObjects')}`);
        } finally {
            isLoading = false;
            // Falls während des Ladens eine neue Anfrage kam
            if (pendingRefresh) {
                pendingRefresh = false;
                lastBbox = ''; // Force reload
                scheduleRefresh();
            }
        }
    }

    // ============ CLICK HANDLER ============
    function setupClickHandler() {
        const mapEl = document.getElementById('WazeMap') || document.getElementById('map');
        if (!mapEl) {
            setTimeout(setupClickHandler, 1000);
            return;
        }
        mapEl.addEventListener('click', handleClick, true);
        log('Click handler installiert');
    }

    function handleClick(e) {
        if (!CONFIG.enabled) return;
        if (e.target.closest('.sidebar, .toolbar, #mly-viewer, button, input, a, select, .olControlPanel')) return;

        const mapEl = e.currentTarget;
        const rect = mapEl.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;

        const feature = findFeatureAt(x, y);
        if (feature) {
            e.stopPropagation();
            e.preventDefault();
            onFeatureClick(feature);
        }
    }

    function findFeatureAt(x, y) {
        const tolerance = 15;
        let best = null;
        let bestDist = Infinity;

        const olMap = W.map.olMap || W.map.getOLMap?.() || W.map;

        const check = (layer) => {
            if (!layer?.features) return;
            for (const f of layer.features) {
                if (!f.geometry || !f.fid) continue;
                try {
                    const lonlat = new OpenLayers.LonLat(f.geometry.x, f.geometry.y);
                    let px;
                    if (olMap.getPixelFromLonLat) {
                        px = olMap.getPixelFromLonLat(lonlat);
                    } else {
                        const res = olMap.getResolution();
                        const extent = olMap.getExtent();
                        if (res && extent) {
                            px = {
                                x: (f.geometry.x - extent.left) / res,
                                y: (extent.top - f.geometry.y) / res
                            };
                        }
                    }
                    if (!px) continue;

                    const dx = x - px.x;
                    const dy = y - px.y;
                    const dist = Math.sqrt(dx*dx + dy*dy);

                    if (dist < tolerance && dist < bestDist) {
                        bestDist = dist;
                        best = f;
                    }
                } catch (e) {}
            }
        };

        check(featureLayer);
        check(signLayer);
        check(imageLayer);
        return best;
    }

    async function onFeatureClick(feature) {
        const data = featuresById.get(feature.fid);
        if (!data) return;

        log('Klick:', feature.fid);

        if (data.type === 'image') {
            currentSequence = data.sequence;
            // Sequenz automatisch laden für Navigation
            if (data.sequence) {
                loadSequenceForNavigation(data.sequence, data.id);
            }
            openViewer(data.id, data);
        } else if (data.type === 'sign' || data.type === 'feature') {
            // Zeige Info und lade erstes Bild
            if (data.images && data.images.length > 0) {
                openViewer(data.images[0].id, data);
            } else {
                const imgId = await getSignImage(data.id);
                if (imgId) {
                    openViewer(imgId, data);
                } else {
                    setStatus(t('noImageFound'));
                }
            }
        }
    }

    // Sequenz im Hintergrund laden
    async function loadSequenceForNavigation(sequenceId, currentImageId) {
        const images = await loadSequenceImages(sequenceId);
        sequenceImages = images.map(i => i.id);
        currentImageIndex = sequenceImages.indexOf(currentImageId);
        if (currentImageIndex === -1) currentImageIndex = 0;
        updateNavButtons();
        log(`Sequenz geladen: ${sequenceImages.length} Bilder`);
    }

    // ============ VIEWER ============
    function createViewer() {
        if (viewerDiv) return;

        GM_addStyle(`
            #mly-viewer {
                position: fixed; bottom: 20px; right: 20px;
                width: 420px; background: #1a1a2e; border-radius: 12px;
                box-shadow: 0 8px 32px rgba(0,0,0,0.6);
                z-index: 100000; display: none; flex-direction: column;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                max-height: 90vh; overflow: hidden;
            }
            #mly-viewer.open { display: flex !important; }
            #mly-viewer-head {
                display: flex; justify-content: space-between; align-items: center;
                padding: 12px 15px; background: linear-gradient(135deg, #16213e, #1a1a2e);
                cursor: move; border-radius: 12px 12px 0 0;
            }
            #mly-viewer-head span { color: #05CB63; font-weight: 600; font-size: 14px; }
            #mly-viewer-head .mly-btns { display: flex; gap: 5px; }
            #mly-viewer-head button {
                background: rgba(255,255,255,0.1); border: none; color: #fff;
                font-size: 14px; cursor: pointer; padding: 6px 10px; border-radius: 6px;
            }
            #mly-viewer-head button:hover { background: rgba(255,255,255,0.2); }
            #mly-viewer-body { padding: 15px; overflow-y: auto; }
            #mly-thumb-container { position: relative; }
            #mly-thumb {
                width: 100%; height: 240px; object-fit: cover;
                border-radius: 8px; background: #000; cursor: pointer;
            }
            #mly-thumb:hover { opacity: 0.95; }
            #mly-nav {
                position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%);
                display: flex; gap: 8px;
            }
            #mly-nav button {
                background: rgba(0,0,0,0.7); border: none; color: #fff;
                padding: 8px 15px; border-radius: 20px; cursor: pointer; font-size: 16px;
            }
            #mly-nav button:hover { background: rgba(0,0,0,0.9); }
            #mly-nav button:disabled { opacity: 0.3; cursor: not-allowed; }
            #mly-badge {
                position: absolute; top: 10px; left: 10px;
                background: rgba(0,0,0,0.7); color: #fff; padding: 5px 10px;
                border-radius: 15px; font-size: 12px;
            }
            #mly-viewer-info {
                margin-top: 12px; color: #ccc; font-size: 12px;
                background: rgba(255,255,255,0.05); padding: 10px; border-radius: 8px;
            }
            #mly-viewer-info .mly-row {
                display: flex; justify-content: space-between; margin: 4px 0;
            }
            #mly-viewer-info .mly-label { color: #888; }
            #mly-viewer-info .mly-value { color: #fff; }
            #mly-detection-info {
                margin-top: 10px; padding: 10px; background: rgba(231,76,60,0.3);
                border-radius: 8px; border-left: 3px solid #e74c3c;
                color: #fff;
            }
            #mly-detection-info .mly-icon { font-size: 24px; margin-right: 8px; }
            #mly-detection-info b { color: #fff; }
            #mly-detection-info small { color: #ddd; }
        `);

        viewerDiv = document.createElement('div');
        viewerDiv.id = 'mly-viewer';
        viewerDiv.innerHTML = `
            <div id="mly-viewer-head">
                <span>🗺️ ${t('viewer')}</span>
                <div class="mly-btns">
                    <button id="mly-btn-copy" title="${t('linkCopied').replace('!', '')}">📋</button>
                    <button id="mly-btn-close" title="${t('shortcutClose')}">✕</button>
                </div>
            </div>
            <div id="mly-viewer-body">
                <div id="mly-thumb-container">
                    <img id="mly-thumb" src="" alt="Mapillary">
                    <div id="mly-badge"></div>
                    <div id="mly-nav">
                        <button id="mly-prev" title="${t('prevImage')}">◀</button>
                        <button id="mly-next" title="${t('nextImage')}">▶</button>
                    </div>
                </div>
                <div id="mly-detection-info" style="display:none;"></div>
                <div id="mly-viewer-info"></div>
            </div>
        `;
        document.body.appendChild(viewerDiv);

        // Events
        document.getElementById('mly-btn-close').onclick = closeViewer;
        document.getElementById('mly-btn-copy').onclick = copyImageLink;
        document.getElementById('mly-thumb').onclick = openInMapillary;
        document.getElementById('mly-prev').onclick = () => navigateSequence(-1);
        document.getElementById('mly-next').onclick = () => navigateSequence(1);

        // Draggable
        let drag = false, sx, sy, sl, st;
        const head = document.getElementById('mly-viewer-head');
        head.onmousedown = (e) => {
            if (e.target.tagName === 'BUTTON') return;
            drag = true; sx = e.clientX; sy = e.clientY;
            const r = viewerDiv.getBoundingClientRect();
            sl = r.left; st = r.top;
        };
        document.onmousemove = (e) => {
            if (!drag) return;
            viewerDiv.style.left = (sl + e.clientX - sx) + 'px';
            viewerDiv.style.top = (st + e.clientY - sy) + 'px';
            viewerDiv.style.right = 'auto';
            viewerDiv.style.bottom = 'auto';
        };
        document.onmouseup = () => drag = false;
    }

    let currentImageData = null;

    async function openViewer(imageId, featureData = {}) {
        createViewer();
        viewerDiv.classList.add('open');

        const thumb = document.getElementById('mly-thumb');
        const badge = document.getElementById('mly-badge');
        const infoEl = document.getElementById('mly-viewer-info');
        const detectionEl = document.getElementById('mly-detection-info');

        // Sofort Thumbnail laden (Mapillary URL-Schema)
        thumb.src = `https://thumbnails.mapillary.com/${imageId}/thumb-1024.jpg`;
        
        // Badge sofort setzen wenn bekannt
        badge.textContent = '';
        if (featureData.type === 'sign') badge.textContent = featureData.icon + ' ' + featureData.name;
        else if (featureData.type === 'feature') badge.textContent = featureData.icon + ' ' + featureData.name;
        else if (featureData.isPano) badge.textContent = '360°';

        // Detection Info sofort anzeigen
        if (featureData.type === 'sign' || featureData.type === 'feature') {
            detectionEl.style.display = 'block';
            const firstSeen = featureData.firstSeen ? new Date(featureData.firstSeen).toLocaleDateString(currentLang) : '-';
            const lastSeen = featureData.lastSeen ? new Date(featureData.lastSeen).toLocaleDateString(currentLang) : '-';
            detectionEl.innerHTML = `
                <span class="mly-icon">${featureData.icon}</span>
                <b>${featureData.name}</b><br>
                <small>${t('firstSeen')}: ${firstSeen} | ${t('lastSeen')}: ${lastSeen}</small>
            `;
        } else {
            detectionEl.style.display = 'none';
        }

        // Basis-Info sofort anzeigen
        const dateStr = featureData.date ? new Date(featureData.date).toLocaleDateString(currentLang, { 
            year: 'numeric', month: 'long', day: 'numeric'
        }) : '';
        
        infoEl.innerHTML = `
            <div class="mly-row"><span class="mly-label">${t('date')}:</span><span class="mly-value">${dateStr || t('loading')}</span></div>
            <div class="mly-row"><span class="mly-label">${t('creator')}:</span><span class="mly-value" id="mly-info-creator">${t('loading')}</span></div>
            <div class="mly-row"><span class="mly-label">${t('camera')}:</span><span class="mly-value" id="mly-info-camera">-</span></div>
            <div class="mly-row"><span class="mly-label">${t('resolution')}:</span><span class="mly-value" id="mly-info-size">-</span></div>
            <div class="mly-row"><span class="mly-label">ID:</span><span class="mly-value" style="font-size:10px;">${imageId}</span></div>
        `;

        currentImageData = { id: imageId, featureData };
        updateNavButtons();

        // Details im Hintergrund laden
        getImageDetails(imageId).then(data => {
            if (currentImageData?.id !== imageId) return; // Abgebrochen
            
            currentImageData = { ...data, featureData };
            
            // Besseres Thumbnail wenn verfügbar
            if (data.thumb_2048_url) {
                thumb.src = data.thumb_2048_url;
            }
            
            // Badge aktualisieren
            if (data.is_pano && !featureData.type) badge.textContent = '360°';
            
            // Details aktualisieren
            const date = data.captured_at ? new Date(data.captured_at).toLocaleDateString(currentLang, { 
                year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' 
            }) : '-';
            const creator = data.creator?.username || '-';
            const camera = [data.make, data.model].filter(Boolean).join(' ') || '-';
            const size = data.width && data.height ? `${data.width}×${data.height}` : '-';

            infoEl.innerHTML = `
                <div class="mly-row"><span class="mly-label">${t('date')}:</span><span class="mly-value">${date}</span></div>
                <div class="mly-row"><span class="mly-label">${t('creator')}:</span><span class="mly-value">${creator}</span></div>
                <div class="mly-row"><span class="mly-label">${t('camera')}:</span><span class="mly-value">${camera}</span></div>
                <div class="mly-row"><span class="mly-label">${t('resolution')}:</span><span class="mly-value">${size}</span></div>
                <div class="mly-row"><span class="mly-label">ID:</span><span class="mly-value" style="font-size:10px;">${imageId}</span></div>
            `;
        });

        log('Viewer:', imageId);
    }

    function closeViewer() {
        if (viewerDiv) viewerDiv.classList.remove('open');
        sequenceImages = [];
        currentImageIndex = 0;
    }

    function getMapillaryUrl(imageId) {
        // Koordinaten aus currentImageData wenn verfügbar
        let url = `https://www.mapillary.com/app/?pKey=${imageId}&focus=photo`;
        
        if (currentImageData?.computed_geometry?.coordinates) {
            const [lon, lat] = currentImageData.computed_geometry.coordinates;
            url += `&lat=${lat.toFixed(7)}&lng=${lon.toFixed(7)}&z=17`;
        }
        
        return url;
    }

    function openInMapillary() {
        if (currentImageData?.id) {
            window.open(getMapillaryUrl(currentImageData.id), '_blank');
        }
    }

    function copyImageLink() {
        if (currentImageData?.id) {
            const url = getMapillaryUrl(currentImageData.id);
            GM_setClipboard(url);
            setStatus(t('linkCopied'));
        }
    }

    function navigateSequence(direction) {
        if (sequenceImages.length === 0) return;

        currentImageIndex += direction;
        if (currentImageIndex < 0) currentImageIndex = 0;
        if (currentImageIndex >= sequenceImages.length) currentImageIndex = sequenceImages.length - 1;

        openViewer(sequenceImages[currentImageIndex], currentImageData?.featureData || {});
    }

    function updateNavButtons() {
        const prevBtn = document.getElementById('mly-prev');
        const nextBtn = document.getElementById('mly-next');

        if (sequenceImages.length > 0) {
            prevBtn.disabled = currentImageIndex <= 0;
            nextBtn.disabled = currentImageIndex >= sequenceImages.length - 1;
            prevBtn.style.display = 'block';
            nextBtn.style.display = 'block';
        } else {
            prevBtn.style.display = 'none';
            nextBtn.style.display = 'none';
        }
    }

    // ============ STATUS ============
    function setStatus(msg) {
        const el = document.getElementById('mly-status');
        if (el) el.textContent = msg;
        log(msg);
    }

    // ============ UI PANEL ============
    function createUI() {
        const tabHtml = `
            <div id="mly-panel" style="padding: 10px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
                <div style="display: flex; align-items: center; margin-bottom: 15px;">
                    <span style="font-size: 24px; margin-right: 10px;">🗺️</span>
                    <div>
                        <div style="font-weight: 600; font-size: 14px;">Mapillary Viewer</div>
                        <div style="font-size: 11px; color: #888;">v${SCRIPT_VERSION}</div>
                    </div>
                </div>

                <div id="mly-status" style="background: #f0f0f0; padding: 8px; border-radius: 6px; margin-bottom: 15px; font-size: 12px; text-align: center;">
                    ${t('ready')}
                </div>

                <div style="margin-bottom: 15px;">
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 8px;">
                        <input type="checkbox" id="mly-enabled" ${CONFIG.enabled ? 'checked' : ''} style="margin-right: 8px;">
                        <span style="font-weight: 500;">${t('enabled')}</span>
                    </label>
                </div>

                <div style="border-top: 1px solid #ddd; padding-top: 12px; margin-bottom: 12px;">
                    <div style="font-weight: 600; margin-bottom: 10px; font-size: 13px;">📷 ${t('display')}</div>
                    
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 6px;">
                        <input type="checkbox" id="mly-showImages" ${CONFIG.showImages ? 'checked' : ''} style="margin-right: 8px;">
                        <span style="color: ${CONFIG.imageColor};">●</span>&nbsp;${t('images')}
                    </label>
                    
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 6px;">
                        <input type="checkbox" id="mly-showPanos" ${CONFIG.showPanos ? 'checked' : ''} style="margin-right: 8px;">
                        <span style="color: ${CONFIG.panoColor};">●</span>&nbsp;${t('panoramas')}
                    </label>
                    
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 6px;">
                        <input type="checkbox" id="mly-showSigns" ${CONFIG.showSigns ? 'checked' : ''} style="margin-right: 8px;">
                        <span style="color: ${CONFIG.signColor};">▲</span>&nbsp;${t('trafficSigns')}
                    </label>
                    
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 6px;">
                        <input type="checkbox" id="mly-showFeatures" ${CONFIG.showFeatures ? 'checked' : ''} style="margin-right: 8px;">
                        <span style="color: ${CONFIG.featureColor};">■</span>&nbsp;${t('objects')}
                    </label>
                    
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 6px;">
                        <input type="checkbox" id="mly-showSequenceLines" ${CONFIG.showSequenceLines ? 'checked' : ''} style="margin-right: 8px;">
                        <span>📍</span>&nbsp;${t('sequenceLines')}
                    </label>
                </div>

                <div id="mly-sign-filters" style="border-top: 1px solid #ddd; padding-top: 12px; margin-bottom: 12px; display: ${CONFIG.showSigns ? 'block' : 'none'};">
                    <div style="font-weight: 600; margin-bottom: 10px; font-size: 13px;">🚦 ${t('signFilters')}</div>
                    
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 5px; font-size: 12px;">
                        <input type="checkbox" id="mly-sign-speed" ${CONFIG.signFilters.speed ? 'checked' : ''} style="margin-right: 6px;">
                        <span>🚗</span>&nbsp;${t('speed')}
                    </label>
                    
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 5px; font-size: 12px;">
                        <input type="checkbox" id="mly-sign-stop" ${CONFIG.signFilters.stop ? 'checked' : ''} style="margin-right: 6px;">
                        <span>🛑</span>&nbsp;${t('stopYield')}
                    </label>
                    
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 5px; font-size: 12px;">
                        <input type="checkbox" id="mly-sign-turn" ${CONFIG.signFilters.turn ? 'checked' : ''} style="margin-right: 6px;">
                        <span>↩️</span>&nbsp;${t('turnDirection')}
                    </label>
                    
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 5px; font-size: 12px;">
                        <input type="checkbox" id="mly-sign-noEntry" ${CONFIG.signFilters.noEntry ? 'checked' : ''} style="margin-right: 6px;">
                        <span>⛔</span>&nbsp;${t('prohibitions')}
                    </label>
                    
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 5px; font-size: 12px;">
                        <input type="checkbox" id="mly-sign-parking" ${CONFIG.signFilters.parking ? 'checked' : ''} style="margin-right: 6px;">
                        <span>🅿️</span>&nbsp;${t('parking')}
                    </label>
                    
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 5px; font-size: 12px;">
                        <input type="checkbox" id="mly-sign-pedestrian" ${CONFIG.signFilters.pedestrian ? 'checked' : ''} style="margin-right: 6px;">
                        <span>🚶</span>&nbsp;${t('pedestrianCyclist')}
                    </label>
                    
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 5px; font-size: 12px;">
                        <input type="checkbox" id="mly-sign-warning" ${CONFIG.signFilters.warning ? 'checked' : ''} style="margin-right: 6px;">
                        <span>⚠️</span>&nbsp;${t('warningSigns')}
                    </label>
                    
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 5px; font-size: 12px;">
                        <input type="checkbox" id="mly-sign-info" ${CONFIG.signFilters.info ? 'checked' : ''} style="margin-right: 6px;">
                        <span>ℹ️</span>&nbsp;${t('infoSigns')}
                    </label>
                    
                    <div style="margin-top: 8px; display: flex; gap: 6px;">
                        <button id="mly-signs-all" style="flex: 1; padding: 4px 8px; font-size: 11px; background: #05CB63; color: white; border: none; border-radius: 4px; cursor: pointer;">${t('all')}</button>
                        <button id="mly-signs-none" style="flex: 1; padding: 4px 8px; font-size: 11px; background: #e74c3c; color: white; border: none; border-radius: 4px; cursor: pointer;">${t('none')}</button>
                    </div>
                </div>

                <div style="border-top: 1px solid #ddd; padding-top: 12px; margin-bottom: 12px;">
                    <div style="font-weight: 600; margin-bottom: 10px; font-size: 13px;">🎨 ${t('appearance')}</div>
                    
                    <div style="margin-bottom: 8px;">
                        <label style="font-size: 12px; display: block; margin-bottom: 4px;">${t('opacity')}: <span id="mly-opacity-val">${Math.round(CONFIG.opacity * 100)}%</span></label>
                        <input type="range" id="mly-opacity" min="20" max="100" value="${CONFIG.opacity * 100}" style="width: 100%;">
                    </div>
                </div>

                <div style="border-top: 1px solid #ddd; padding-top: 12px; margin-bottom: 12px;">
                    <div style="font-weight: 600; margin-bottom: 10px; font-size: 13px;">⚡ ${t('performance')}</div>
                    
                    <div style="margin-bottom: 10px;">
                        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
                            <label style="font-size: 12px;">${t('maxImages')}:</label>
                            <span id="mly-maxImages-val" style="font-weight: 600; color: #05CB63; background: #e8f5e9; padding: 2px 8px; border-radius: 4px; font-size: 12px;">${CONFIG.maxImages}</span>
                        </div>
                        <input type="range" id="mly-maxImages" min="100" max="5000" step="100" value="${CONFIG.maxImages}" style="width: 100%;">
                        <div style="display: flex; justify-content: space-between; font-size: 10px; color: #888;">
                            <span>100 (${t('fast')})</span>
                            <span>5000 (${t('complete')})</span>
                        </div>
                    </div>
                    
                    <div style="margin-bottom: 8px;">
                        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
                            <label style="font-size: 12px;">${t('maxSigns')}:</label>
                            <span id="mly-maxSigns-val" style="font-weight: 600; color: #e74c3c; background: #ffebee; padding: 2px 8px; border-radius: 4px; font-size: 12px;">${CONFIG.maxSigns}</span>
                        </div>
                        <input type="range" id="mly-maxSigns" min="50" max="2000" step="50" value="${CONFIG.maxSigns}" style="width: 100%;">
                        <div style="display: flex; justify-content: space-between; font-size: 10px; color: #888;">
                            <span>50 (${t('fast')})</span>
                            <span>2000 (${t('complete')})</span>
                        </div>
                    </div>
                </div>

                <div style="border-top: 1px solid #ddd; padding-top: 12px; margin-bottom: 12px;">
                    <div style="font-weight: 600; margin-bottom: 10px; font-size: 13px;">📅 ${t('dateFilter')}</div>
                    
                    <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 8px;">
                        <input type="checkbox" id="mly-filterByDate" ${CONFIG.filterByDate ? 'checked' : ''} style="margin-right: 8px;">
                        <span>${t('filterByDate')}</span>
                    </label>
                    
                    <div id="mly-date-fields" style="display: ${CONFIG.filterByDate ? 'block' : 'none'}; background: #f5f5f5; padding: 10px; border-radius: 6px;">
                        <div style="margin-bottom: 8px;">
                            <label style="font-size: 11px; display: block; margin-bottom: 4px; font-weight: 500;">${t('from')}:</label>
                            <input type="date" id="mly-dateFrom" value="${CONFIG.dateFrom}" style="width: 100%; padding: 6px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; background: white;">
                        </div>
                        <div>
                            <label style="font-size: 11px; display: block; margin-bottom: 4px; font-weight: 500;">${t('to')}:</label>
                            <input type="date" id="mly-dateTo" value="${CONFIG.dateTo}" style="width: 100%; padding: 6px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; background: white;">
                        </div>
                        <div style="margin-top: 8px; font-size: 10px; color: #666;">
                            ${t('noLimit')}
                        </div>
                    </div>
                </div>

                <div style="border-top: 1px solid #ddd; padding-top: 12px;">
                    <button id="mly-refresh" style="width: 100%; padding: 10px; background: #05CB63; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; margin-bottom: 8px;">
                        🔄 ${t('refresh')}
                    </button>
                    <button id="mly-clear-cache" style="width: 100%; padding: 8px; background: #e74c3c; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 12px;">
                        🗑️ ${t('clearCache')}
                    </button>
                </div>

                <div style="margin-top: 15px; padding-top: 12px; border-top: 1px solid #ddd; font-size: 11px; color: #888;">
                    <div style="margin-bottom: 4px;"><b>${t('shortcuts')}:</b></div>
                    <div>${t('shortcutClose')}</div>
                </div>
            </div>
        `;

        try {
            const { tabLabel, tabPane } = W.userscripts.registerSidebarTab(SCRIPT_ID);
            tabLabel.innerText = 'Mapillary';
            tabLabel.title = 'Mapillary Viewer';
            tabPane.innerHTML = tabHtml;

            // Helper-Funktionen die im tabPane suchen
            const getEl = (id) => tabPane.querySelector('#' + id);
            const setVal = (id, val) => { const el = getEl(id); if (el) el.value = val; };
            const setChecked = (id, val) => { const el = getEl(id); if (el) el.checked = val; };
            const setText = (id, val) => { const el = getEl(id); if (el) el.textContent = val; };
            
            // UI-Werte aus gespeicherter Config setzen
            setChecked('mly-enabled', CONFIG.enabled);
            setChecked('mly-showImages', CONFIG.showImages);
            setChecked('mly-showPanos', CONFIG.showPanos);
            setChecked('mly-showSigns', CONFIG.showSigns);
            setChecked('mly-showFeatures', CONFIG.showFeatures);
            setChecked('mly-showSequenceLines', CONFIG.showSequenceLines);
            setChecked('mly-filterByDate', CONFIG.filterByDate);
            
            setVal('mly-opacity', CONFIG.opacity * 100);
            setText('mly-opacity-val', Math.round(CONFIG.opacity * 100) + '%');
            
            setVal('mly-maxImages', CONFIG.maxImages);
            setText('mly-maxImages-val', CONFIG.maxImages);
            
            setVal('mly-maxSigns', CONFIG.maxSigns);
            setText('mly-maxSigns-val', CONFIG.maxSigns);
            
            setVal('mly-dateFrom', CONFIG.dateFrom || '');
            setVal('mly-dateTo', CONFIG.dateTo || '');
            
            // Schilder-Filter
            ['speed', 'stop', 'turn', 'noEntry', 'parking', 'pedestrian', 'warning', 'info'].forEach(key => {
                setChecked('mly-sign-' + key, CONFIG.signFilters?.[key] ?? true);
            });
            
            // Sichtbarkeit der Bereiche
            const signFiltersDiv = getEl('mly-sign-filters');
            if (signFiltersDiv) signFiltersDiv.style.display = CONFIG.showSigns ? 'block' : 'none';
            
            const dateFieldsDiv = getEl('mly-date-fields');
            if (dateFieldsDiv) dateFieldsDiv.style.display = CONFIG.filterByDate ? 'block' : 'none';

            // Event Listeners - auch mit getEl
            const bindCheckbox = (id, key) => {
                const el = getEl(id);
                if (el) {
                    el.onchange = () => {
                        CONFIG[key] = el.checked;
                        saveConfig();
                        if (key === 'enabled') {
                            syncLayerToggle(CONFIG.enabled);
                            if (CONFIG.enabled) createLayers();
                            else removeLayers();
                        } else if (key === 'filterByDate') {
                            getEl('mly-date-fields').style.display = CONFIG.filterByDate ? 'block' : 'none';
                            lastBbox = '';
                            refreshData();
                        } else {
                            lastBbox = '';
                            refreshData();
                        }
                    };
                }
            };

            bindCheckbox('mly-enabled', 'enabled');
            bindCheckbox('mly-showImages', 'showImages');
            bindCheckbox('mly-showPanos', 'showPanos');
            bindCheckbox('mly-showFeatures', 'showFeatures');
            bindCheckbox('mly-showSequenceLines', 'showSequenceLines');
            bindCheckbox('mly-filterByDate', 'filterByDate');

            // Schilder-Checkbox mit Sichtbarkeit des Filter-Bereichs
            const showSignsEl = getEl('mly-showSigns');
            if (showSignsEl) {
                showSignsEl.onchange = () => {
                    CONFIG.showSigns = showSignsEl.checked;
                    saveConfig();
                    getEl('mly-sign-filters').style.display = CONFIG.showSigns ? 'block' : 'none';
                    lastBbox = '';
                    refreshData();
                };
            }

            // Schilder-Filter Checkboxen
            const signFilterKeys = ['speed', 'stop', 'turn', 'noEntry', 'parking', 'pedestrian', 'warning', 'info'];
            signFilterKeys.forEach(key => {
                const el = getEl('mly-sign-' + key);
                if (el) {
                    el.onchange = () => {
                        CONFIG.signFilters[key] = el.checked;
                        saveConfig();
                        lastBbox = '';
                        refreshData();
                    };
                }
            });

            // Alle/Keine Buttons
            const signsAllBtn = getEl('mly-signs-all');
            const signsNoneBtn = getEl('mly-signs-none');
            if (signsAllBtn) {
                signsAllBtn.onclick = () => {
                    signFilterKeys.forEach(key => {
                        CONFIG.signFilters[key] = true;
                        const el = getEl('mly-sign-' + key);
                        if (el) el.checked = true;
                    });
                    saveConfig();
                    lastBbox = '';
                    refreshData();
                };
            }
            if (signsNoneBtn) {
                signsNoneBtn.onclick = () => {
                    signFilterKeys.forEach(key => {
                        CONFIG.signFilters[key] = false;
                        const el = getEl('mly-sign-' + key);
                        if (el) el.checked = false;
                    });
                    saveConfig();
                    lastBbox = '';
                    refreshData();
                };
            }

            const opacityEl = getEl('mly-opacity');
            const opacityValEl = getEl('mly-opacity-val');
            if (opacityEl) {
                opacityEl.oninput = () => {
                    CONFIG.opacity = opacityEl.value / 100;
                    if (opacityValEl) opacityValEl.textContent = opacityEl.value + '%';
                    saveConfig();
                    // Update layer styles
                    [imageLayer, signLayer, featureLayer].forEach(l => {
                        if (l?.styleMap?.styles?.default?.defaultStyle) {
                            l.styleMap.styles.default.defaultStyle.fillOpacity = CONFIG.opacity;
                            l.redraw();
                        }
                    });
                };
            }

            // Max Images Slider
            const maxImagesEl = getEl('mly-maxImages');
            const maxImagesValEl = getEl('mly-maxImages-val');
            if (maxImagesEl) {
                maxImagesEl.oninput = () => {
                    CONFIG.maxImages = parseInt(maxImagesEl.value);
                    if (maxImagesValEl) maxImagesValEl.textContent = CONFIG.maxImages;
                };
                maxImagesEl.onchange = () => {
                    saveConfig();
                    lastBbox = '';
                    refreshData();
                };
            }

            // Max Signs Slider
            const maxSignsEl = getEl('mly-maxSigns');
            const maxSignsValEl = getEl('mly-maxSigns-val');
            if (maxSignsEl) {
                maxSignsEl.oninput = () => {
                    CONFIG.maxSigns = parseInt(maxSignsEl.value);
                    if (maxSignsValEl) maxSignsValEl.textContent = CONFIG.maxSigns;
                };
                maxSignsEl.onchange = () => {
                    saveConfig();
                    lastBbox = '';
                    refreshData();
                };
            }

            const dateFromEl = getEl('mly-dateFrom');
            const dateToEl = getEl('mly-dateTo');
            if (dateFromEl) {
                dateFromEl.onchange = () => {
                    CONFIG.dateFrom = dateFromEl.value;
                    saveConfig();
                    if (CONFIG.filterByDate) { lastBbox = ''; refreshData(); }
                };
            }
            if (dateToEl) {
                dateToEl.onchange = () => {
                    CONFIG.dateTo = dateToEl.value;
                    saveConfig();
                    if (CONFIG.filterByDate) { lastBbox = ''; refreshData(); }
                };
            }

            const refreshBtn = getEl('mly-refresh');
            if (refreshBtn) {
                refreshBtn.onclick = () => {
                    lastBbox = '';
                    refreshData();
                };
            }

            const clearCacheBtn = getEl('mly-clear-cache');
            if (clearCacheBtn) {
                clearCacheBtn.onclick = () => {
                    cache.clear();
                    lastBbox = '';
                    setStatus(t('cacheCleared'));
                    refreshData();
                };
            }

            log('UI erstellt');
        } catch (e) {
            log('UI Fehler: ' + e.message);
        }
    }

    // ============ KEYBOARD SHORTCUTS ============
    function setupKeyboard() {
        document.addEventListener('keydown', (e) => {
            // Ignoriere wenn in Input-Feld
            if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;

            if (e.key === 'Escape') {
                closeViewer();
            }

            // Pfeiltasten für Navigation
            if (viewerDiv?.classList.contains('open') && sequenceImages.length > 0) {
                if (e.key === 'ArrowLeft') {
                    e.preventDefault();
                    navigateSequence(-1);
                }
                if (e.key === 'ArrowRight') {
                    e.preventDefault();
                    navigateSequence(1);
                }
            }
        });
        log('Tastenkürzel aktiviert');
    }

    // ============ LAYER CHECKBOX (rechte Sidebar - Andere Daten) ============
    let layerSidebarCheckbox = null;
    
    function createLayerCheckbox() {
        // Finde "Andere Daten" / "Other Data" Sektion
        function findInsertionPoint() {
            // Suche nach "Satellitenbild" Label
            const labels = document.querySelectorAll('wz-checkbox, label');
            for (const el of labels) {
                const text = el.textContent || '';
                if (/Satellitenbild|Satellite/i.test(text)) {
                    return { element: el, mode: 'after' };
                }
            }
            
            // Fallback: Suche "Andere Daten" Header
            const headers = document.querySelectorAll('wz-section-header, h3, h4');
            for (const h of headers) {
                if (/Andere Daten|Other Data/i.test(h.textContent || '')) {
                    const section = h.closest('wz-card, section, div');
                    if (section) {
                        const list = section.querySelector('wz-list, ul, div');
                        if (list) return { element: list, mode: 'prepend' };
                    }
                }
            }
            
            return null;
        }
        
        function buildCheckbox() {
            const item = document.createElement('wz-checkbox');
            item.id = 'mly-layer-toggle';
            item.checked = CONFIG.enabled;
            item.innerHTML = 'Mapillary';
            
            item.addEventListener('change', (e) => {
                CONFIG.enabled = e.target.checked;
                saveConfig();
                syncPanelCheckbox();
                
                if (CONFIG.enabled) {
                    createLayers();
                } else {
                    removeLayers();
                }
            });
            
            return item;
        }
        
        function mount() {
            // Prüfe ob schon vorhanden
            if (document.getElementById('mly-layer-toggle')) return true;
            
            const target = findInsertionPoint();
            if (!target) return false;
            
            const checkbox = buildCheckbox();
            layerSidebarCheckbox = checkbox;
            
            if (target.mode === 'after') {
                target.element.insertAdjacentElement('afterend', checkbox);
            } else if (target.mode === 'prepend') {
                target.element.insertBefore(checkbox, target.element.firstChild);
            } else {
                target.element.appendChild(checkbox);
            }
            
            log('Layer-Toggle in Sidebar eingefügt');
            return true;
        }
        
        // Versuche sofort zu mounten
        if (!mount()) {
            // Falls nicht gefunden, beobachte DOM-Änderungen
            const observer = new MutationObserver(() => {
                if (mount()) observer.disconnect();
            });
            observer.observe(document.body, { childList: true, subtree: true });
            
            // Timeout nach 30 Sekunden
            setTimeout(() => observer.disconnect(), 30000);
        }
    }
    
    // Sync Panel-Checkbox mit Sidebar-Checkbox
    function syncPanelCheckbox() {
        const panelCheckbox = document.getElementById('mly-enabled');
        if (panelCheckbox) panelCheckbox.checked = CONFIG.enabled;
    }
    
    // Sync Sidebar-Checkbox mit Panel-Checkbox
    function syncLayerToggle(enabled) {
        if (layerSidebarCheckbox) layerSidebarCheckbox.checked = enabled;
    }

    // ============ INIT ============
    function init() {
        // Sprache erkennen
        currentLang = detectLanguage();
        log(`Language: ${currentLang}`);
        
        loadConfig();
        log(`v${SCRIPT_VERSION} gestartet`);

        createUI();
        createLayerCheckbox();
        setupClickHandler();
        setupKeyboard();

        if (CONFIG.enabled) {
            createLayers();
        }

        setStatus(t('ready'));
    }

    // Warte auf WME
    if (W?.userscripts?.state?.isReady) {
        init();
    } else {
        document.addEventListener('wme-ready', init, { once: true });
    }

})();