Greasy Fork is available in English.

Pano Detective

Find the exact time a Google Street View image was taken (default coverage)

// ==UserScript==
// @name         Pano Detective
// @namespace    https://greasyfork.org/users/1179204
// @version      1.6.1
// @description  Find the exact time a Google Street View image was taken (default coverage)
// @author       KaKa
// @match        *://www.google.com/*
// @match        *://www.google.ru/*
// @match        *://www.google.de/*
// @match        *://www.google.fr/*
// @match        *://www.google.ca/*
// @match        *://www.google.it/*
// @match        *://www.google.co.uk/*
// @match        *://www.google.co.jp/*
// @match        *://www.google.co.id/*
// @match        *://www.google.co.in/*
// @match        *://www.google.co.kr/*
// @match        *://www.google.co.za/*
// @match        *://www.google.com.hk/*
// @match        *://www.google.com.br/*
// @icon         https://www.svgrepo.com/show/485785/magnifier.svg
// @require      https://cdn.jsdelivr.net/npm/sweetalert2@11
// @require      https://cdn.jsdelivr.net/npm/chinese-lunar@0.1.4/lib/chinese-lunar.min.js
// @require      https://cdn.jsdelivr.net/npm/browser-geo-tz@0.1.0/dist/geotz.min.js
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @license      BSD
// ==/UserScript==
(function() {

    const date_format=0 // 0:default(locale format) 1:yyyy-mm-dd  2:yyyy/mm/dd  3:dd/mm/yyyy  4:mm/dd/yyyy  5:Month dd, yyyy  6:dd Month, yyyy 7:Lunar

    const time_format=1     // 1:(hh:mm:ss)  2:(hh:mm:ss am/pm)

    const accuracy=2;       // default setting is 2 seconds
    /*===================================================================================================================================================================================================================================*/

    let dateSpan,detectButton,downloadButton,previousListener,zoomLevel,w,h,formattedTime,capturePano

    let type=2;

    let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun','Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
    let dateSvg=`<svg width="22px" height="22px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#9AA0a6"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g id="Calendar / Calendar_Days"> <path id="Vector" d="M8 4H7.2002C6.08009 4 5.51962 4 5.0918 4.21799C4.71547 4.40973 4.40973 4.71547 4.21799 5.0918C4 5.51962 4 6.08009 4 7.2002V8M8 4H16M8 4V2M16 4H16.8002C17.9203 4 18.4796 4 18.9074 4.21799C19.2837 4.40973 19.5905 4.71547 19.7822 5.0918C20 5.5192 20 6.07899 20 7.19691V8M16 4V2M4 8V16.8002C4 17.9203 4 18.4801 4.21799 18.9079C4.40973 19.2842 4.71547 19.5905 5.0918 19.7822C5.5192 20 6.07899 20 7.19691 20H16.8031C17.921 20 18.48 20 18.9074 19.7822C19.2837 19.5905 19.5905 19.2842 19.7822 18.9079C20 18.4805 20 17.9215 20 16.8036V8M4 8H20M16 16H16.002L16.002 16.002L16 16.002V16ZM12 16H12.002L12.002 16.002L12 16.002V16ZM8 16H8.002L8.00195 16.002L8 16.002V16ZM16.002 12V12.002L16 12.002V12H16.002ZM12 12H12.002L12.002 12.002L12 12.002V12ZM8 12H8.002L8.00195 12.002L8 12.002V12Z" stroke="#9AA0a6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g> </g></svg>`
    let date_Svg=`<svg width="22px" height="22px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g id="Calendar / Calendar_Days"> <path id="Vector" d="M8 4H7.2002C6.08009 4 5.51962 4 5.0918 4.21799C4.71547 4.40973 4.40973 4.71547 4.21799 5.0918C4 5.51962 4 6.08009 4 7.2002V8M8 4H16M8 4V2M16 4H16.8002C17.9203 4 18.4796 4 18.9074 4.21799C19.2837 4.40973 19.5905 4.71547 19.7822 5.0918C20 5.5192 20 6.07899 20 7.19691V8M16 4V2M4 8V16.8002C4 17.9203 4 18.4801 4.21799 18.9079C4.40973 19.2842 4.71547 19.5905 5.0918 19.7822C5.5192 20 6.07899 20 7.19691 20H16.8031C17.921 20 18.48 20 18.9074 19.7822C19.2837 19.5905 19.5905 19.2842 19.7822 18.9079C20 18.4805 20 17.9215 20 16.8036V8M4 8H20M16 16H16.002L16.002 16.002L16 16.002V16ZM12 16H12.002L12.002 16.002L12 16.002V16ZM8 16H8.002L8.00195 16.002L8 16.002V16ZM16.002 12V12.002L16 12.002V12H16.002ZM12 12H12.002L12.002 12.002L12 12.002V12ZM8 12H8.002L8.00195 12.002L8 12.002V12Z" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g> </g></svg>`
    let iconSvg=`<svg width="22px" height="22px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g id="Interface / Download"> <path id="Vector" d="M6 21H18M12 3V17M12 17L17 12M12 17L7 12" stroke="#9AA0a6" stroke-width="2.16" stroke-linecap="round" stroke-linejoin="round"></path> </g> </g></svg>`
    let icon_Svg=`<svg width="22px" height="22px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g id="Interface / Download"> <path id="Vector" d="M6 21H18M12 3V17M12 17L17 12M12 17L7 12" stroke="#ffffff" stroke-width="2.16" stroke-linecap="round" stroke-linejoin="round"></path> </g> </g></svg>`

    const iconUrl=svgToUrl(iconSvg)
    const icon_Url=svgToUrl(icon_Svg)
    const svgUrl=svgToUrl(dateSvg)
    const svg_Url=svgToUrl(date_Svg)
    const moon_phase=['🌑','🌒','🌓','🌔','🌕','🌖','🌗','🌘']

    function svgToUrl(svgText) {
        const svgBlob = new Blob([svgText], {type: 'image/svg+xml'});
        const svgUrl = URL.createObjectURL(svgBlob);
        return svgUrl;
    }

    function extractParams(link) {
        const regex = /@(-?\d+\.\d+),(-?\d+\.\d+),.*?\/data=!3m\d+!1e\d+!3m\d+!1s([^!]+)!/;

        const match = link.match(regex);

        if (match && match.length === 4) {
            var lat = match[1];
            var lng = match[2];
            var panoId = match[3];
            return {lat,lng,panoId}
        } else {
            console.error('Invalid Google Street View link format');
            return null;
        }
    }

    async function UE(t, e, s, d,r) {
        try {
            const url = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${t}`;
            let payload = createPayload(t, e,s,d,r);

            const response = await fetch(url, {
                method: "POST",
                headers: {
                    "content-type": "application/json+protobuf",
                    "x-user-agent": "grpc-web-javascript/0.1"
                },
                body: payload,
                mode: "cors",
                credentials: "omit"
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            } else {
                return await response.json();
            }
        } catch (error) {
            console.error(`There was a problem with the UE function: ${error.message}`);
        }
    }

    function createPayload(mode,coorData,s,d,r) {
        let payload;
        if (mode === 'GetMetadata') {
            const length=coorData.length
            if (length>22){
                type=10
            }
            else type=2
            payload = [["apiv3",null,null,null,"US",null,null,null,null,null,[[0]]],["en","US"],[[[type,coorData]]],[[1,2,3,4,8,6]]];
        }
        else if (mode === 'SingleImageSearch') {
            payload=[["apiv3"],[[null,null,parseFloat(coorData.lat),parseFloat(coorData.lng)],r],[[null,null,null,null,null,null,null,null,null,null,[s,d]],null,null,null,null,null,null,null,[2],null,[[[type,true,2]]]],[[2,6]]]}
        else {
            throw new Error("Invalid mode!");
        }
        return JSON.stringify(payload);
    }

    async function binarySearch(c, start,end,panoId) {
        let capture
        let response
        while( (end - start > accuracy)) {
            let mid=Math.round((start + end)/2) ;
            response = await UE("SingleImageSearch", c, start,end,30);
            if (response&&response.length>1){
                if (response[1][1][1]==panoId){
                    start=mid
                }
                else{
                    start=mid+start-end
                    end=start-mid+end
                }
            }
            else {
                start=mid+start-end
                end=start-mid+end
            }
            capture=Math.round((start + end)/2)
        }
        return capture
    }

    async function downloadPanoramaImage(panoId, fileName,panoramaWidth,panoramaHeight) {
        return new Promise(async (resolve, reject) => {
            try {
                const imageUrl = `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${panoId}&output=tile&zoom=${zoomLevel}&nbt=1&fover=2`;
                const tileWidth = 512;
                const tileHeight = 512;
                const zoomTiles=[2,4,8,16,32]

                const tilesPerRow = Math.min(Math.ceil(panoramaWidth / tileWidth),zoomTiles[zoomLevel-1]);
                const tilesPerColumn = Math.min(Math.ceil(panoramaHeight / tileHeight),zoomTiles[zoomLevel-1]/2);

                const canvasWidth = tilesPerRow * tileWidth;
                const canvasHeight = tilesPerColumn * tileHeight;

                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                canvas.width = canvasWidth;
                canvas.height = canvasHeight;

                for (let y = 0; y < tilesPerColumn; y++) {
                    for (let x = 0; x < tilesPerRow; x++) {
                        const tileUrl = `${imageUrl}&x=${x}&y=${y}`;
                        const tile = await loadImage(tileUrl);
                        ctx.drawImage(tile, x * tileWidth, y * tileHeight, tileWidth, tileHeight);
                    }
                }

                canvas.toBlob(blob => {
                    const url = window.URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = fileName;
                    document.body.appendChild(a);
                    a.click();
                    window.URL.revokeObjectURL(url);
                    resolve();
                }, 'image/jpeg');
            } catch (error) {
                Swal.fire('Error!', error.toString(),'error');
                reject(error);
            }
        });
    }

    async function loadImage(url) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.crossOrigin = 'Anonymous';
            img.onload = () => resolve(img);
            img.onerror = () => reject(new Error(`Failed to load image from ${url}`));
            img.src = url;
        });
    }

    function monthToTimestamp(m) {

        const [year, month] = m

        const startDate =Math.round( new Date(year, month-1,0).getTime()/1000);

        const endDate =Math.round(new Date(year, month, 2).getTime()/1000)

        return { startDate, endDate };
    }

    async function getLocal(coord, timestamp) {
        const systemTimezoneOffset = -new Date().getTimezoneOffset() * 60;

        try {
            var offset_hours
            const timezone=await GeoTZ.find(coord[0],coord[1])
            const offset = await GeoTZ.toOffset(timezone);


            if(offset){
                offset_hours=parseInt(offset/60)
            }
            else if (offset===0) offset_hours=0

            const offsetDiff = systemTimezoneOffset -offset_hours*3600;
            const convertedTimestamp = timestamp -offsetDiff;
            return convertedTimestamp;
        } catch (error) {
            throw error;
        }
    }

    function getMoonPhaseIcon(dayOfMonth) {
        const cycleDays = 29.53;
        const phaseIndex = Math.floor((dayOfMonth % cycleDays) / (cycleDays / moon_phase.length));
        return moon_phase[phaseIndex];
    }
    function formatTimestamp(timestamp) {
        var date_text,time_text
        const date = new Date(timestamp * 1000);
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const day = String(date.getDate()).padStart(2, '0');
        const hours = String(date.getHours()).padStart(2, '0');
        const minutes = String(date.getMinutes()).padStart(2, '0');
        const seconds = String(date.getSeconds()).padStart(2, '0');

        if (date_format===2) date_text=`${year}/${month}/${day}`
        else if (date_format===3) date_text=`${day}/${month}/${year}`
        else if (date_format===4) date_text=`${month}/${day}/${year}`
        else if (date_format===5) date_text=`${months[parseInt(month)]} ${parseInt(day)}, ${year}`
        else if (date_format===6) date_text=`${parseInt(day)} ${months[parseInt(month)]}, ${year}`
        else if (date_format===7) {
            const lunarDate=chineseLunar.solarToLunar(date)
            date_text = `${year}-${month}-${day} ${chineseLunar.format(lunarDate, 'MD')} ${getMoonPhaseIcon(lunarDate.day)}`;}
        else date_text=`${year}-${month}-${day}` ;

        if(time_format===1) time_text=`${hours}:${minutes}:${seconds}`
        else{
            const period = parseInt(hours) >= 12 ? 'pm' : 'am';

            var _hours = parseInt(hours) % 12 || 12;
            _hours = String(_hours).padStart(2, '0');
            time_text= `${_hours}:${minutes}:${seconds} ${period}`;
        }

        if(!date_format)return date.toLocaleString().replace('T', ' ');
        else return `${date_text} ${time_text}`;

    }

    function formatLatLng(lat, lng) {

        const latDirection = lat >= 0 ? 'N' : 'S';
        const latAbs = Math.abs(lat).toFixed(4);
        const latStr = `${latAbs}°${latDirection}`;


        const lngDirection = lng >= 0 ? 'E' : 'W';
        const lngAbs = Math.abs(lng).toFixed(4);
        const lngStr = `${lngAbs}°${lngDirection}`;

        return `${latStr}, ${lngStr}`;
    }

    async function addCustomButton() {

        const controlContainer=document.getElementById('app-container')

        const navigationDiv = document.querySelector("[role='navigation']");
        if (!navigationDiv) {
            console.error('Navigation div not found inside titlecard');
            return;
        }
        const logoSpan=navigationDiv.querySelector('span.ilzTS')
        const logoDiv=navigationDiv.querySelector('div.p4x6kc')
        dateSpan = navigationDiv.querySelector('span.mqX5ad');
        if (!dateSpan){
            dateSpan = navigationDiv.querySelector('span.lchoPb');
            if(!dateSpan){
                dateSpan = navigationDiv.querySelector('div.mqX5ad');
                if(!dateSpan)
                {
                    dateSpan = navigationDiv.querySelector('div.lchoPb');
                }
            }
        }
        if (!detectButton){
            detectButton = document.createElement("button");
        }
        if (!downloadButton){
            downloadButton = document.createElement("button");
        }
        const symbol=document.querySelector("[jsaction='titlecard.settings']")
        const buttonContainer = symbol.parentNode;
        if(symbol){
            const spotlight=document.querySelector("[jsaction='titlecard.spotlight']")
            const rect_=spotlight.getBoundingClientRect()
            if(rect_.top){
                const rect = symbol.getBoundingClientRect();
                var absoluteTop = rect.top
                var absoluteLeft = rect.left}
        }


        detectButton.id = 'detect-button';
        detectButton.style.marginTop = '1px';
        setupButton(detectButton, svgUrl,absoluteTop,absoluteLeft);

        downloadButton.id = 'download-button';
        setupButton(downloadButton, iconUrl,absoluteTop,absoluteLeft);
        if(absoluteLeft&&absoluteTop) downloadButton.style.marginLeft = '26px';
        else downloadButton.style.marginLeft='1px'


        if (!previousListener){
            previousListener=true
            addButtonHoverEffect(detectButton, svg_Url, svgUrl);
            addButtonHoverEffect(downloadButton, icon_Url, iconUrl);
            downloadButton.addEventListener("click",async function(){
                const { value: zoom, dismiss: inputDismiss } = await Swal.fire({
                    title: 'Image Quality',
                    html:
                    '<select id="zoom-select" class="swal2-input" style="width:180px; height:40px; font-size:16px;white-space:prewrap">' +
                    '<option value="1">1 (100KB~500KB)</option>' +
                    '<option value="2">2 (500KB~1MB)</option>' +
                    '<option value="3">3 (1MB~4MB)</option>' +
                    '<option value="4">4 (4MB~8MB)</option>' +
                    '<option value="5">5 (8MB~15MB)</option>' +
                    '</select>',
                    icon: 'question',
                    showCancelButton: true,
                    showCloseButton: true,
                    allowOutsideClick: false,
                    confirmButtonColor: '#3085d6',
                    cancelButtonColor: '#d33',
                    confirmButtonText: 'Yes',
                    cancelButtonText: 'Cancel',
                    preConfirm: () => {
                        return document.getElementById('zoom-select').value;
                    }
                });
                if (zoom){
                    zoomLevel=parseInt(zoom)
                    const currentUrl = window.location.href;
                    var panoId=extractParams(currentUrl).panoId;
                    var panoDate,lat,lng
                    const metaData = await UE('GetMetadata', panoId);
                    if (!metaData) {
                        console.error('Failed to get metadata');
                        return;
                    }
                    try{
                        w=parseInt(metaData[1][0][2][2][1])
                        h=parseInt(metaData[1][0][2][2][0])
                        panoDate=metaData[1][0][6][7]
                        lat=metaData[1][0][5][0][1][0][2]
                        lng=metaData[1][0][5][0][1][0][3]

                    }
                    catch (error){
                        try{
                            w=parseInt(metaData[1][2][2][1])
                            h=parseInt(metaData[1][2][2][0])
                            panoDate=metaData[1][6][7]
                            lat=metaData[1][5][0][1][0][2]
                            lng=metaData[1][5][0][1][0][3]
                        }
                        catch (error){
                            console.log(error)
                            return
                        }
                    }
                    if(w&&h){
                        const gpsTag=formatLatLng(lat,lng)
                        var timeTag
                        if(panoId===capturePano&&formattedTime) timeTag=formattedTime
                        else timeTag=`${panoDate[0]}-${panoDate[1]}`
                        const fileName = `${gpsTag}(${timeTag}).jpg`;
                        const swal = Swal.fire({
                            title: 'Downloading',
                            text: 'Please wait...',
                            allowOutsideClick: false,
                            allowEscapeKey: false,
                            showConfirmButton: false,
                            didOpen: () => {
                                Swal.showLoading();
                            }
                        });
                        await downloadPanoramaImage(panoId, fileName,w,h);
                        swal.close()
                        Swal.fire('Success!','Download completed', 'success');
                    }

                }

            })
            detectButton.addEventListener("click",async function() {
                if (dateSpan){
                    dateSpan.textContent='loading...'
                }
                const currentUrl = window.location.href;
                var altitude
                var panoId=extractParams(currentUrl).panoId;
                var lat=extractParams(currentUrl).lat
                var lng=extractParams(currentUrl).lng
                if (panoId.length>22)type=3
                try {

                    const metaData = await UE('GetMetadata', panoId);
                    if (!metaData) {
                        console.error('Failed to get metadata');
                        return;
                    }

                    var panoDate
                    try {
                        panoDate = metaData[1][0][6][7];
                        capturePano=metaData[1][0][1][1]
                        altitude=metaData[1][0][5][0][1][1][0]
                    } catch (error) {
                        try {
                            panoDate = metaData[1][6][7];
                            capturePano=metaData[1][1][1]
                            altitude=metaData[1][5][0][1][1][0]
                        } catch (error) {
                            console.log(error);
                            return;
                        }
                    }
                    if (logoSpan){
                        logoSpan.textContent=`${altitude}m`
                        logoDiv.style.backgroundImage=`url(${altitude>50?'https://www.svgrepo.com/show/406668/mountain.svg':'https://www.svgrepo.com/show/414747/ocean-sea-splash.svg'})`
                        GM_setClipboard(`${altitude},"${panoId}"`,'text')

                    }
                    if (!panoDate) {
                        dateSpan.textContent='unknown'
                        console.error('Failed to get panoDate');
                        return;
                    }

                    const timeRange = monthToTimestamp(panoDate);
                    if (!timeRange) {
                        console.error('Failed to convert panoDate to timestamp');
                        return;
                    }

                    try {
                        const captureTime = await binarySearch({"lat":lat,"lng":lng},timeRange.startDate,timeRange.endDate,panoId);
                        if (!captureTime) {

                            console.error('Failed to get capture time');
                            return;
                        }
                        const exactTime=await getLocal([lat,lng],captureTime)

                        if(!exactTime){
                            console.error('Failed to get exact time');
                        }
                        formattedTime=formatTimestamp(exactTime)
                        if(dateSpan){
                            dateSpan.textContent = formattedTime;
                        }
                    } catch (error) {
                        console.log(error);
                    }
                } catch (error) {
                    console.error(error);
                }
            })
        }
        if (navigationDiv) {
            const previewButton=navigationDiv.querySelector('#detect-button')
            if (!previewButton){
                buttonContainer.appendChild(detectButton)
            }
            const previewButton_=navigationDiv.querySelector('#download-button')
            if (!previewButton_){
                buttonContainer.appendChild(downloadButton)
            }
        }
    }

    function checkPosition(button,backgroundUrl) {
        const symbol=document.querySelector("[jsaction='titlecard.settings']")
        if(!symbol) return
        const rect = symbol.getBoundingClientRect();
        const spotlight=document.querySelector("[jsaction='titlecard.spotlight']")
        const rect_=spotlight.getBoundingClientRect()
        var absoluteTop = rect.top
        var absoluteLeft = rect.left
        if(rect_.top){
            if(absoluteTop&&absoluteLeft){
                button.style.position='fixed'
                button.style.top=`${absoluteTop+40}px`
                button.style.left=`${absoluteLeft-25}px`}}
        else{
            button.style.position=null
            button.style.top=null
            button.style.left=null
        }
    }

    function setupButton(button, backgroundUrl,top,left) {
        button.style.backgroundImage = `url(${backgroundUrl})`;
        button.style.backgroundSize = 'cover';
        button.style.backgroundPosition = 'center';
        button.style.display = 'block';
        button.style.width = '24px';
        button.style.height = '24px';
        button.style.fontSize = '12px';
        button.style.borderRadius = '10px';
        button.style.cursor = 'pointer';
        button.style.backgroundColor = 'transparent';
        if(top&&left){
            button.style.position='fixed'
            button.style.top=`${top+40}px`
            button.style.left=`${left-25}px`
            setInterval(function (){checkPosition(button,backgroundUrl)}, 100)
        }
    }

    function calculateFOV(zoom) {
        const pi = Math.PI;
        const argument = (3 / 4) * Math.pow(2, 1 - zoom);
        const radians = Math.atan(argument);
        const degrees = (360 / pi) * radians;
        return degrees;
    }

    async function getShortUrl(pageUrl) {
        const url = 'https://www.google.com/maps/rpc/shorturl';
        const panoId=extractParams(pageUrl).panoId;
        const urlObj = new URL(pageUrl);
        const path = urlObj.pathname;
        const pathRegex = /@([^,]+),([^,]+),(\d+)a,([^y]+)y,([^h]+)h,([^t]+)t/;
        const pathMatch = path.match(pathRegex);
        const lat = pathMatch ? pathMatch[1] : null;
        const lng = pathMatch ? pathMatch[2] : null;
        const a=pathMatch ? pathMatch[3] : null;
        const y = pathMatch ? pathMatch[4] : null;
        const h = pathMatch ? pathMatch[5] : null;
        const t = pathMatch ? pathMatch[6] : null;

        const pb = `!1shttps://www.google.com/maps/@${lat},${lng},${a}a,${y}y,${h}h,${t}t/data=*213m5*211e1*213m3*211s${panoId}*212e0*216shttps%3A%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fpanoid%3D${panoId}%26cb_client%3Dmaps_sv.share%26w%3D900%26h%3D600%26yaw%3D${h}%26pitch%3D${t-90}%26thumbfov%3D100*217i16384*218i8192?coh=205410&entry=tts!2m1!7e81!6b1`;


        const params = new URLSearchParams({
            authuser: '0',
            hl: 'en',
            gl: 'us',
            pb: pb
        }).toString();

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `${url}?${params}`,
                onload: function (response) {
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            const text = response.responseText;
                            const match = text.match(/"([^"]+)"/);
                            if (match && match[1]) {
                                resolve(match[1]);
                            } else {
                                reject('No URL found.');
                            }
                        } catch (error) {
                            reject('Failed to parse response: ' + error);
                        }
                    } else {
                        reject('Request failed with status: ' + response.status);
                    }
                },
                onerror: function (error) {
                    reject('Request error: ' + error);
                }
            });
        });
    }

    function addButtonHoverEffect(button, hoverImageUrl, defaultImageUrl) {
        button.addEventListener("mouseenter", function(event) {
            button.style.backgroundImage = `url(${hoverImageUrl})`;
        });
        button.addEventListener("mouseleave", function(event) {
            button.style.backgroundImage = `url(${defaultImageUrl})`;
        });
    }


    function onPageLoad() {
        const sceneFooter=document.getElementsByClassName('scene-footer')[0]
        const observer = new MutationObserver(function(mutationsList) {
            for (let mutation of mutationsList) {
                const controlContainer=document.getElementById('app-container')
                if(controlContainer){
                    addCustomButton()}
            };
        });
        const config = { childList: true, subtree: true, attributes: true };

        observer.observe(sceneFooter, config);
        setTimeout(function() {
            addCustomButton();
        }, 1200);
    }

    window.addEventListener('load', onPageLoad);


    let onKeyDown = async (e) => {
        if ((e.ctrlKey || e.metaKey)&&(e.key === 'x' || e.key === 'X')) {
            e.stopImmediatePropagation();
            let pageUrl = window.location.href;
            const currentLink=await getShortUrl(pageUrl)
            GM_setClipboard(currentLink, 'text');
            Swal.fire({
                title: 'Copy Succeed',
                text: 'Short Link has been pasted to your clipboard!',
                icon: 'success',
                timer: 1000,
                showConfirmButton: false,
            });
        }
    }
    document.addEventListener("keydown", onKeyDown);
})();