// ==UserScript==
// @name Geocaching Puzzle Helper
// @description Show hidden user-added elements on Geocaching Mystery Cache Page
// @match http://www.geocaching.com/geocache/*
// @match https://www.geocaching.com/geocache/*
// @match http://geocaching.com/geocache/*
// @match https://geocaching.com/geocache/*
// @version 1.21
// @namespace https://gf.qytechs.cn/en/scripts/464566-geocaching-puzzle-helper
// @homepage https://gf.qytechs.cn/en/scripts/464566-geocaching-puzzle-helper
// @license MIT
// ==/UserScript==
/* Features
-- Add several links to the map links
-- Checks if cache is at posted coordinates and adds a function to the top to quick replace
-- Show coordinate in decimal format
-- Present button to highlight comments, white text, link and hidden link information
*/
(function () {
'use strict';
// Entry point
const descriptions = [
"ctl00_ContentBody_ShortDescription",
"ctl00_ContentBody_LongDescription",
];
descriptions.forEach(id => scanElemForStuff(document.getElementById(id)));
showFinalLocation();
addCustomLink("Ingress", buildIngressURL());
addCustomLink("HMDB", buildHMDBURL());
addCustomLink("NowListenToMe", buildNowListenToMeURL());
appendDecimalMinutes();
/**
* Appends decimal minutes to the location panel.
*/
function appendDecimalMinutes() {
const elem = document.getElementById("ctl00_ContentBody_LocationSubPanel");
if (elem) {
elem.innerText += `DEC: ${mapLatLng.lat}, ${mapLatLng.lng}\n`;
}
}
/**
* Adds a custom link to the map links section.
* @param {string} name - The name of the link.
* @param {string} url - The URL for the link.
*/
function addCustomLink(name, url) {
const mapLinks = document.getElementById("ctl00_ContentBody_MapLinks_MapLinks");
if (mapLinks) {
const list = mapLinks.querySelector('ul');
const listItem = document.createElement('li');
const link = document.createElement('a');
link.setAttribute("target", "_blank");
link.href = url;
link.innerText = name;
listItem.appendChild(link);
list.appendChild(listItem);
}
}
/**
* Builds the Ingress link URL.
*/
function buildIngressURL() {
return `https://intel.ingress.com/intel?ll=${mapLatLng.lat},${mapLatLng.lng}&z=16`;
}
/**
* Builds the HMDB link URL.
*/
function buildHMDBURL() {
return `https://www.hmdb.org/results.asp?Search=Proximity&SearchFor=${mapLatLng.lat},${mapLatLng.lng}&Miles=10&MilesType=1&HistMark=Y&WarMem=Y`;
}
/**
* Builds the NowListenToMe link URL.
*/
function buildNowListenToMeURL() {
return `http://nowlistento.me/geocalc?StartCoord=${mapLatLng.lat},${mapLatLng.lng}&z=16`;
}
/**
* Scans an element for hosted links, extra text, links, white text, and comments.
* @param {HTMLElement} elem - The element to scan.
*/
function scanElemForStuff(elem) {
if (!elem) return;
[
{ label: "Hosted", getData: getHostedLinks },
{ label: "Extra", getData: getExtraText },
{ label: "Links", getData: getAllLinks },
{ label: "White Text", getData: getWhiteText },
{ label: "Comments", getData: getAllComments },
].forEach(({ label, getData }) => {
const data = getData(elem);
if (data.length > 0) {
addButton(elem, label, data.join('\r\n'), onClickHandler);
}
});
}
/**
* Adds a button to a parent element.
*/
function addButton(parent, text, title, onclick, append = false) {
const button = document.createElement('button');
button.innerHTML = text;
button.title = title;
button.onclick = onclick;
button.addEventListener('contextmenu', e => e.preventDefault());
append ? parent.appendChild(button) : parent.insertBefore(button, parent.firstChild);
}
// Utility functions for data extraction
function getAllComments(rootElem) {
const iterator = document.createNodeIterator(rootElem, NodeFilter.SHOW_COMMENT, null, false);
const comments = [];
let curNode;
while ((curNode = iterator.nextNode())) {
comments.push(curNode.nodeValue);
}
return comments;
}
function getAllLinks(rootElem) {
return Array.from(rootElem.getElementsByTagName('a')).map(link => link.href);
}
function getHostedLinks(rootElem) {
const imgs = Array.from(rootElem.getElementsByTagName('img'));
return imgs
.map(img => img.src.toLowerCase())
.filter(src =>
(!src.includes("s3.amazonaws.com/gs-geo-images") &&
!src.includes(".geocaching.com") &&
!src.includes(".groundspeak.com")) ||
src.includes("?")
);
}
function getWhiteText(rootElem) {
const whiteColors = ["#ffffff", "white", "rgb(255, 255, 255)"];
return Array.from(rootElem.getElementsByTagName("*"))
.filter(el => whiteColors.includes(el.style.color) || whiteColors.includes(el.getAttribute("color")))
.map(el => el.innerHTML);
}
function getExtraText(rootElem) {
const attributes = ["alt", "name", "id", "title"];
return Array.from(rootElem.getElementsByTagName("*"))
.flatMap(el => attributes.map(attr => el.getAttribute(attr)).filter(Boolean));
}
/**
* Handles click events for buttons.
*/
function onClickHandler(e) {
alert(this.title);
return false;
}
/**
* Displays the final location if available.
*/
function showFinalLocation() {
const elem = document.getElementById("awpt_FN");
if (!elem) return;
const coordElem = elem.parentNode?.nextElementSibling?.nextElementSibling?.nextElementSibling;
if (coordElem && coordElem.innerText.length > 4) {
const locElem = document.getElementById("uxLatLonLink");
if (locElem) {
addButton(locElem, "FN", coordElem.innerText, onClickHandler, true);
}
}
}
})();