google map scraper

google map result

Version vom 02.06.2025. Aktuellste Version

// ==UserScript==
// @name         google map scraper
// @namespace    http://google.com/
// @version      1.0.1
// @description  google map result
// @author       Web Automation Lover
// @match        *://*.google.com/maps/search/*/*
// @match        *://*.google.ad/maps/search/**
// @match        *://*.google.ae/maps/search/**
// @match        *://*.google.ac/maps/search/**
// @match        *://*.google.com.ag/maps/search/**
// @match        *://*.google.com.ai/maps/search/**
// @match        *://*.google.com.af/maps/search/**
// @match        *://*.google.al/maps/search/**
// @match        *://*.google.am/maps/search/**
// @match        *://*.google.co.ao/maps/search/**
// @match        *://*.google.at/maps/search/**
// @match        *://*.google.com.ar/maps/search/**
// @match        *://*.google.as/maps/search/**
// @match        *://*.google.com.au/maps/search/**
// @match        *://*.google.com.bd/maps/search/**
// @match        *://*.google.az/maps/search/**
// @match        *://*.google.ba/maps/search/**
// @match        *://*.google.bg/maps/search/**
// @match        *://*.google.be/maps/search/**
// @match        *://*.google.bf/maps/search/**
// @match        *://*.google.com.bh/maps/search/**
// @match        *://*.google.com.bn/maps/search/**
// @match        *://*.google.bi/maps/search/**
// @match        *://*.google.bj/maps/search/**
// @match        *://*.google.bs/maps/search/**
// @match        *://*.google.com.bo/maps/search/**
// @match        *://*.google.com.br/maps/search/**
// @match        *://*.google.bt/maps/search/**
// @match        *://*.google.co.bw/maps/search/**
// @match        *://*.google.by/maps/search/**
// @match        *://*.google.com.bz/maps/search/**
// @match        *://*.google.ca/maps/search/**
// @match        *://*.google.com.kh/maps/search/**
// @match        *://*.google.cc/maps/search/**
// @match        *://*.google.cd/maps/search/**
// @match        *://*.google.cf/maps/search/**
// @match        *://*.google.cat/maps/search/**
// @match        *://*.google.cg/maps/search/**
// @match        *://*.google.ch/maps/search/**
// @match        *://*.google.ci/maps/search/**
// @match        *://*.google.co.ck/maps/search/**
// @match        *://*.google.cl/maps/search/**
// @match        *://*.google.cm/maps/search/**
// @match        *://*.google.cn/maps/search/**
// @match        *://*.google.com.co/maps/search/**
// @match        *://*.google.co.cr/maps/search/**
// @match        *://*.google.com.cu/maps/search/**
// @match        *://*.google.cv/maps/search/**
// @match        *://*.google.com.cy/maps/search/**
// @match        *://*.google.cz/maps/search/**
// @match        *://*.google.de/maps/search/**
// @match        *://*.google.dj/maps/search/**
// @match        *://*.google.dk/maps/search/**
// @match        *://*.google.dm/maps/search/**
// @match        *://*.google.com.do/maps/search/**
// @match        *://*.google.dz/maps/search/**
// @match        *://*.google.com.ec/maps/search/**
// @match        *://*.google.ee/maps/search/**
// @match        *://*.google.com.eg/maps/search/**
// @match        *://*.google.es/maps/search/**
// @match        *://*.google.com.et/maps/search/**
// @match        *://*.google.fi/maps/search/**
// @match        *://*.google.com.fj/maps/search/**
// @match        *://*.google.fm/maps/search/**
// @match        *://*.google.fr/maps/search/**
// @match        *://*.google.ga/maps/search/**
// @match        *://*.google.ge/maps/search/**
// @match        *://*.google.gf/maps/search/**
// @match        *://*.google.gg/maps/search/**
// @match        *://*.google.com.gh/maps/search/**
// @match        *://*.google.com.gi/maps/search/**
// @match        *://*.google.gl/maps/search/**
// @match        *://*.google.gm/maps/search/**
// @match        *://*.google.gp/maps/search/**
// @match        *://*.google.gr/maps/search/**
// @match        *://*.google.com.gt/maps/search/**
// @match        *://*.google.gy/maps/search/**
// @match        *://*.google.com.hk/maps/search/**
// @match        *://*.google.hn/maps/search/**
// @match        *://*.google.hr/maps/search/**
// @match        *://*.google.ht/maps/search/**
// @match        *://*.google.hu/maps/search/**
// @match        *://*.google.co.id/maps/search/**
// @match        *://*.google.iq/maps/search/**
// @match        *://*.google.ie/maps/search/**
// @match        *://*.google.co.il/maps/search/**
// @match        *://*.google.im/maps/search/**
// @match        *://*.google.co.in/maps/search/**
// @match        *://*.google.io/maps/search/**
// @match        *://*.google.is/maps/search/**
// @match        *://*.google.it/maps/search/**
// @match        *://*.google.je/maps/search/**
// @match        *://*.google.com.jm/maps/search/**
// @match        *://*.google.jo/maps/search/**
// @match        *://*.google.co.jp/maps/search/**
// @match        *://*.google.co.ke/maps/search/**
// @match        *://*.google.ki/maps/search/**
// @match        *://*.google.kg/maps/search/**
// @match        *://*.google.co.kr/maps/search/**
// @match        *://*.google.com.kw/maps/search/**
// @match        *://*.google.kz/maps/search/**
// @match        *://*.google.la/maps/search/**
// @match        *://*.google.com.lb/maps/search/**
// @match        *://*.google.com.lc/maps/search/**
// @match        *://*.google.li/maps/search/**
// @match        *://*.google.lk/maps/search/**
// @match        *://*.google.co.ls/maps/search/**
// @match        *://*.google.lt/maps/search/**
// @match        *://*.google.lu/maps/search/**
// @match        *://*.google.lv/maps/search/**
// @match        *://*.google.com.ly/maps/search/**
// @match        *://*.google.co.ma/maps/search/**
// @match        *://*.google.md/maps/search/**
// @match        *://*.google.me/maps/search/**
// @match        *://*.google.mg/maps/search/**
// @match        *://*.google.mk/maps/search/**
// @match        *://*.google.ml/maps/search/**
// @match        *://*.google.com.mm/maps/search/**
// @match        *://*.google.mn/maps/search/**
// @match        *://*.google.ms/maps/search/**
// @match        *://*.google.com.mt/maps/search/**
// @match        *://*.google.mu/maps/search/**
// @match        *://*.google.mv/maps/search/**
// @match        *://*.google.mw/maps/search/**
// @match        *://*.google.com.mx/maps/search/**
// @match        *://*.google.com.my/maps/search/**
// @match        *://*.google.co.mz/maps/search/**
// @match        *://*.google.com.na/maps/search/**
// @match        *://*.google.ne/maps/search/**
// @match        *://*.google.com.nf/maps/search/**
// @match        *://*.google.com.ng/maps/search/**
// @match        *://*.google.com.ni/maps/search/**
// @match        *://*.google.nl/maps/search/**
// @match        *://*.google.no/maps/search/**
// @match        *://*.google.com.np/maps/search/**
// @match        *://*.google.nr/maps/search/**
// @match        *://*.google.nu/maps/search/**
// @match        *://*.google.co.nz/maps/search/**
// @match        *://*.google.com.om/maps/search/**
// @match        *://*.google.com.pk/maps/search/**
// @match        *://*.google.com.pa/maps/search/**
// @match        *://*.google.com.pe/maps/search/**
// @match        *://*.google.com.ph/maps/search/**
// @match        *://*.google.pl/maps/search/**
// @match        *://*.google.com.pg/maps/search/**
// @match        *://*.google.pn/maps/search/**
// @match        *://*.google.com.pr/maps/search/**
// @match        *://*.google.ps/maps/search/**
// @match        *://*.google.pt/maps/search/**
// @match        *://*.google.com.py/maps/search/**
// @match        *://*.google.com.qa/maps/search/**
// @match        *://*.google.ro/maps/search/**
// @match        *://*.google.rs/maps/search/**
// @match        *://*.google.ru/maps/search/**
// @match        *://*.google.rw/maps/search/**
// @match        *://*.google.com.sa/maps/search/**
// @match        *://*.google.com.sb/maps/search/**
// @match        *://*.google.sc/maps/search/**
// @match        *://*.google.co.th/maps/search/**
// @match        *://*.google.com.tj/maps/search/**
// @match        *://*.google.tk/maps/search/**
// @match        *://*.google.tl/maps/search/**
// @match        *://*.google.tm/maps/search/**
// @match        *://*.google.to/maps/search/**
// @match        *://*.google.tn/maps/search/**
// @match        *://*.google.com.tr/maps/search/**
// @match        *://*.google.tt/maps/search/**
// @match        *://*.google.com.tw/maps/search/**
// @match        *://*.google.co.tz/maps/search/**
// @match        *://*.google.se/maps/search/**
// @match        *://*.google.com.sg/maps/search/**
// @match        *://*.google.sh/maps/search/**
// @match        *://*.google.si/maps/search/**
// @match        *://*.google.sk/maps/search/**
// @match        *://*.google.com.sl/maps/search/**
// @match        *://*.google.sn/maps/search/**
// @match        *://*.google.sm/maps/search/**
// @match        *://*.google.so/maps/search/**
// @match        *://*.google.st/maps/search/**
// @match        *://*.google.sr/maps/search/**
// @match        *://*.google.com.sv/maps/search/**
// @match        *://*.google.td/maps/search/**
// @match        *://*.google.tg/maps/search/**
// @match        *://*.google.com.ua/maps/search/**
// @match        *://*.google.co.ug/maps/search/**
// @match        *://*.google.co.uk/maps/search/**
// @match        *://*.google.com/maps/search/**
// @match        *://*.google.com.uy/maps/search/**
// @match        *://*.google.co.uz/maps/search/**
// @match        *://*.google.com.vc/maps/search/**
// @match        *://*.google.co.ve/maps/search/**
// @match        *://*.google.vg/maps/search/**
// @match        *://*.google.co.vi/maps/search/**
// @match        *://*.google.com.vn/maps/search/**
// @match        *://*.google.vu/maps/search/**
// @match        *://*.google.ws/maps/search/**
// @match        *://*.google.co.za/maps/search/**
// @match        *://*.google.co.zm/maps/search/**
// @match        *://*.google.co.zw/maps/search/**
// @supportURL   https://github.com/webAutomationLover/google-map-scraper/issues
// @homepageURL  https://github.com/webAutomationLover/google-map-scraper
// @icon         https://www.google.com/s2/favicons?sz=64&domain=xiaohongshu.com
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @resource     bootstrapCSS https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css
// @require      https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.16.9/xlsx.full.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @require      https://code.jquery.com/jquery-3.5.1.min.js
// @require      https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/js/bootstrap.min.js
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    class DataManager {
        constructor() {
            this.data = new Map();
            this.errorLog = [];
            this.defaultFields = new Set([
                'name', 'fullAddress', 'phones', 'website',
                'averageRating', 'reviewCount', 'categories'
            ]);
            this.selectedFields = new Set(this.defaultFields);
            this.loadSelectedFields();
            this.validationRules = {
                name: { required: true, minLength: 1 },
                fullAddress: { required: true, minLength: 5 },
                phones: { pattern: /^[0-9+\s-]+$/ },
                website: { pattern: /^https?:\/\/.+/ },
                averageRating: { min: 0, max: 5 },
                reviewCount: { min: 0 }
            };
        }

        loadSelectedFields() {
            const savedFields = localStorage.getItem('googleMapsScraperSelectedFields');
            if (savedFields) {
                this.selectedFields = new Set(JSON.parse(savedFields));
            }
        }

        saveSelectedFields() {
            localStorage.setItem('googleMapsScraperSelectedFields',
                JSON.stringify(Array.from(this.selectedFields)));
        }

        addItem(item) {
            if (!item || !item.placeId) {
                console.log('Skipping item: Missing placeId');
                return;
            }

            const isDuplicate = this.data.has(item.placeId);
            if (isDuplicate) {
                console.log(`Duplicate record found - placeId: ${item.placeId}, name: ${item.name}`);
            } else {
                console.log(`New record added - placeId: ${item.placeId}, name: ${item.name}`);
            }

            this.data.set(item.placeId, item);
        }

        getItems() {
            return Array.from(this.data.values());
        }

        getCount() {
            return this.data.size;
        }

        logError(error) {
            this.errorLog.push({
                timestamp: new Date().toISOString(),
                error: error.message || error
            });
            console.error('Error:', error);
        }

        getErrors() {
            return this.errorLog;
        }

        getSelectedFields() {
            return Array.from(this.selectedFields);
        }

        setSelectedFields(fields) {
            this.selectedFields = new Set(fields);
            this.saveSelectedFields();
        }

        getPreviewData(limit = 5) {
            return this.getItems().slice(0, limit);
        }

        validateItem(item) {
            const errors = [];
            Object.entries(this.validationRules).forEach(([field, rules]) => {
                const value = item[field];

                if (rules.required && (!value || value.length === 0)) {
                    errors.push(`${field} is required`);
                }

                if (value) {
                    if (rules.minLength && value.length < rules.minLength) {
                        errors.push(`${field} length cannot be less than ${rules.minLength}`);
                    }
                    if (rules.pattern && !rules.pattern.test(value)) {
                        errors.push(`${field} format is incorrect`);
                    }
                    if (rules.min !== undefined && value < rules.min) {
                        errors.push(`${field} cannot be less than ${rules.min}`);
                    }
                    if (rules.max !== undefined && value > rules.max) {
                        errors.push(`${field} cannot be greater than ${rules.max}`);
                    }
                }
            });
            return errors;
        }

        cleanData() {
            const cleanedData = new Map();
            this.data.forEach((item, key) => {
                const cleanedItem = { ...item };
                // Clean phone number format
                if (cleanedItem.phones) {
                    cleanedItem.phones = cleanedItem.phones.replace(/[^\d+]/g, '');
                }
                // Clean website format
                if (cleanedItem.website && !cleanedItem.website.startsWith('http')) {
                    cleanedItem.website = 'https://' + cleanedItem.website;
                }
                // Clean rating
                if (cleanedItem.averageRating) {
                    cleanedItem.averageRating = Math.min(5, Math.max(0, parseFloat(cleanedItem.averageRating)));
                }
                cleanedData.set(key, cleanedItem);
            });
            this.data = cleanedData;
        }

        prepareData() {
            this.cleanData();
            const data = this.getItems().map(item => {
                const filteredItem = {};
                this.getSelectedFields().forEach(field => {
                    filteredItem[field] = item[field];
                });
                return filteredItem;
            });

            const validationErrors = [];
            data.forEach((item, index) => {
                const errors = this.validateItem(item);
                if (errors.length > 0) {
                    validationErrors.push({
                        index,
                        item: item.name || `Item ${index}`,
                        errors
                    });
                }
            });

            if (validationErrors.length > 0) {
                console.warn('Data validation warnings:', validationErrors);
                this.logError({
                    type: 'validation',
                    errors: validationErrors
                });
            }

            return data;
        }

        resetToDefaultFields() {
            this.selectedFields = new Set(this.defaultFields);
            this.saveSelectedFields();
        }
    }

    class ConfigManager {
        constructor() {
            this.config = {
                scrollSpeed: 1000,
                scrollInterval: {
                    min: 5,
                    max: 10
                }
            };
            this.loadConfig();
            this.onConfigChange = null;
        }

        loadConfig() {
            const savedConfig = localStorage.getItem('googleMapsScraperConfig');
            if (savedConfig) {
                this.config = { ...this.config, ...JSON.parse(savedConfig) };
            }
        }

        saveConfig() {
            localStorage.setItem('googleMapsScraperConfig', JSON.stringify(this.config));
        }

        updateConfig(newConfig) {
            this.config = { ...this.config, ...newConfig };
            this.saveConfig();
            if (this.onConfigChange) {
                this.onConfigChange(this.config);
            }
        }
    }

    class AutoScrollManager {
        constructor() {
            this.scrollTimer = null;
            this.countdownTimer = null;
            this.isLoading = false;
            this.createCountdownDisplay();
        }

        createCountdownDisplay() {
            this.countdownDisplay = $('<div class="countdown-display" style="display: none; position: fixed; top: 10px; right: 10px; background: rgba(0,0,0,0.7); color: white; padding: 5px 10px; border-radius: 4px; font-size: 12px; z-index: 9999;"></div>');
            $('body').append(this.countdownDisplay);
        }

        startCountdown(seconds) {
            if (this.countdownTimer) {
                clearInterval(this.countdownTimer);
            }

            this.countdownDisplay.show();
            let remainingSeconds = seconds;

            const updateDisplay = () => {
                this.countdownDisplay.text(`Next scroll in ${remainingSeconds}s`);
                if (remainingSeconds <= 0) {
                    clearInterval(this.countdownTimer);
                    this.countdownTimer = null;
                }
                remainingSeconds--;
            };

            updateDisplay();
            this.countdownTimer = setInterval(updateDisplay, 1000);
        }

        getRandomInteger(min, max) {
            return Math.floor(Math.random() * (max - min + 1)) + min;
        }

        checkIfReachedEnd() {
            const elScroll = document.querySelector('[role="feed"]');
            if (!elScroll) return false;

            const lastChild = elScroll.lastElementChild;
            return lastChild && lastChild.getAttribute('style')?.includes('height: 64px');
        }

        scroll() {
            const elScroll = document.querySelector('[role="feed"]');
            if (!elScroll) {
                console.warn("Scrollable element not found. Stopping auto-scroll.");
                this.stop();
                return;
            }

            // 每次滚动时重新读取配置
            const config = configManager.config;
            elScroll.scrollBy({
                top: config.scrollSpeed,
                behavior: 'smooth'
            });

            if (this.checkIfReachedEnd()) {
                console.log("Reached end of results.");
                this.stop();
                return;
            }

            // 使用最新的配置计算下一次滚动的时间间隔
            const nextInterval = this.getRandomInteger(
                config.scrollInterval.min,
                config.scrollInterval.max
            );
            this.startCountdown(nextInterval);
            this.scrollTimer = setTimeout(() => this.scroll(), nextInterval * 1000);
        }

        start() {
            if (this.isLoading) return;

            // Check if already at the end
            if (this.checkIfReachedEnd()) {
                alert('Already reached the end of results. Please try a new search or move the map to load more results.');
                return;
            }

            this.isLoading = true;
            console.log('Starting auto scroll with config:', configManager.config);

            // Update button states
            startAutoScrollButton.html('Stop Auto Scroll');
            startAutoScrollButton.removeClass('btn-secondary').addClass('btn-danger');
            bsButton.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Export Data (' + dataManager.getCount() + ')');

            // 使用最新的配置开始第一次滚动
            const config = configManager.config;
            const firstInterval = this.getRandomInteger(
                config.scrollInterval.min,
                config.scrollInterval.max
            );
            this.startCountdown(firstInterval);
            this.scrollTimer = setTimeout(() => this.scroll(), firstInterval * 1000);
        }

        stop() {
            if (this.scrollTimer) {
                clearTimeout(this.scrollTimer);
                this.scrollTimer = null;
            }
            if (this.countdownTimer) {
                clearInterval(this.countdownTimer);
                this.countdownTimer = null;
            }
            this.isLoading = false;
            this.countdownDisplay.hide();

            // Reset button states
            startAutoScrollButton.html('Start Auto Scroll');
            startAutoScrollButton.removeClass('btn-danger').addClass('btn-secondary');
            bsButton.prop('disabled', false).html('Export Data (' + dataManager.getCount() + ')');
        }
    }

    class ExportManager {
        constructor(dataManager) {
            this.dataManager = dataManager;
        }

        getFileName(extension) {
            const now = new Date();
            const timestamp = now.getFullYear() + '-' +
                String(now.getMonth() + 1).padStart(2, '0') + '-' +
                String(now.getDate()).padStart(2, '0') + '-' +
                String(now.getHours()).padStart(2, '0') + ':' +
                String(now.getMinutes()).padStart(2, '0') + ':' +
                String(now.getSeconds()).padStart(2, '0');
            return `google_maps_data_${timestamp}.${extension}`;
        }

        exportToXLSX() {
            try {
                const data = this.dataManager.prepareData();
                const ws = XLSX.utils.json_to_sheet(data);
                const wb = XLSX.utils.book_new();
                XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');

                if (this.dataManager.getErrors().length > 0) {
                    const errorWs = XLSX.utils.json_to_sheet(this.dataManager.getErrors());
                    XLSX.utils.book_append_sheet(wb, errorWs, 'Errors');
                }

                // Convert to binary string
                const wbout = XLSX.write(wb, {
                    bookType: 'xlsx',
                    type: 'binary',
                    bookSST: true
                });

                // Convert binary string to array buffer
                const buf = new ArrayBuffer(wbout.length);
                const view = new Uint8Array(buf);
                for (let i = 0; i < wbout.length; i++) {
                    view[i] = wbout.charCodeAt(i) & 0xFF;
                }

                // Create blob and download
                const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
                this.downloadFile(blob, this.getFileName('xlsx'), 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
            } catch (error) {
                this.dataManager.logError(error);
                alert('Error exporting to XLSX. Please check console for details.');
            }
        }

        exportToCSV() {
            try {
                const data = this.dataManager.prepareData();
                const ws = XLSX.utils.json_to_sheet(data);
                const csv = XLSX.utils.sheet_to_csv(ws);
                this.downloadFile(csv, this.getFileName('csv'), 'text/csv');
            } catch (error) {
                this.dataManager.logError(error);
                alert('Error exporting to CSV. Please check console for details.');
            }
        }

        exportToJSON() {
            try {
                const data = this.dataManager.prepareData();
                const json = JSON.stringify(data, null, 2);
                this.downloadFile(json, this.getFileName('json'), 'application/json');
            } catch (error) {
                this.dataManager.logError(error);
                alert('Error exporting to JSON. Please check console for details.');
            }
        }

        downloadFile(content, filename, mimeType) {
            const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType });
            const a = document.createElement('a');
            a.href = URL.createObjectURL(blob);
            a.download = filename;
            a.click();
            URL.revokeObjectURL(a.href);
        }
    }

    const dataManager = new DataManager();
    const configManager = new ConfigManager();
    const autoScrollManager = new AutoScrollManager();
    const exportManager = new ExportManager(dataManager);

    window.jsonArr = [];

    GM_addStyle(GM_getResourceText("bootstrapCSS"));

    const modalHTML = `
        <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title" id="myModalLabel">Bootstrap Modal</h5>
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                            <span aria-hidden="true">×</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        This is the content of the Bootstrap modal.
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                        <button type="button" class="btn btn-primary">Save changes</button>
                    </div>
                </div>
            </div>
        </div>
    `;
    $('body').append(modalHTML);

    const bsButton = $('<button type="button" class="btn btn-primary">Export Data (0)</button>');
    const startAutoScrollButton = $('<button type="button" class="btn btn-secondary ml-2" id="start-scroll-button">Start Auto Scroll</button>');
    const configButton = $('<button type="button" class="btn btn-outline-secondary ml-2" id="config-button"><i class="fas fa-cog"></i></button>');

    bsButton.click(function() {
        updateFieldSelector();
        updatePreviewTable();
        $('#previewModal').modal('show');
    });

    function updateButtonText() {
        bsButton.text('Export Data (' + dataManager.getCount() + ')');
    }

    startAutoScrollButton.click(() => {
        console.log('Start auto scroll button clicked');
        if (autoScrollManager.isLoading) {
            autoScrollManager.stop();
        } else {
            autoScrollManager.start();
        }
    });

    // Create configuration panel
    const configModalHTML = `
        <div class="modal fade" id="configModal" tabindex="-1" role="dialog">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title">Settings</h5>
                        <button type="button" class="close" data-dismiss="modal">
                            <span>&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <form id="configForm">
                            <div class="form-group">
                                <label>Scroll Speed (pixels/second)</label>
                                <input type="number" class="form-control" id="scrollSpeed" 
                                       value="${configManager.config.scrollSpeed}">
                            </div>
                            <div class="form-group">
                                <label>Scroll Interval (seconds)</label>
                                <small class="form-text text-muted mb-2">
                                    Set a range for random interval between scrolls. The script will randomly choose a value between min and max.
                                </small>
                                <div class="row">
                                    <div class="col">
                                        <input type="number" class="form-control" id="scrollIntervalMin" 
                                               placeholder="Min" value="${configManager.config.scrollInterval.min}">
                                    </div>
                                    <div class="col">
                                        <input type="number" class="form-control" id="scrollIntervalMax" 
                                               placeholder="Max" value="${configManager.config.scrollInterval.max}">
                                    </div>
                                </div>
                            </div>
                        </form>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                        <button type="button" class="btn btn-primary" id="saveConfig">Save Settings</button>
                    </div>
                </div>
            </div>
        </div>
    `;

    $('body').append(configModalHTML);

    // Add configuration button
    configButton.click(() => {
        console.log('Config button clicked');
        try {
            if ($('#configModal').length === 0) {
                console.error('Config modal not found in DOM');
                return;
            }
            $('#configModal').modal('show');
        } catch (error) {
            console.error('Error showing config modal:', error);
            alert('Unable to open settings panel. Please refresh the page and try again.');
        }
    });

    // Save configuration
    $('#saveConfig').click(() => {
        const newConfig = {
            scrollSpeed: parseInt($('#scrollSpeed').val()),
            scrollInterval: {
                min: parseInt($('#scrollIntervalMin').val()),
                max: parseInt($('#scrollIntervalMax').val())
            }
        };

        configManager.updateConfig(newConfig);

        $('#configModal').modal('hide');
    });

    // Modify button injection function
    const injectButton = () => {
        const targetDiv = document.querySelectorAll('#assistive-chips > div > div > div > div > div > div > div > div > div')[1];
        if (targetDiv && !document.querySelector('#my-custom-button')) {
            console.log('Injecting buttons');
            const bsButtonDOM = bsButton[0];
            bsButtonDOM.id = 'my-custom-button';
            bsButtonDOM.style.marginLeft = '20px';

            const parentDiv = targetDiv;
            parentDiv.appendChild(bsButtonDOM);
            parentDiv.appendChild(startAutoScrollButton[0]);
            parentDiv.appendChild(configButton[0]);
        }
    };

    // Add debug logs
    const observer = new MutationObserver(mutations => {
        console.log('DOM mutation detected');
        injectButton();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: false,
        characterData: false
    });

    // Modify XHR to capture data
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;

    XMLHttpRequest.prototype.open = function(method, url) {
        this._url = url;
        return originalOpen.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = function() {
        this.addEventListener('load', function() {
            if (this._url.includes('/search?tbm=map')) {
                try {
                    var rspJson = JSON.parse(this.responseText.replace(`/*""*/`,""));
                    var e = rspJson.d;
                    var cleanedData = e.replace(`)]}'`, "");

                    let parsedData = JSON.parse(cleanedData);
                    let dataList = parsedData[0][1];

                    let filteredData = dataList.filter(item => {
                        return item?.[14] !== undefined;
                    });

                    if (!filteredData || filteredData.length < 1) {
                        filteredData = parsedData[64];
                    }

                    if (filteredData) {
                        var formatedData = formatAllData(filteredData);
                        formatedData.forEach(item => dataManager.addItem(item));
                        console.log('Total items:', dataManager.getCount());
                        // Update export button with loading icon if auto-scroll is active
                        if (autoScrollManager.isLoading) {
                            bsButton.html('<i class="fas fa-spinner fa-spin"></i> Export Data (' + dataManager.getCount() + ')');
                        } else {
                            updateButtonText();
                        }
                    }
                } catch (error) {
                    dataManager.logError(error);
                }
            }
        });

        return originalSend.apply(this, arguments);
    };

    // Format data for export
    function formatAllData(allDataList) {
        return allDataList.map(d => formatDataItem(d)).filter(d => d.name);
    }

    // Format individual data item
    function formatDataItem(item) {
        const fieldConfig = {
            fullAddress: [39],
            placeId: [78],
            kgmid: [89],
            categories: [13],
            feature: [32, 0, 1],
            cid: [10],
            featuredImage: [37, 0, 0, 6, 0],
            phones: [],
            icon: [122, 0, 1],
            name: [11],
            latitude: [9, 2],
            longitude: [9, 3],
            reviewCount: [4, 8],
            reviewURL: [4, 3, 0],
            averageRating: [4, 7],
            street: [183, 0, 0, 1, 1],
            municipality: [183, 1, 3],
            openingHours: [],
            website: [7, 0],
            domain: [7, 1]
        };

        const resultData = {};
        Object.keys(fieldConfig).forEach(key => {
            resultData[key] = handleSingleField(fieldConfig[key]);
        });

        // Process special fields
        resultData.phones = handleSingleField([178, 0, 1])?.map(d => d?.[0]);
        resultData.openingHours = handleSingleField([34, 1])?.map(d => [`${d[0]}:[${d[1]}]`])?.join(', ');
        resultData.googleMapsURL = "https://www.google.com/maps?cid=".concat(resultData.cid);
        resultData.googleKnowledgeURL = "https://www.google.com/maps/search/*?kgmid=".concat(resultData.kgmid, "&kponly");

        // Format array fields
        resultData.phones = resultData.phones?.join?.(', ');
        resultData.categories = resultData.categories?.join?.(', ');
        resultData.street = resultData.street?.join?.(', ');

        function handleSingleField(config) {
            const itemData = item[1];
            if (!itemData) {
                return;
            }
            if (!config || !config.length) {
                return;
            }
            let currentData = itemData;
            for (let i = 0; i < config.length; i++) {
                currentData = currentData?.[config[i]];
            }
            return currentData;
        }

        return resultData;
    }

    // Create preview modal
    const previewModalHTML = `
        <div class="modal fade" id="previewModal" tabindex="-1" role="dialog">
            <div class="modal-dialog modal-lg" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title">Data Preview</h5>
                        <button type="button" class="close" data-dismiss="modal">
                            <span>&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <div class="form-group">
                            <div class="d-flex justify-content-between align-items-center mb-2">
                                <label class="mb-0">Select Export Fields:</label>
                                <div>
                                    <button type="button" class="btn btn-sm btn-outline-secondary mr-2" id="selectAllFields">Select All</button>
                                    <button type="button" class="btn btn-sm btn-outline-secondary" id="resetFields">Reset</button>
                                </div>
                            </div>
                            <div id="fieldSelector" class="d-flex flex-wrap">
                            </div>
                        </div>
                        <div class="table-responsive">
                            <table class="table table-striped">
                                <thead id="previewTableHead"></thead>
                                <tbody id="previewTableBody"></tbody>
                            </table>
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                        <button type="button" class="btn btn-danger" id="clearData">Clear Data</button>
                        <button type="button" class="btn btn-success" id="exportXLSX">Export XLSX</button>
                        <button type="button" class="btn btn-info" id="exportCSV">Export CSV</button>
                        <button type="button" class="btn btn-warning" id="exportJSON">Export JSON</button>
                    </div>
                </div>
            </div>
        </div>
    `;

    // Create confirmation modal for clearing data
    const confirmModalHTML = `
        <div class="modal fade" id="confirmClearModal" tabindex="-1" role="dialog">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title">Confirm Clear Data</h5>
                        <button type="button" class="close" data-dismiss="modal">
                            <span>&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <p>Are you sure you want to clear all collected data? This action cannot be undone.</p>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
                        <button type="button" class="btn btn-danger" id="confirmClear">Clear All Data</button>
                    </div>
                </div>
            </div>
        </div>
    `;

    $('body').append(previewModalHTML);
    $('body').append(confirmModalHTML);

    function updatePreviewTable() {
        const previewData = dataManager.getPreviewData();
        const fields = dataManager.getSelectedFields();

        // Update table header
        const thead = $('#previewTableHead');
        thead.empty();
        const headerRow = $('<tr>');
        fields.forEach(field => {
            headerRow.append(`<th>${field}</th>`);
        });
        thead.append(headerRow);

        // Update table content
        const tbody = $('#previewTableBody');
        tbody.empty();
        previewData.forEach(item => {
            const row = $('<tr>');
            fields.forEach(field => {
                row.append(`<td>${item[field] || ''}</td>`);
            });
            tbody.append(row);
        });
    }

    function updateFieldSelector() {
        const fieldSelector = $('#fieldSelector');
        fieldSelector.empty();

        const allFields = [
            'name', 'fullAddress', 'phones', 'website', 'averageRating',
            'reviewCount', 'categories', 'featuredImage', 'latitude',
            'longitude', 'street', 'municipality', 'openingHours',
            'placeId', 'kgmid', 'feature', 'cid', 'icon', 'reviewURL',
            'domain', 'googleMapsURL', 'googleKnowledgeURL'
        ];

        allFields.forEach(field => {
            const div = $(`
                <div class="custom-control custom-checkbox mr-3 mb-2">
                    <input type="checkbox" class="custom-control-input" id="field_${field}" 
                           ${dataManager.selectedFields.has(field) ? 'checked' : ''}>
                    <label class="custom-control-label" for="field_${field}">${field}</label>
                </div>
            `);
            fieldSelector.append(div);
        });

        // Add field selection event listener
        $('.custom-control-input').change(function() {
            const field = $(this).attr('id').replace('field_', '');
            if (this.checked) {
                dataManager.selectedFields.add(field);
                dataManager.saveSelectedFields();
                updatePreviewTable();
            } else {
                // Check if this is the last selected field
                if (dataManager.selectedFields.size <= 1) {
                    // Prevent unchecking the last field
                    $(this).prop('checked', true);
                    alert('至少需要选择一个导出字段');
                    return;
                }
                dataManager.selectedFields.delete(field);
                dataManager.saveSelectedFields();
                updatePreviewTable();
            }
        });

        // Add select all button event listener
        $('#selectAllFields').off('click').on('click', () => {
            $('.custom-control-input').prop('checked', true);
            allFields.forEach(field => {
                dataManager.selectedFields.add(field);
            });
            dataManager.saveSelectedFields();
            updatePreviewTable();
        });

        // Add reset button event listener
        $('#resetFields').off('click').on('click', () => {
            dataManager.resetToDefaultFields();
            // Update checkboxes to match default fields
            $('.custom-control-input').each(function() {
                const field = $(this).attr('id').replace('field_', '');
                $(this).prop('checked', dataManager.selectedFields.has(field));
            });
            updatePreviewTable();
        });
    }

    // Add export button event listener
    $('#exportXLSX').click(() => {
        exportManager.exportToXLSX();
        $('#previewModal').modal('hide');
    });

    $('#exportCSV').click(() => {
        exportManager.exportToCSV();
        $('#previewModal').modal('hide');
    });

    $('#exportJSON').click(() => {
        exportManager.exportToJSON();
        $('#previewModal').modal('hide');
    });

    // Add clear data button event listener
    $('#clearData').click(() => {
        $('#confirmClearModal').modal('show');
    });

    // Add confirm clear button event listener
    $('#confirmClear').click(() => {
        dataManager.data.clear();
        dataManager.errorLog = [];
        updateButtonText();
        updatePreviewTable();
        $('#confirmClearModal').modal('hide');
        $('#previewModal').modal('hide');
    });

    // Add Font Awesome for icons
    const fontAwesomeLink = $('<link>', {
        rel: 'stylesheet',
        href: 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css'
    });
    $('head').append(fontAwesomeLink);

})();