Created
August 24, 2025 10:36
-
-
Save dobrovolsky/88f736c8200637c7f343c3691b73556e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name GPX Studio Maps Opener | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.4 | |
| // @description Adds a button to open the map in Google Maps or Strava | |
| // @author You | |
| // @match https://gpx.studio/* | |
| // @grant none | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // Function to get current coordinates and zoom from URL | |
| function getCurrentMapData() { | |
| const hash = window.location.hash; | |
| // Format: #zoom/lat/lng | |
| const match = hash.match(/#([\d.]+)\/([\d.-]+)\/([\d.-]+)/); | |
| if (match) { | |
| return { | |
| zoom: parseFloat(match[1]), | |
| lat: parseFloat(match[2]), | |
| lng: parseFloat(match[3]) | |
| }; | |
| } | |
| return { zoom: 15, lat: 49.21075, lng: 28.42181 }; // fallback values | |
| } | |
| // Function to convert zoom level for different services | |
| function convertZoom(originalZoom, service) { | |
| switch(service) { | |
| case 'google': | |
| // Google Maps usually uses similar zoom levels | |
| return Math.round(originalZoom); | |
| case 'strava': | |
| // Strava may differ slightly, but usually similar | |
| return Math.round(originalZoom); | |
| default: | |
| return Math.round(originalZoom); | |
| } | |
| } | |
| // Function to close popup | |
| function closePopup(popup, overlay) { | |
| if (popup && popup.parentNode) { | |
| document.body.removeChild(popup); | |
| } | |
| if (overlay && overlay.parentNode) { | |
| document.body.removeChild(overlay); | |
| } | |
| } | |
| // Function to create popup | |
| function createPopup() { | |
| const mapData = getCurrentMapData(); | |
| // Create overlay (darkened background) | |
| const overlay = document.createElement('div'); | |
| overlay.style.cssText = ` | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0,0,0,0.5); | |
| z-index: 9999; | |
| `; | |
| // Create popup | |
| const popup = document.createElement('div'); | |
| popup.id = 'maps-popup'; | |
| popup.style.cssText = ` | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background: white; | |
| border: 2px solid #333; | |
| border-radius: 8px; | |
| padding: 20px; | |
| z-index: 10000; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.3); | |
| font-family: Arial, sans-serif; | |
| min-width: 300px; | |
| `; | |
| popup.innerHTML = ` | |
| <h3 style="margin-top: 0; color: #333; text-align: center;">Open Map</h3> | |
| <div style="text-align: center; color: #666; margin-bottom: 15px;"> | |
| <p style="margin: 5px 0;">Coordinates: ${mapData.lat.toFixed(5)}, ${mapData.lng.toFixed(5)}</p> | |
| <p style="margin: 5px 0;">Zoom: ${mapData.zoom.toFixed(1)}</p> | |
| </div> | |
| <div style="margin: 20px 0; display: flex; flex-direction: column; gap: 10px;"> | |
| <button id="open-gmaps" style=" | |
| background: #4285f4; | |
| color: white; | |
| border: none; | |
| padding: 12px 20px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| font-weight: 500; | |
| transition: background 0.2s; | |
| width: 100%; | |
| ">📍 Google Maps (zoom: ${convertZoom(mapData.zoom, 'google')})</button> | |
| <button id="open-strava" style=" | |
| background: #fc4c02; | |
| color: white; | |
| border: none; | |
| padding: 12px 20px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| font-weight: 500; | |
| transition: background 0.2s; | |
| width: 100%; | |
| ">🔥 Strava Heatmap (zoom: ${convertZoom(mapData.zoom, 'strava')})</button> | |
| </div> | |
| <button id="close-popup" style=" | |
| background: #666; | |
| color: white; | |
| border: none; | |
| padding: 8px 16px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 12px; | |
| display: block; | |
| margin: 10px auto 0; | |
| ">Close</button> | |
| `; | |
| // Hover effects | |
| const gmapsBtn = popup.querySelector('#open-gmaps'); | |
| const stravaBtn = popup.querySelector('#open-strava'); | |
| gmapsBtn.onmouseenter = () => gmapsBtn.style.background = '#3367d6'; | |
| gmapsBtn.onmouseleave = () => gmapsBtn.style.background = '#4285f4'; | |
| stravaBtn.onmouseenter = () => stravaBtn.style.background = '#e63900'; | |
| stravaBtn.onmouseleave = () => stravaBtn.style.background = '#fc4c02'; | |
| // Event handlers for buttons | |
| gmapsBtn.onclick = function() { | |
| const googleZoom = convertZoom(mapData.zoom, 'google'); | |
| const url = `https://www.google.com/maps/@${mapData.lat},${mapData.lng},${googleZoom}z`; | |
| window.open(url, '_blank'); | |
| closePopup(popup, overlay); | |
| }; | |
| stravaBtn.onclick = function() { | |
| const stravaZoom = convertZoom(mapData.zoom, 'strava'); | |
| const url = `https://www.strava.com/maps/global-heatmap#${stravaZoom}/${mapData.lat}/${mapData.lng}`; | |
| window.open(url, '_blank'); | |
| closePopup(popup, overlay); | |
| }; | |
| popup.querySelector('#close-popup').onclick = function() { | |
| closePopup(popup, overlay); | |
| }; | |
| // Close on outside click | |
| overlay.onclick = function() { | |
| closePopup(popup, overlay); | |
| }; | |
| // Close on Escape | |
| const escapeHandler = function(e) { | |
| if (e.key === 'Escape') { | |
| closePopup(popup, overlay); | |
| document.removeEventListener('keydown', escapeHandler); | |
| } | |
| }; | |
| document.addEventListener('keydown', escapeHandler); | |
| // Add elements to DOM | |
| document.body.appendChild(overlay); | |
| document.body.appendChild(popup); | |
| } | |
| // Function to create button | |
| function createButton() { | |
| const button = document.createElement('button'); | |
| button.id = 'maps-opener-btn'; | |
| button.innerHTML = '🗺️'; | |
| button.title = 'Open in Google Maps or Strava'; | |
| button.style.cssText = ` | |
| background: white; | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| padding: 0px; | |
| margin: 0px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| box-shadow: 0 0 10px rgba(0,0,0,0.1); | |
| z-index: 1000; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 29px; | |
| height: 29px; | |
| line-height: 1; | |
| `; | |
| // Hover effect | |
| button.onmouseenter = function() { | |
| button.style.background = '#f0f0f0'; | |
| }; | |
| button.onmouseleave = function() { | |
| button.style.background = 'white'; | |
| }; | |
| button.onclick = createPopup; | |
| return button; | |
| } | |
| // Function to add button to map controls | |
| function addButton() { | |
| // Look for container for controls | |
| const controlContainer = document.querySelector('.mapboxgl-ctrl-top-right'); | |
| if (controlContainer && !document.querySelector('#maps-opener-btn')) { | |
| const button = createButton(); | |
| // Create wrapper in Mapbox GL style | |
| const controlGroup = document.createElement('div'); | |
| controlGroup.className = 'mapboxgl-ctrl mapboxgl-ctrl-group'; | |
| controlGroup.appendChild(button); | |
| controlContainer.appendChild(controlGroup); | |
| console.log('Maps opener button added'); | |
| } else if (!document.querySelector('#maps-opener-btn')) { | |
| // If container is not ready yet, try again later | |
| setTimeout(addButton, 1000); | |
| } | |
| } | |
| // Wait for page load | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', function() { | |
| setTimeout(addButton, 2000); | |
| }); | |
| } else { | |
| setTimeout(addButton, 2000); | |
| } | |
| // Also observe DOM changes in case of dynamic loading | |
| const observer = new MutationObserver(function(mutations) { | |
| const controlContainer = document.querySelector('.mapboxgl-ctrl-top-right'); | |
| if (controlContainer && !document.querySelector('#maps-opener-btn')) { | |
| addButton(); | |
| } | |
| }); | |
| observer.observe(document.body, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment