- // ==UserScript==
- // @name Geocaching Trackable Map Visualizer
- // @namespace http://tampermonkey.net/
- // @version 1.0.0
- // @description View your trackables on an interactive map. See where all your Geocaching trackables have been at a glance!
- // @author ViezeVingertjes
- // @match *://*.geocaching.com/track/search.aspx*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=geocaching.com
- // @grant none
- // @require https://unpkg.com/leaflet@1.9.4/dist/leaflet.js
- // @resource LEAFLET_CSS https://unpkg.com/leaflet@1.9.4/dist/leaflet.css
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- // Inject Leaflet CSS
- const linkElement = document.createElement('link');
- linkElement.rel = 'stylesheet';
- linkElement.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
- document.head.appendChild(linkElement);
-
- console.log("Geocaching Trackable Page Enhancer script loaded!");
-
- /**
- * Helper function to check if an element is visible
- * @param {Element} element - The DOM element to check
- * @returns {boolean} - Whether the element is visible
- */
- function isElementVisible(element) {
- if (!element) return false;
-
- const style = window.getComputedStyle(element);
- return style.display !== 'none' &&
- style.visibility !== 'hidden' &&
- style.opacity !== '0' &&
- element.offsetWidth > 0 &&
- element.offsetHeight > 0;
- }
-
- /**
- * Extracts trackable information from anchor elements on the page
- * @returns {Map} Map of trackable objects with id as key
- */
- function extractTrackablesFromPage() {
- const anchorElements = document.querySelectorAll("a");
- const trackablesMap = new Map();
- const trackableUrlPrefix = "https://www.geocaching.com/track/details.aspx?id=";
-
- anchorElements.forEach(anchor => {
- const href = anchor.getAttribute("href");
- if (href && href.startsWith(trackableUrlPrefix)) {
- try {
- const url = new URL(href, document.baseURI);
- const id = url.searchParams.get("id");
- const name = anchor.innerText.trim();
-
- if (id && name && !trackablesMap.has(id)) {
- trackablesMap.set(id, { id, name });
- }
- } catch (e) {
- console.error("Error parsing URL or extracting trackable info:", e, href);
- }
- }
- });
-
- return trackablesMap;
- }
-
- /**
- * Parse trackable stops from the map page HTML content
- * @param {string} htmlContent - HTML content from the trackable map page
- * @param {string} trackableId - ID of the trackable for error reporting
- * @returns {Array} Array of stop objects with coordinates and cache names
- */
- function parseTrackableStops(htmlContent, trackableId) {
- const stops = [];
- const tbStopsRegex = /var tbStops\s*=\s*(\[[\s\S]*?\])\s*;/;
- const match = htmlContent.match(tbStopsRegex);
-
- if (!match || !match[1]) {
- console.warn(`tbStops not found for trackable ${trackableId}`);
- return stops;
- }
-
- try {
- const arrayContentString = match[1].slice(1, -1); // Remove outer brackets
- const objectRegex = /{[\s\S]*?}/g;
- const coordRegex = /ll\s*:\s*\[\s*([\d\.-]+)\s*,\s*([\d\.-]+)\s*\]/;
- const nameRegex = /n\s*:\s*"([^"]*)"/;
- let objectMatch;
-
- while ((objectMatch = objectRegex.exec(arrayContentString)) !== null) {
- const objectString = objectMatch[0];
- const coordMatch = objectString.match(coordRegex);
- const nameMatch = objectString.match(nameRegex);
-
- if (coordMatch && coordMatch[1] && coordMatch[2] && nameMatch && nameMatch[1]) {
- try {
- const lat = parseFloat(coordMatch[1]);
- const lon = parseFloat(coordMatch[2]);
- const name = nameMatch[1];
- stops.push({
- coordinates: [lat, lon],
- cacheName: name
- });
- } catch (e) {
- console.error(`Error parsing coordinates for trackable ${trackableId}:`, e);
- }
- } else {
- console.error(`Failed to extract data from object string for trackable ${trackableId}:`, objectString);
- }
- }
- } catch (e) {
- console.error(`Error processing tbStops for trackable ${trackableId}:`, e);
- }
-
- return stops;
- }
-
- /**
- * Fetches and processes trackable stops data
- * @param {Object} trackable - The trackable object to enrich with stops
- * @returns {Object} The enriched trackable object
- */
- async function fetchTrackableStops(trackable) {
- const mapUrl = `https://www.geocaching.com/track/map_gm.aspx?ID=${trackable.id}`;
-
- try {
- const response = await fetch(mapUrl);
- if (!response.ok) {
- console.error(`Failed to fetch ${mapUrl}: ${response.status} ${response.statusText}`);
- trackable.stops = [];
- return trackable;
- }
-
- const htmlContent = await response.text();
- trackable.stops = parseTrackableStops(htmlContent, trackable.id);
-
- } catch (error) {
- console.error(`Error fetching stops for trackable ${trackable.id}:`, error);
- trackable.stops = [];
- }
-
- return trackable;
- }
-
- /**
- * Enriches trackables with their stop information
- * @param {Array} trackables - Array of trackable objects
- * @returns {Array} Array of enriched trackable objects
- */
- async function enrichTrackablesWithStops(trackables) {
- if (trackables.length === 0) {
- console.log("No trackables found to enrich.");
- return [];
- }
-
- const enrichedTrackables = await Promise.all(
- trackables.map(trackable => fetchTrackableStops(trackable))
- );
-
- console.log("Enriched Trackables (with stops):", enrichedTrackables);
- return enrichedTrackables;
- }
-
- // Add a global variable to track if processing is currently in progress
- let isProcessingTrackables = false;
- // Add a global variable to store the map instance
- let trackableMap = null;
-
- /**
- * Displays the trackable data on a map
- * @param {Array} trackables - Array of trackable objects with stops
- * @param {HTMLElement} [existingContainer] - Optional existing map container
- */
- function displayTrackablesMap(trackables, existingContainer) {
- // Filter trackables with stops
- const trackablesWithStops = trackables.filter(t => t.stops && t.stops.length > 0);
-
- if (trackablesWithStops.length === 0) {
- console.log('No trackables with stops to display on map');
- return;
- }
-
- // Sort trackables by number of stops (descending)
- trackablesWithStops.sort((a, b) => b.stops.length - a.stops.length);
-
- // Extract the last stop from each trackable
- const mapPoints = trackablesWithStops.map(trackable => {
- const lastStop = trackable.stops[trackable.stops.length - 1];
- return {
- trackableId: trackable.id,
- trackableName: trackable.name,
- cacheName: lastStop.cacheName,
- coordinates: lastStop.coordinates,
- totalStops: trackable.stops.length
- };
- });
-
- console.log('Map points for display:', mapPoints);
-
- // Group points by coordinates to combine markers at the same location
- const groupedPoints = {};
- mapPoints.forEach(point => {
- const coordKey = point.coordinates.join(',');
- if (!groupedPoints[coordKey]) {
- groupedPoints[coordKey] = {
- coordinates: point.coordinates,
- cacheName: point.cacheName, // Use the cache name from the first trackable at this location
- trackables: []
- };
- }
- groupedPoints[coordKey].trackables.push(point);
- });
-
- console.log('Grouped map points:', groupedPoints);
-
- // Convert back to array for display and sort by total number of trackables (descending)
- const combinedMapPoints = Object.values(groupedPoints);
- combinedMapPoints.sort((a, b) => b.trackables.length - a.trackables.length);
-
- // If we don't have a map instance, we can't proceed
- if (!trackableMap) {
- console.error('No map instance available');
- return;
- }
-
- // Remove any loading message
- const loadingControl = document.querySelector('.loading-message');
- if (loadingControl && loadingControl.parentNode) {
- loadingControl.parentNode.removeChild(loadingControl);
- }
-
- // Clear any existing markers
- trackableMap.eachLayer(layer => {
- if (layer instanceof L.Marker || layer instanceof L.Tooltip) {
- trackableMap.removeLayer(layer);
- }
- });
-
- // Calculate bounding box for all points
- let minLat = 90, maxLat = -90, minLon = 180, maxLon = -180;
-
- combinedMapPoints.forEach(point => {
- const [lat, lon] = point.coordinates;
- minLat = Math.min(minLat, lat);
- maxLat = Math.max(maxLat, lat);
- minLon = Math.min(minLon, lon);
- maxLon = Math.max(maxLon, lon);
- });
-
- // Add padding
- const latPadding = Math.max(0.05, (maxLat - minLat) * 0.1);
- const lonPadding = Math.max(0.05, (maxLon - minLon) * 0.1);
-
- minLat = Math.max(-85, minLat - latPadding);
- maxLat = Math.min(85, maxLat + latPadding);
- minLon = Math.max(-180, minLon - lonPadding);
- maxLon = Math.min(180, maxLon + lonPadding);
-
- // Fit the map to the bounds of all markers
- try {
- trackableMap.fitBounds([
- [minLat, minLon],
- [maxLat, maxLon]
- ]);
- } catch (e) {
- console.error('Error fitting map bounds:', e);
- }
-
- // Store markers for reference
- const markers = [];
-
- // Define a good palette of distinct colors
- const colorPalette = [
- '#e6194B', // Red
- '#3cb44b', // Green
- '#ffe119', // Yellow
- '#4363d8', // Blue
- '#f58231', // Orange
- '#911eb4', // Purple
- '#42d4f4', // Cyan
- '#f032e6', // Magenta
- '#bfef45', // Lime
- '#fabed4', // Pink
- '#469990', // Teal
- '#dcbeff', // Lavender
- '#9A6324', // Brown
- '#fffac8', // Beige
- '#800000', // Maroon
- '#aaffc3', // Mint
- '#808000', // Olive
- '#ffd8b1', // Apricot
- '#000075', // Navy
- '#a9a9a9', // Grey
- '#ffffff', // White
- '#000000' // Black
- ];
-
- // Create a map to track used colors for cache names
- const cacheColorMap = new Map();
- // Track last used color index for round-robin assignment
- let lastColorIndex = -1;
-
- // Get a color ensuring no consecutive identical colors
- function getColorForCache(cacheName) {
- // If we already assigned a color to this cache, use it
- if (cacheColorMap.has(cacheName)) {
- return cacheColorMap.get(cacheName);
- }
-
- // Get the next color in round-robin fashion
- lastColorIndex = (lastColorIndex + 1) % colorPalette.length;
-
- // Find a different color if this would create consecutive same colors
- if (markers.length > 0) {
- const prevMarker = markers[markers.length - 1];
- const prevColor = getColorForCache(prevMarker.point.cacheName);
-
- // If colors would match, skip to next color
- if (colorPalette[lastColorIndex] === prevColor) {
- lastColorIndex = (lastColorIndex + 1) % colorPalette.length;
- }
- }
-
- const color = colorPalette[lastColorIndex];
- cacheColorMap.set(cacheName, color);
- return color;
- }
-
- // Add markers for each point
- combinedMapPoints.forEach((point, index) => {
- const [lat, lon] = point.coordinates;
- const trackables = point.trackables;
- const trackableCount = trackables.length;
-
- // Sort trackables at this location by number of stops (descending)
- trackables.sort((a, b) => b.totalStops - a.totalStops);
-
- // Get a color based on cache name
- const markerColor = getColorForCache(point.cacheName);
-
- // Create popup content with basic information
- let popupContent = `
- <div>
- <div style="font-weight: bold; margin-bottom: 5px;">${point.cacheName}</div>
- <div style="font-size: 12px; color: #666; margin-bottom: 8px;">Coordinates: ${lat.toFixed(6)}, ${lon.toFixed(6)}</div>
- `;
-
- if (trackables.length > 1) {
- popupContent += `<div style="font-weight: bold; margin-bottom: 8px; color: ${markerColor};">${trackables.length} Trackables at this Location</div>`;
- }
-
- // Add each trackable with simple formatting
- trackables.forEach((tb, i) => {
- popupContent += `
- <div style="margin-top: 8px; ${i > 0 ? 'border-top: 1px solid #eee; padding-top: 8px;' : ''}">
- <div style="font-weight: bold;">${i+1}. ${tb.trackableName}</div>
- ${tb.totalStops ? `<div style="font-size: 12px; color: #666;">Total stops: ${tb.totalStops}</div>` : ''}
- <div><a href="https://www.geocaching.com/track/details.aspx?id=${tb.trackableId}" target="_blank" style="color: #007bff; text-decoration: none;">View trackable details</a></div>
- </div>
- `;
- });
-
- popupContent += '</div>';
-
- // Create a colored marker for this point
- const markerIcon = L.divIcon({
- className: '',
- html: `<div style="background-color: ${markerColor}; width: 24px; height: 24px; border-radius: 12px; border: 2px solid white; box-shadow: 0 1px 3px rgba(0,0,0,0.4);"></div>`,
- iconSize: [28, 28],
- iconAnchor: [14, 14]
- });
- const marker = L.marker([lat, lon], { icon: markerIcon }).addTo(trackableMap);
-
- // Add a label for the marker
- const labelText = point.cacheName + (trackables.length > 1 ? ` (${trackables.length})` : '');
-
- const label = L.tooltip({
- permanent: true,
- direction: 'top',
- className: 'trackable-marker-label',
- offset: [0, -12]
- })
- .setContent(labelText)
- .setLatLng([lat, lon]);
-
- label.addTo(trackableMap);
-
- // Bind popup to marker
- marker.bindPopup(popupContent);
-
- // Hide label when popup is open
- marker.on('popupopen', function() {
- trackableMap.removeLayer(label);
- });
-
- // Show label when popup is closed
- marker.on('popupclose', function() {
- label.addTo(trackableMap);
- });
-
- markers.push({
- marker,
- label,
- point
- });
- });
-
- // Update the legend
- const mapSection = existingContainer.closest('#gc-trackables-map-section');
- const legendContainer = mapSection ? mapSection.querySelector('#trackables-map-legend') : null;
-
- if (legendContainer) {
- // Get the content container
- const legendContent = document.getElementById('trackables-map-legend-content');
- if (!legendContent) return;
-
- // Clear any existing content
- legendContent.innerHTML = '';
-
- // Add entries for each marker/location
- markers.forEach((markerData, index) => {
- const { marker, point, label } = markerData;
- const trackables = point.trackables;
-
- // For each location, create a section
- const sectionContainer = document.createElement('div');
- sectionContainer.style.marginBottom = index < markers.length - 1 ? '10px' : '0';
- sectionContainer.style.paddingBottom = index < markers.length - 1 ? '10px' : '0';
- sectionContainer.style.borderBottom = index < markers.length - 1 ? '1px solid #eee' : 'none';
-
- // Location header
- const locationHeader = document.createElement('div');
- locationHeader.style.display = 'flex';
- locationHeader.style.alignItems = 'center';
- locationHeader.style.marginBottom = '5px';
- locationHeader.style.cursor = 'pointer';
-
- // Create color dot to match marker color
- const colorDot = document.createElement('span');
- colorDot.style.width = '16px';
- colorDot.style.height = '16px';
- colorDot.style.borderRadius = '50%';
- colorDot.style.backgroundColor = getColorForCache(point.cacheName);
- colorDot.style.display = 'inline-block';
- colorDot.style.marginRight = '8px';
- colorDot.style.border = '1px solid rgba(0,0,0,0.2)';
-
- locationHeader.appendChild(colorDot);
-
- // Location text
- let locationText;
- locationText = document.createElement('div');
-
- if (trackables.length === 1) {
- locationText.textContent = trackables[0].cacheName;
- } else {
- locationText.textContent = `${point.cacheName} (${trackables.length} trackables)`;
- }
-
- locationText.style.fontWeight = 'bold';
- locationHeader.appendChild(locationText);
-
- // Add click event to zoom to marker
- locationHeader.addEventListener('click', () => {
- trackableMap.setView(marker.getLatLng(), 15);
-
- // Slight delay to ensure map has completed moving before opening popup
- setTimeout(() => {
- marker.openPopup();
- }, 300);
- });
-
- // Add hover effect
- locationHeader.addEventListener('mouseenter', () => {
- locationHeader.style.backgroundColor = '#f0f0f0';
- });
-
- locationHeader.addEventListener('mouseleave', () => {
- locationHeader.style.backgroundColor = '';
- });
-
- sectionContainer.appendChild(locationHeader);
-
- // Add individual trackable items if there are multiple at this location
- if (trackables.length > 1) {
- const trackablesList = document.createElement('div');
- trackablesList.style.marginLeft = '24px';
-
- trackables.forEach((tb, i) => {
- const trackableItem = document.createElement('div');
- trackableItem.style.padding = '3px 0';
- trackableItem.style.fontSize = '12px';
- trackableItem.style.display = 'flex';
- trackableItem.style.alignItems = 'center';
- trackableItem.style.cursor = 'pointer';
-
- const bulletPoint = document.createElement('span');
- bulletPoint.textContent = '•';
- bulletPoint.style.marginRight = '5px';
- trackableItem.appendChild(bulletPoint);
-
- const tbName = document.createElement('span');
- tbName.innerHTML = `<span style="font-weight: bold;">${tb.trackableName}</span>${tb.totalStops ? ` (${tb.totalStops} stops)` : ''}`;
- trackableItem.appendChild(tbName);
-
- // Add click handler to open trackable page
- trackableItem.addEventListener('click', () => {
- window.open(`https://www.geocaching.com/track/details.aspx?id=${tb.trackableId}`, '_blank');
- });
-
- // Add hover effect
- trackableItem.addEventListener('mouseenter', () => {
- trackableItem.style.backgroundColor = '#f0f0f0';
- trackableItem.style.color = '#0066cc';
- });
-
- trackableItem.addEventListener('mouseleave', () => {
- trackableItem.style.backgroundColor = '';
- trackableItem.style.color = '';
- });
-
- trackablesList.appendChild(trackableItem);
- });
-
- sectionContainer.appendChild(trackablesList);
- } else if (trackables.length === 1) {
- // Make single trackable clickable too
- const tb = trackables[0];
- const trackableItem = document.createElement('div');
- trackableItem.style.marginLeft = '28px';
- trackableItem.style.fontSize = '12px';
- trackableItem.style.cursor = 'pointer';
- trackableItem.style.display = 'flex';
- trackableItem.style.alignItems = 'center';
-
- const bulletPoint = document.createElement('span');
- bulletPoint.textContent = '•';
- bulletPoint.style.marginRight = '5px';
- trackableItem.appendChild(bulletPoint);
-
- const tbName = document.createElement('span');
- tbName.innerHTML = `<span style="font-weight: bold;">${tb.trackableName}</span>${tb.totalStops ? ` (${tb.totalStops} stops)` : ''}`;
- trackableItem.appendChild(tbName);
-
- // Add click handler to open trackable page
- trackableItem.addEventListener('click', () => {
- window.open(`https://www.geocaching.com/track/details.aspx?id=${tb.trackableId}`, '_blank');
- });
-
- // Add hover effect
- trackableItem.addEventListener('mouseenter', () => {
- trackableItem.style.backgroundColor = '#f0f0f0';
- trackableItem.style.color = '#0066cc';
- });
-
- trackableItem.addEventListener('mouseleave', () => {
- trackableItem.style.backgroundColor = '';
- trackableItem.style.color = '';
- });
-
- sectionContainer.appendChild(trackableItem);
- }
-
- legendContent.appendChild(sectionContainer);
- });
- }
- }
-
- /**
- * Creates a map using Leaflet
- * @param {HTMLElement} container - The container to add the map to
- * @param {Array} points - The points to display on the map
- */
- function createSimpleMapWithMarkers(container, points) {
- if (!container || !points || points.length === 0) return;
-
- // Calculate bounding box for all points
- let minLat = 90;
- let maxLat = -90;
- let minLon = 180;
- let maxLon = -180;
-
- points.forEach(point => {
- const [lat, lon] = point.coordinates;
- minLat = Math.min(minLat, lat);
- maxLat = Math.max(maxLat, lat);
- minLon = Math.min(minLon, lon);
- maxLon = Math.max(maxLon, lon);
- });
-
- // Add padding
- const latPadding = Math.max(0.05, (maxLat - minLat) * 0.1);
- const lonPadding = Math.max(0.05, (maxLon - minLon) * 0.1);
-
- minLat = Math.max(-85, minLat - latPadding);
- maxLat = Math.min(85, maxLat + latPadding);
- minLon = Math.max(-180, minLon - lonPadding);
- maxLon = Math.min(180, maxLon + lonPadding);
-
- // Clear the container
- container.innerHTML = '';
-
- // Create map container for Leaflet
- const mapViewContainer = document.createElement('div');
- mapViewContainer.id = 'leaflet-map';
- mapViewContainer.style.width = '100%';
- mapViewContainer.style.height = '500px';
- mapViewContainer.style.border = '1px solid #ddd';
- mapViewContainer.style.borderRadius = '4px';
- container.appendChild(mapViewContainer);
-
- // Initialize the map
- const map = L.map('leaflet-map').fitBounds([
- [minLat, minLon],
- [maxLat, maxLon]
- ]);
-
- // Add OpenStreetMap tile layer
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
- attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
- maxZoom: 18
- }).addTo(map);
-
- // Add custom CSS for marker labels
- const style = document.createElement('style');
- style.textContent = `
- .trackable-marker-label {
- background: white;
- border: 1px solid #333;
- border-radius: 4px;
- padding: 2px 6px;
- font-weight: bold;
- white-space: nowrap;
- text-align: center;
- box-shadow: 0 1px 3px rgba(0,0,0,0.2);
- pointer-events: none;
- }
- `;
- document.head.appendChild(style);
-
- // Store markers for reference
- const markers = [];
-
- // Define a good palette of distinct colors
- const colorPalette = [
- '#e6194B', // Red
- '#3cb44b', // Green
- '#ffe119', // Yellow
- '#4363d8', // Blue
- '#f58231', // Orange
- '#911eb4', // Purple
- '#42d4f4', // Cyan
- '#f032e6', // Magenta
- '#bfef45', // Lime
- '#fabed4', // Pink
- '#469990', // Teal
- '#dcbeff', // Lavender
- '#9A6324', // Brown
- '#fffac8', // Beige
- '#800000', // Maroon
- '#aaffc3', // Mint
- '#808000', // Olive
- '#ffd8b1', // Apricot
- '#000075', // Navy
- '#a9a9a9', // Grey
- '#ffffff', // White
- '#000000' // Black
- ];
-
- // Create a map to track used colors for cache names
- const cacheColorMap = new Map();
- // Track last used color index for round-robin assignment
- let lastColorIndex = -1;
-
- // Get a color ensuring no consecutive identical colors
- function getColorForCache(cacheName) {
- // If we already assigned a color to this cache, use it
- if (cacheColorMap.has(cacheName)) {
- return cacheColorMap.get(cacheName);
- }
-
- // Get the next color in round-robin fashion
- lastColorIndex = (lastColorIndex + 1) % colorPalette.length;
-
- // Find a different color if this would create consecutive same colors
- if (markers.length > 0) {
- const prevMarker = markers[markers.length - 1];
- const prevColor = getColorForCache(prevMarker.point.cacheName);
-
- // If colors would match, skip to next color
- if (colorPalette[lastColorIndex] === prevColor) {
- lastColorIndex = (lastColorIndex + 1) % colorPalette.length;
- }
- }
-
- const color = colorPalette[lastColorIndex];
- cacheColorMap.set(cacheName, color);
- return color;
- }
-
- // Add markers for each point
- points.forEach((point, index) => {
- const [lat, lon] = point.coordinates;
- const trackables = point.trackables;
- const trackableCount = trackables.length;
-
- // Sort trackables at this location by number of stops (descending)
- trackables.sort((a, b) => b.totalStops - a.totalStops);
-
- // Get a color based on cache name hash for better distribution
- const markerColor = getColorForCache(point.cacheName);
-
- // Create popup content with basic information
- let popupContent = `
- <div>
- <div style="font-weight: bold; margin-bottom: 5px;">${point.cacheName}</div>
- <div style="font-size: 12px; color: #666; margin-bottom: 8px;">Coordinates: ${lat.toFixed(6)}, ${lon.toFixed(6)}</div>
- `;
-
- if (trackables.length > 1) {
- popupContent += `<div style="font-weight: bold; margin-bottom: 8px; color: ${markerColor};">${trackables.length} Trackables at this Location</div>`;
- }
-
- // Add each trackable with simple formatting
- trackables.forEach((tb, i) => {
- popupContent += `
- <div style="margin-top: 8px; ${i > 0 ? 'border-top: 1px solid #eee; padding-top: 8px;' : ''}">
- <div style="font-weight: bold;">${i+1}. ${tb.trackableName}</div>
- ${tb.totalStops ? `<div style="font-size: 12px; color: #666;">Total stops: ${tb.totalStops}</div>` : ''}
- <div><a href="https://www.geocaching.com/track/details.aspx?id=${tb.trackableId}" target="_blank" style="color: #007bff; text-decoration: none;">View trackable details</a></div>
- </div>
- `;
- });
-
- popupContent += '</div>';
-
- // Create a colored marker for this point
- const markerIcon = L.divIcon({
- className: '',
- html: `<div style="background-color: ${markerColor}; width: 24px; height: 24px; border-radius: 12px; border: 2px solid white; box-shadow: 0 1px 3px rgba(0,0,0,0.4);"></div>`,
- iconSize: [28, 28],
- iconAnchor: [14, 14]
- });
- const marker = L.marker([lat, lon], { icon: markerIcon }).addTo(map);
-
- // Add a label for the marker
- const labelText = trackables.length === 1 ?
- point.cacheName + (trackables.length > 1 ? ` (${trackables.length})` : '') :
- `${point.cacheName} (${trackables.length})`;
-
- const label = L.tooltip({
- permanent: true,
- direction: 'top',
- className: 'trackable-marker-label',
- offset: [0, -12]
- })
- .setContent(labelText)
- .setLatLng([lat, lon]);
-
- label.addTo(map);
-
- // Bind popup to marker
- marker.bindPopup(popupContent);
-
- // Hide label when popup is open
- marker.on('popupopen', function() {
- map.removeLayer(label);
- });
-
- // Show label when popup is closed
- marker.on('popupclose', function() {
- label.addTo(map);
- });
-
- markers.push({
- marker,
- label,
- point
- });
- });
-
- // Find the legend container - should be a sibling of our map container
- const mapSection = container.closest('#gc-trackables-map-section');
- const legendContainer = mapSection ? mapSection.querySelector('#trackables-map-legend') : null;
-
- if (legendContainer) {
- // Get the content container
- const legendContent = document.getElementById('trackables-map-legend-content');
- if (!legendContent) return;
-
- // Clear any existing content
- legendContent.innerHTML = '';
-
- // Add entries for each marker/location
- markers.forEach((markerData, index) => {
- const { marker, point, label } = markerData;
- const trackables = point.trackables;
- const trackableCount = trackables.length;
-
- // For each location, create a section
- const sectionContainer = document.createElement('div');
- sectionContainer.style.marginBottom = index < markers.length - 1 ? '10px' : '0';
- sectionContainer.style.paddingBottom = index < markers.length - 1 ? '10px' : '0';
- sectionContainer.style.borderBottom = index < markers.length - 1 ? '1px solid #eee' : 'none';
-
- // Location header
- const locationHeader = document.createElement('div');
- locationHeader.style.display = 'flex';
- locationHeader.style.alignItems = 'center';
- locationHeader.style.marginBottom = '5px';
- locationHeader.style.cursor = 'pointer';
-
- // Create color dot to match marker color
- const colorDot = document.createElement('span');
- colorDot.style.width = '16px';
- colorDot.style.height = '16px';
- colorDot.style.borderRadius = '50%';
- colorDot.style.backgroundColor = getColorForCache(point.cacheName);
- colorDot.style.display = 'inline-block';
- colorDot.style.marginRight = '8px';
- colorDot.style.border = '1px solid rgba(0,0,0,0.2)';
-
- locationHeader.appendChild(colorDot);
-
- // Location text
- let locationText;
- locationText = document.createElement('div');
-
- if (trackables.length === 1) {
- locationText.textContent = trackables[0].cacheName;
- } else {
- locationText.textContent = `${point.cacheName} (${trackables.length} trackables)`;
- }
-
- locationText.style.fontWeight = 'bold';
- locationHeader.appendChild(locationText);
-
- // Add click event to zoom to marker
- locationHeader.addEventListener('click', () => {
- map.setView(marker.getLatLng(), 15);
-
- // Slight delay to ensure map has completed moving before opening popup
- setTimeout(() => {
- marker.openPopup();
- }, 300);
- });
-
- // Add hover effect
- locationHeader.addEventListener('mouseenter', () => {
- locationHeader.style.backgroundColor = '#f0f0f0';
- });
-
- locationHeader.addEventListener('mouseleave', () => {
- locationHeader.style.backgroundColor = '';
- });
-
- sectionContainer.appendChild(locationHeader);
-
- // Add individual trackable items if there are multiple at this location
- if (trackables.length > 1) {
- const trackablesList = document.createElement('div');
- trackablesList.style.marginLeft = '24px';
-
- trackables.forEach((tb, i) => {
- const trackableItem = document.createElement('div');
- trackableItem.style.padding = '3px 0';
- trackableItem.style.fontSize = '12px';
- trackableItem.style.display = 'flex';
- trackableItem.style.alignItems = 'center';
- trackableItem.style.cursor = 'pointer';
-
- const bulletPoint = document.createElement('span');
- bulletPoint.textContent = '•';
- bulletPoint.style.marginRight = '5px';
- trackableItem.appendChild(bulletPoint);
-
- const tbName = document.createElement('span');
- tbName.innerHTML = `<span style="font-weight: bold;">${tb.trackableName}</span>${tb.totalStops ? ` (${tb.totalStops} stops)` : ''}`;
- trackableItem.appendChild(tbName);
-
- // Add click handler to open trackable page
- trackableItem.addEventListener('click', () => {
- window.open(`https://www.geocaching.com/track/details.aspx?id=${tb.trackableId}`, '_blank');
- });
-
- // Add hover effect
- trackableItem.addEventListener('mouseenter', () => {
- trackableItem.style.backgroundColor = '#f0f0f0';
- trackableItem.style.color = '#0066cc';
- });
-
- trackableItem.addEventListener('mouseleave', () => {
- trackableItem.style.backgroundColor = '';
- trackableItem.style.color = '';
- });
-
- trackablesList.appendChild(trackableItem);
- });
-
- sectionContainer.appendChild(trackablesList);
- } else if (trackables.length === 1) {
- // Make single trackable clickable too
- const tb = trackables[0];
- const trackableItem = document.createElement('div');
- trackableItem.style.marginLeft = '28px';
- trackableItem.style.fontSize = '12px';
- trackableItem.style.cursor = 'pointer';
- trackableItem.style.display = 'flex';
- trackableItem.style.alignItems = 'center';
-
- const bulletPoint = document.createElement('span');
- bulletPoint.textContent = '•';
- bulletPoint.style.marginRight = '5px';
- trackableItem.appendChild(bulletPoint);
-
- const tbName = document.createElement('span');
- tbName.innerHTML = `<span style="font-weight: bold;">${tb.trackableName}</span>${tb.totalStops ? ` (${tb.totalStops} stops)` : ''}`;
- trackableItem.appendChild(tbName);
-
- // Add click handler to open trackable page
- trackableItem.addEventListener('click', () => {
- window.open(`https://www.geocaching.com/track/details.aspx?id=${tb.trackableId}`, '_blank');
- });
-
- // Add hover effect
- trackableItem.addEventListener('mouseenter', () => {
- trackableItem.style.backgroundColor = '#f0f0f0';
- trackableItem.style.color = '#0066cc';
- });
-
- trackableItem.addEventListener('mouseleave', () => {
- trackableItem.style.backgroundColor = '';
- trackableItem.style.color = '';
- });
-
- sectionContainer.appendChild(trackableItem);
- }
-
- legendContent.appendChild(sectionContainer);
- });
- }
- }
-
- // Function to safely inject the map container into the page
- function safelyInjectMap() {
- // First, identify the main container and the search panel
- const pageWrapper = document.querySelector('#Content, #content, .Content');
-
- if (!pageWrapper) {
- console.error('Could not find main content wrapper');
- return null;
- }
-
- // Clear any existing map we might have added before
- const existingMap = document.getElementById('gc-trackables-map-section');
- if (existingMap) {
- existingMap.remove();
- }
-
- // Create our map container with a distinctive ID
- const mapSection = document.createElement('div');
- mapSection.id = 'gc-trackables-map-section';
- mapSection.style.width = '100%';
- mapSection.style.clear = 'both';
- mapSection.style.position = 'relative';
- mapSection.style.margin = '20px 0';
- mapSection.style.padding = '0';
- mapSection.style.backgroundColor = '#fff';
- mapSection.style.boxSizing = 'border-box';
-
- // Add title
- const mapTitle = document.createElement('h3');
- mapTitle.textContent = 'Trackable Locations Map';
- mapTitle.style.margin = '0 0 10px 0';
- mapTitle.style.padding = '0';
- mapTitle.style.fontSize = '16px';
- mapTitle.style.fontWeight = 'bold';
- mapSection.appendChild(mapTitle);
-
- // Create map container
- const mapContainer = document.createElement('div');
- mapContainer.id = 'trackables-map-container';
- mapContainer.style.width = '100%';
- mapContainer.style.height = '500px';
- mapContainer.style.border = '1px solid #ddd';
- mapContainer.style.borderRadius = '4px';
- mapContainer.style.marginBottom = '10px';
- mapContainer.style.boxSizing = 'border-box';
- mapSection.appendChild(mapContainer);
-
- // Create legend container that will be filled by the map creation function
- const legendContainer = document.createElement('div');
- legendContainer.id = 'trackables-map-legend';
- legendContainer.style.marginTop = '10px';
- legendContainer.style.width = '100%';
- legendContainer.style.boxSizing = 'border-box';
- legendContainer.style.border = '1px solid #eee';
- legendContainer.style.borderRadius = '4px';
- legendContainer.style.backgroundColor = '#fff';
-
- // Create collapsible header for legend
- const legendHeader = document.createElement('div');
- legendHeader.style.padding = '10px';
- legendHeader.style.borderBottom = '1px solid #eee';
- legendHeader.style.display = 'flex';
- legendHeader.style.alignItems = 'center';
- legendHeader.style.justifyContent = 'space-between';
- legendHeader.style.cursor = 'pointer';
-
- // Create title text
- const headerText = document.createElement('div');
- headerText.textContent = 'Trackables';
- headerText.style.fontWeight = 'bold';
- headerText.style.fontSize = '14px';
-
- // Create arrow indicator
- const arrowIndicator = document.createElement('div');
- arrowIndicator.innerHTML = '▲'; // Up arrow (collapsed)
- arrowIndicator.style.transition = 'transform 0.3s';
- arrowIndicator.style.fontSize = '12px';
-
- // Append elements to header
- legendHeader.appendChild(headerText);
- legendHeader.appendChild(arrowIndicator);
- legendContainer.appendChild(legendHeader);
-
- // Create content container for the legend
- const legendContent = document.createElement('div');
- legendContent.id = 'trackables-map-legend-content';
- legendContent.style.padding = '10px';
- legendContent.style.display = 'none'; // Hidden by default
- legendContent.style.maxHeight = 'none';
- legendContent.style.overflowY = 'visible';
- legendContainer.appendChild(legendContent);
-
- // Add click event to toggle legend visibility
- legendHeader.addEventListener('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
-
- const isVisible = legendContent.style.display !== 'none';
- legendContent.style.display = isVisible ? 'none' : 'block';
- arrowIndicator.innerHTML = isVisible ? '▲' : '▼'; // Up arrow when closed, down arrow when open
-
- return false;
- });
-
- mapSection.appendChild(legendContainer);
-
- // Look for the best insertion point
- let inserted = false;
-
- // Method 1: Try to find common table containers
- const tableContainers = Array.from(document.querySelectorAll('.Table, table, .table-container'));
- for (const table of tableContainers) {
- // Only consider visible tables
- if (isElementVisible(table)) {
- const tableParent = table.parentNode;
-
- // Insert before the table
- tableParent.insertBefore(mapSection, table);
- inserted = true;
- break;
- }
- }
-
- // Method 2: If we couldn't find a table, try to find section headings
- if (!inserted) {
- const sectionHeadings = Array.from(document.querySelectorAll('h1, h2, h3'));
- for (const heading of sectionHeadings) {
- // Look for headings related to trackables or search
- const headingText = heading.textContent.toLowerCase();
- if ((headingText.includes('trackable') || headingText.includes('search')) && isElementVisible(heading)) {
- // Insert after the heading
- if (heading.nextSibling) {
- heading.parentNode.insertBefore(mapSection, heading.nextSibling);
- } else {
- heading.parentNode.appendChild(mapSection);
- }
- inserted = true;
- break;
- }
- }
- }
-
- // Method 3: Last resort - insert at top of content area
- if (!inserted) {
- // Insert at the beginning of the content area
- if (pageWrapper.firstChild) {
- pageWrapper.insertBefore(mapSection, pageWrapper.firstChild);
- } else {
- pageWrapper.appendChild(mapSection);
- }
- }
-
- // Initialize the empty map
- const mapViewContainer = document.createElement('div');
- mapViewContainer.id = 'leaflet-map';
- mapViewContainer.style.width = '100%';
- mapViewContainer.style.height = '500px';
- mapViewContainer.style.border = '1px solid #ddd';
- mapViewContainer.style.borderRadius = '4px';
- mapContainer.appendChild(mapViewContainer);
-
- // Initialize the map with a default view (world map)
- try {
- // Create a new map instance
- trackableMap = L.map('leaflet-map').setView([20, 0], 2);
-
- // Add OpenStreetMap tile layer
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
- attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
- maxZoom: 18
- }).addTo(trackableMap);
-
- // Add "Loading trackables..." message
- const loadingMessage = L.control({position: 'bottomleft'});
- loadingMessage.onAdd = function(map) {
- const div = L.DomUtil.create('div', 'loading-message');
- div.innerHTML = '<div style="background-color: white; padding: 5px 10px; border-radius: 4px; border: 1px solid #ccc; font-weight: bold;">Loading trackable data...</div>';
- return div;
- };
- loadingMessage.addTo(trackableMap);
- } catch (e) {
- console.error('Error initializing map:', e);
- }
-
- // Add custom CSS for marker labels
- const style = document.createElement('style');
- style.textContent = `
- .trackable-marker-label {
- background: white;
- border: 1px solid #333;
- border-radius: 4px;
- padding: 2px 6px;
- font-weight: bold;
- white-space: nowrap;
- text-align: center;
- box-shadow: 0 1px 3px rgba(0,0,0,0.2);
- pointer-events: none;
- }
- `;
- document.head.appendChild(style);
-
- return mapContainer;
- }
-
- // Find trackables on the page and process them
- async function processTrackables() {
- // Prevent concurrent processing
- if (isProcessingTrackables) {
- console.log('Already processing trackables, skipping duplicate call');
- return;
- }
-
- isProcessingTrackables = true;
-
- try {
- // Create and inject map container first before processing data
- const mapContainer = safelyInjectMap();
-
- // Extract trackables
- const trackablesMap = extractTrackablesFromPage();
- console.log(`Found ${trackablesMap.size} trackables on page`);
-
- if (trackablesMap.size === 0) {
- console.log('No trackables found on page');
- // Update the map with a "No trackables" message
- if (trackableMap) {
- // Remove any loading message
- const loadingControl = document.querySelector('.loading-message');
- if (loadingControl && loadingControl.parentNode) {
- loadingControl.parentNode.removeChild(loadingControl);
- }
-
- const noDataMessage = L.control({position: 'bottomleft'});
- noDataMessage.onAdd = function(map) {
- const div = L.DomUtil.create('div', 'no-data-message');
- div.innerHTML = '<div style="background-color: white; padding: 5px 10px; border-radius: 4px; border: 1px solid #ccc;">No trackable location data available</div>';
- return div;
- };
- noDataMessage.addTo(trackableMap);
- }
- isProcessingTrackables = false;
- return;
- }
-
- const trackables = Array.from(trackablesMap.values());
- console.log('Trackables found:', trackables);
-
- // Enrich trackables with stop data
- const enrichedTrackables = await enrichTrackablesWithStops(trackables);
-
- // Display on map
- displayTrackablesMap(enrichedTrackables, mapContainer);
- } catch (error) {
- console.error('Error in processTrackables:', error);
- } finally {
- // Always reset the processing flag
- isProcessingTrackables = false;
- }
- }
-
- // Run on page load and after AJAX content updates
- setTimeout(processTrackables, 1000);
-
- // Track if the map has been added to the page
- let mapAdded = false;
-
- // Create a MutationObserver to watch for content changes
- const observer = new MutationObserver(function(mutations) {
- // Don't trigger if we're already processing or if we created the map element
- if (isProcessingTrackables || mapAdded) return;
-
- let shouldReprocess = false;
-
- // Check if any mutations affect our elements of interest (trackable links)
- for (const mutation of mutations) {
- // Skip mutations caused by our own map
- if (mutation.target.id === 'gc-trackables-map-section' ||
- mutation.target.closest('#gc-trackables-map-section')) {
- continue;
- }
-
- // Skip mutations that don't add nodes - we only care about content being added
- if (mutation.type !== 'childList' || mutation.addedNodes.length === 0) {
- continue;
- }
-
- // Look for relevant data tables or trackable links
- if (mutation.target.classList.contains('Table') ||
- mutation.target.querySelector('.Table') ||
- mutation.target.querySelector('a[href*="track/details.aspx"]')) {
- shouldReprocess = true;
- break;
- }
- }
-
- if (shouldReprocess) {
- console.log('Content changed, reprocessing trackables');
- processTrackables().finally(() => {
- mapAdded = true;
-
- // Disconnect observer after first successful map creation to prevent further updates
- // This prevents repeated refreshing while still allowing the initial map to be created
- observer.disconnect();
- });
- }
- });
-
- // Start observing with configuration
- observer.observe(document.body, {
- childList: true,
- subtree: true
- });
- })();