{"id":11,"date":"2025-08-18T03:24:04","date_gmt":"2025-08-18T03:24:04","guid":{"rendered":"https:\/\/jkoester.com\/?page_id=11"},"modified":"2025-08-18T19:48:25","modified_gmt":"2025-08-18T19:48:25","slug":"photo-map-tool-for-sony","status":"publish","type":"page","link":"https:\/\/jkoester.com\/index.php\/photo-map-tool-for-sony\/","title":{"rendered":"Photo map tool"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Dynamic Photo Markers App<\/title>\n    <!-- Tailwind CSS for modern styling and responsiveness -->\n    <script src=\"https:\/\/cdn.tailwindcss.com\"><\/script>\n    <style>\n        \/*\n         * Always set the map height explicitly to define the size of the div element that contains the map.\n         * Using vh ensures the map fills the viewport height.\n         *\/\n        #map {\n            height: calc(100vh - 80px); \/* Adjust height for the control panel at the top *\/\n        }\n        \n        \/* Basic styling for the custom marker content *\/\n        .marker-content {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            cursor: pointer;\n            -webkit-user-select: none;\n            -ms-user-select: none;\n            user-select: none;\n        }\n\n        \/* Styling for the arrow part of the marker *\/\n        \/* Updated to a more distinct arrow shape using an SVG path *\/\n        .marker-arrow {\n            fill: #ef4444; \/* Tailwind red-500 *\/\n            transition: transform 0.2s ease-in-out;\n            transform-origin: bottom center;\n        }\n\n        \/* Styling for the label (photo number) part of the marker *\/\n        .marker-label {\n            font-size: 0.75rem; \/* text-xs *\/\n            font-weight: bold;\n            color: white;\n            background-color: #3b82f6; \/* Tailwind blue-500 *\/\n            padding: 2px 6px;\n            border-radius: 9999px; \/* rounded-full *\/\n            margin-top: 4px;\n            box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);\n            white-space: nowrap;\n        }\n    <\/style>\n<\/head>\n<body class=\"bg-gray-100 font-sans\">\n\n    <!-- Control panel for uploading files and displaying status -->\n    <div class=\"p-4 bg-white shadow-md flex flex-col sm:flex-row items-center justify-between\">\n        <label class=\"block text-sm font-medium text-gray-700\">\n            Select photos to display on the map:\n        <\/label>\n        <div class=\"mt-2 sm:mt-0 flex items-center space-x-2 w-full sm:w-auto\">\n            <input\n                type=\"file\"\n                id=\"photo-upload\"\n                class=\"block w-full sm:w-auto text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 focus:outline-none\"\n                accept=\"image\/jpeg, image\/png\"\n                multiple\n            >\n            <div id=\"loading-spinner\" class=\"hidden flex items-center space-x-2\">\n                <svg class=\"animate-spin h-5 w-5 text-blue-500\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n                    <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"><\/circle>\n                    <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"><\/path>\n                <\/svg>\n                <span class=\"text-gray-600 text-sm\">Processing&#8230;<\/span>\n            <\/div>\n        <\/div>\n    <\/div>\n\n    <!-- The map container -->\n    <div id=\"map\"><\/div>\n\n    <!-- Script for reading EXIF data from images -->\n    <script src=\"https:\/\/cdn.jsdelivr.net\/npm\/exif-js\"><\/script>\n\n    <!-- The main application script -->\n    <script>\n        \/\/ The Maps JavaScript API key will be provided by the Canvas environment.\n        const MAPS_API_KEY = \"AIzaSyDXGGOnfKiuVAQcTlIF3fMhfsYD39RnPNE\";\n        let map;\n        let markers = []; \/\/ Array to hold all markers for easy clearing\n\n        \/\/ --- Helper functions ---\n\n        \/**\n         * Converts GPS data from EXIF (degrees, minutes, seconds) to decimal degrees.\n         * @param {Array<number>} gpsData - The GPS data array [degrees, minutes, seconds].\n         * @param {string} cardinalDirection - The cardinal direction ('N', 'S', 'E', 'W').\n         * @returns {number} The coordinate in decimal degrees.\n         *\/\n        function toDecimalDegrees(gpsData, cardinalDirection) {\n            if (!gpsData) return null;\n            const degrees = gpsData[0] || 0;\n            const minutes = gpsData[1] || 0;\n            const seconds = gpsData[2] || 0;\n            let decimal = degrees + (minutes \/ 60) + (seconds \/ 3600);\n            if (cardinalDirection === 'S' || cardinalDirection === 'W') {\n                decimal = -decimal;\n            }\n            return decimal;\n        }\n\n        \/\/ --- Core application logic ---\n\n        \/**\n         * Initializes the Google Map.\n         *\/\n        async function initMap() {\n            try {\n                console.log(\"initMap function called.\");\n                const { Map } = await google.maps.importLibrary(\"maps\");\n                \n                map = new Map(document.getElementById(\"map\"), {\n                    center: { lat: 34.0522, lng: -118.2437 },\n                    zoom: 12,\n                    mapId: \"DEMO_MAP_ID\",\n                });\n                console.log(\"Map initialized successfully.\");\n\n                const fileInput = document.getElementById('photo-upload');\n                if (fileInput.files.length > 0) {\n                    processFiles(Array.from(fileInput.files));\n                }\n            } catch (error) {\n                console.error(\"Error in initMap:\", error);\n                const mapDiv = document.getElementById(\"map\");\n                mapDiv.innerHTML = `<div class=\"p-8 text-center text-red-600\">Failed to load the Google Map. Please check the Developer Console for more details.<\/div>`;\n            }\n        }\n\n        \/**\n         * Clears all markers from the map.\n         *\/\n        function clearMarkers() {\n            if (markers.length > 0) {\n                markers.forEach(marker => marker.setMap(null));\n                markers = [];\n            }\n        }\n        \n        \/**\n         * Processes an array of files, extracts EXIF data, and adds markers to the map.\n         * @param {File[]} files - An array of image files.\n         *\/\n        function processFiles(files) {\n            console.log(\"Processing files...\");\n            clearMarkers();\n            const loadingSpinner = document.getElementById('loading-spinner');\n            loadingSpinner.classList.remove('hidden');\n\n            const promises = files.map(file => {\n                return new Promise(resolve => {\n                    EXIF.getData(file, function() {\n                        const tags = this.exifdata;\n                        const lat = toDecimalDegrees(tags.GPSLatitude, tags.GPSLatitudeRef);\n                        const lng = toDecimalDegrees(tags.GPSLongitude, tags.GPSLongitudeRef);\n                        \n                        const heading = tags.GPSImgDirection || tags.GPSTrack || 0;\n                        \n                        if (lat !== null && lng !== null) {\n                            console.log(`Found GPS data for file: ${file.name}`);\n                            resolve({\n                                position: { lat: lat, lng: lng },\n                                heading: heading,\n                                photoNumber: file.name,\n                                imageUrl: URL.createObjectURL(file)\n                            });\n                        } else {\n                            console.warn(`No valid GPS data found for file: ${file.name}`);\n                            resolve(null);\n                        }\n                    });\n                });\n            });\n\n            Promise.all(promises).then(photoData => {\n                loadingSpinner.classList.add('hidden');\n                \n                const validPhotoData = photoData.filter(data => data !== null);\n                \n                if (validPhotoData.length > 0) {\n                    const bounds = new google.maps.LatLngBounds();\n                    validPhotoData.forEach(data => bounds.extend(data.position));\n                    map.fitBounds(bounds);\n                    console.log(`Successfully mapped ${validPhotoData.length} photos.`);\n                } else {\n                    console.log(\"No valid photos with GPS data were found.\");\n                }\n\n                validPhotoData.forEach(data => addMarker(data));\n            });\n        }\n\n        \/**\n         * Creates and adds a single marker to the map.\n         * @param {Object} data - The marker data including position, heading, and image info.\n         *\/\n        async function addMarker(data) {\n            console.log(`Adding marker for ${data.photoNumber}`);\n            const { AdvancedMarkerElement } = await google.maps.importLibrary(\"marker\");\n            \n            \/\/ Create a custom HTML element for the marker content.\n            const markerContent = document.createElement(\"div\");\n            markerContent.className = \"marker-content\";\n            \n            \/\/ Create the arrow using an SVG to make it more distinct.\n            const arrowSvg = document.createElementNS('http:\/\/www.w3.org\/2000\/svg', 'svg');\n            arrowSvg.setAttribute('width', '20');\n            arrowSvg.setAttribute('height', '20');\n            arrowSvg.setAttribute('viewBox', '0 0 20 20');\n            arrowSvg.className = 'marker-arrow';\n            arrowSvg.style.transform = `rotate(${data.heading}deg)`;\n            arrowSvg.innerHTML = `<path d=\"M10 0 L20 20 L10 15 L0 20 Z\" \/>`;\n\n            \/\/ Create the clickable label with the file name.\n            const label = document.createElement(\"a\");\n            label.className = \"marker-label\";\n            label.textContent = data.photoNumber;\n            label.href = data.imageUrl;\n            label.target = \"_blank\";\n\n            \/\/ Attach the arrow and label to the marker content.\n            markerContent.appendChild(arrowSvg);\n            markerContent.appendChild(label);\n            \n            \/\/ Create the AdvancedMarkerElement with the custom content.\n            const marker = new AdvancedMarkerElement({\n                map: map,\n                position: data.position,\n                content: markerContent,\n            });\n\n            markers.push(marker);\n        }\n\n        \/\/ --- Event listeners ---\n\n        document.getElementById('photo-upload').addEventListener('change', (event) => {\n            const files = Array.from(event.target.files);\n            if (files.length > 0) {\n                processFiles(files);\n            }\n        });\n        \n        \/\/ --- Initialization ---\n\n        window.initMap = async () => {\n            try {\n                console.log(\"initMap function called.\");\n                const { Map } = await google.maps.importLibrary(\"maps\");\n                \n                map = new Map(document.getElementById(\"map\"), {\n                    center: { lat: 34.0522, lng: -118.2437 },\n                    zoom: 12,\n                    mapId: \"DEMO_MAP_ID\",\n                });\n                console.log(\"Map initialized successfully.\");\n\n                const fileInput = document.getElementById('photo-upload');\n                if (fileInput.files.length > 0) {\n                    processFiles(Array.from(fileInput.files));\n                }\n            } catch (error) {\n                console.error(\"Error in initMap:\", error);\n                const mapDiv = document.getElementById(\"map\");\n                mapDiv.innerHTML = `<div class=\"p-8 text-center text-red-600\">Failed to load the Google Map. Please check the Developer Console for more details.<\/div>`;\n            }\n        };\n\n        window.onload = () => {\n            console.log(\"Window loaded. Loading Google Maps script...\");\n            const mapsScript = document.createElement('script');\n            \/\/ Safely handle the empty API key\n            const apiKeyParam = MAPS_API_KEY ? `key=${MAPS_API_KEY}&` : '';\n            mapsScript.src = `https:\/\/maps.googleapis.com\/maps\/api\/js?${apiKeyParam}callback=initMap&libraries=marker`;\n            document.head.appendChild(mapsScript);\n        };\n    <\/script>\n<\/body>\n<\/html>\n\n","protected":false},"excerpt":{"rendered":"<p>Dynamic Photo Markers App Select photos to display on the map: Processing&#8230;<\/p>\n","protected":false},"author":2,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-11","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/jkoester.com\/index.php\/wp-json\/wp\/v2\/pages\/11","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/jkoester.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/jkoester.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/jkoester.com\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/jkoester.com\/index.php\/wp-json\/wp\/v2\/comments?post=11"}],"version-history":[{"count":4,"href":"https:\/\/jkoester.com\/index.php\/wp-json\/wp\/v2\/pages\/11\/revisions"}],"predecessor-version":[{"id":25,"href":"https:\/\/jkoester.com\/index.php\/wp-json\/wp\/v2\/pages\/11\/revisions\/25"}],"wp:attachment":[{"href":"https:\/\/jkoester.com\/index.php\/wp-json\/wp\/v2\/media?parent=11"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}