// ==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.6
// @namespace https://greasyfork.org/en/scripts/464566-geocaching-puzzle-helper
// @homepage https://greasyfork.org/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();
hasThreeConsecutiveSixDigitNumbers();
/**
* 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;
const hostedLinks = getHostedLinks(elem, []);
const extraTextLinks = getExtraText(elem, hostedLinks);
const allLinks = getAllLinks(elem, hostedLinks, extraTextLinks);
const dataGroups = [
{ label: "Hosted", data: hostedLinks },
{ label: "Extra", data: extraTextLinks },
{ label: "Links", data: allLinks },
{ label: "White Text", data: getWhiteText(elem) },
{ label: "Comments", data: getAllComments(elem) },
];
dataGroups.forEach(({ label, data }) => {
if (data.length > 0) {
addToggle(elem, label, data.join('\r\n'), onClickHandler);
}
});
}
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);
}
/**
* Adds a button to a parent element.
*/
function addToggle(parent, text, title, onclick, append = false) {
var descriptionheader = document.body.querySelector(".h3.CacheDescriptionHeader");
const textcode = text.replace(" ","");
var contentspan = parent.querySelector("#togglecontentspan");
if (contentspan===null) {
var brelem = document.createElement('br');
contentspan = document.createElement('span');
contentspan.id ="togglecontentspan";
parent.insertBefore(brelem,parent.firstChild);
parent.insertBefore(contentspan,parent.firstChild);
}
const buttondiv = document.createElement('div');
buttondiv.style.cursor = "pointer";
buttondiv.style.display = "inline-block";
buttondiv.style.float = "right";
buttondiv.style.fontSize ="12px";
buttondiv.style.fontWeight = "normal";
buttondiv.onclick = onToggleHandler;
const arrowspan = document.createElement('span');
arrowspan.id="clickarrow"+textcode;
arrowspan.innerHTML="▲";
buttondiv.appendChild(arrowspan);
const labelspan = document.createElement('span');
labelspan.innerHTML=text;
buttondiv.appendChild(labelspan);
const textspan = document.createElement('span');
textspan.id = "togglecontent"+textcode;
textspan.innerHTML=title.replace(/\n/g, '<br>');
textspan.style.display = "block";
textspan.style.fontStyle = "italic";
contentspan.appendChild(textspan);
descriptionheader.appendChild(buttondiv);
}
// 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, hostedLinks, extraTextLinks) {
const allLinks = Array.from(rootElem.querySelectorAll('a'));
const uniqueLinks = allLinks
.map(link => link.href.toLowerCase())
.filter(link => !hostedLinks.includes(link) && !extraTextLinks.includes(link));
return [...new Set(uniqueLinks)]; // Ensure no duplicates within allLinks
}
function getHostedLinks(rootElem, otherArray) {
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("?")
))
.filter(src => !otherArray.includes(src));
}
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, hostedLinks) {
const attributes = ["alt", "name", "id", "title"];
return Array.from(rootElem.getElementsByTagName("*"))
.flatMap(el => attributes.map(attr => el.getAttribute(attr)).filter(Boolean))
.filter(text => !hostedLinks.includes(text.toLowerCase())); // Exclude links in hostedLinks
}
/**
* Handles click events for buttons.
*/
function onClickHandler(e) {
alert(this.title);
return false;
}
function onToggleHandler(e) {
const textcode = e.currentTarget.children[1].innerText.replace(" ","");
const content = document.body.querySelector("#togglecontent"+textcode);
const arrow = document.body.querySelector("#clickarrow"+textcode);
if (content.style.display === 'none' || content.style.display === '') {
content.style.display = 'block';
arrow.innerHTML = '▲'; // Up arrow
} else {
content.style.display = 'none';
arrow.innerHTML = '▼'; // Down arrow
}
}
/**
* 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);
}
}
}
function code2LatLon(varA, varB, varC) {
let latSign, lonSign, lonValue, latValue;
console.debug("Converting [" + varA + ", " + varB + ", " + varC + "] to LatLon" )
// 123456 => digit 1 (d1) = 6; digit 2 (d2) = 5; ...
// syntax for varA => digit 1 var A = A1; digit 2 varA = A2; ...
// A3
if ((varA % 1000 - varA % 100) / 100 == 1) {
latSign = 1;
lonSign = 1;
}
// A3
else if ((varA % 1000 - varA % 100) / 100 == 2) {
latSign = -1;
lonSign = 1;
}
// A3
else if ((varA % 1000 - varA % 100) / 100 == 3) {
latSign = 1;
lonSign = -1;
}
// A3
else if ((varA % 1000 - varA % 100) / 100 == 4) {
latSign = -1;
lonSign = -1;
}
//T41140 / Q1TQ01 / 14S4RS
// A6 B3 B4 B6 C1 C2 C4
// TODO: how to iterate only these, not full range ??
// C (d5 + d2) eli C5 + C2 = parillinen
if ( ((varC % 100000 - varC % 10000) / 10000 + (varC % 100 - varC % 10) / 10) % 2 === 0) {
// A4 B2 B5 C3 A6 C2 A1
latValue = Number(((varA % 10000 - varA % 1000) / 1000 * 10 + (varB % 100 - varB % 10) / 10 + (varB % 100000 - varB % 10000) / 10000 * 0.1 + (varC % 1000 - varC % 100) / 100 * 0.01 + (varA % 1000000 - varA % 100000) / 100000 * 0.001 + (varC % 100 - varC % 10) / 10 * 1.0E-4 + varA % 10 * 1.0E-5));
// A5 C6 C1 B3 B6 A2 C5 B1
lonValue = Number(((varA % 100000 - varA % 10000) / 10000 * 100 + (varC % 1000000 - varC % 100000) / 100000 * 10 + varC % 10 + (varB % 1000 - varB % 100) / 100 * 0.1 + (varB % 1000000 - varB % 100000) / 100000 * 0.01 + (varA % 100 - varA % 10) / 10 * 0.001 + (varC % 100000 - varC % 10000) / 10000 * 1.0E-4 + varB % 10 * 1.0E-5));
}
// C (d5 + d2) eli C5+C2= pariton
else if ( ((varC % 100000 - varC % 10000) / 10000 + (varC % 100 - varC % 10) / 10) % 2 !== 0) {
// B6 A1 A4 C6 C3 C2 A6
latValue = Number(((varB % 1000000 - varB % 100000) / 100000 * 10 + varA % 10 + (varA % 10000 - varA % 1000) / 1000 * 0.1 + (varC % 1000000 - varC % 100000) / 100000 * 0.01 + (varC % 1000 - varC % 100) / 100 * 0.001 + (varC % 100 - varC % 10) / 10 * 1.0E-4 + (varA % 1000000 - varA % 100000) / 100000 * 1.0E-5))
// B2 C1 A2 A5 B3 B1 C5 B5
lonValue = Number(((varB % 100 - varB % 10) / 10 * 100 + varC % 10 * 10 + (varA % 100 - varA % 10) / 10 + (varA % 100000 - varA % 10000) / 10000 * 0.1 + (varB % 1000 - varB % 100) / 100 * 0.01 + varB % 10 * 0.001 + (varC % 100000 - varC % 10000) / 10000 * 1.0E-4 + (varB % 100000 - varB % 10000) / 10000 * 1.0E-5));
}
// B4 C4 = ALWAYS ignore
latValue = latSign * latValue;
lonValue = lonSign * lonValue;
return { lat: latValue, lon: lonValue }
}
function hasThreeConsecutiveSixDigitNumbers() {
// Find the <longdescription> tag
const longDescriptionTag = document.getElementById('ctl00_ContentBody_LongDescription');
const noteTag = document.getElementById('srOnlyCacheNote');
if (!longDescriptionTag) {
console.error('No <longdescription> tag found');
return false;
} else {
// Get the text content of the tag
const textContent = longDescriptionTag.textContent;
// Regular expression to match three consecutive six-digit numbers,
// allowing separators like commas, spaces, and newlines
const regex = /\b(\d{6})(?:[ ,\n]+)(\d{6})(?:[ ,\n]+)(\d{6})\b/;
// Test if the pattern exists in the text content
const match = textContent.match(regex);
if (match) {
let s= code2LatLon(match[1],match[2],match[3]);
addToggle(longDescriptionTag,"Reverse wherigo", s.lat.toString() + ", " + s.lon.toString(), onClickHandler, false);
}
}
if (!noteTag) {
console.error('No <srOnlyCacheNote> tag found');
return false;
} else {
// Get the text content of the tag
const textContent = noteTag.innerText;
// Regular expression to match three consecutive six-digit numbers,
// allowing separators like commas, spaces, and newlines
const regex = /\b(\d{6})(?:[ ,\n]+)(\d{6})(?:[ ,\n]+)(\d{6})\b/;
// Test if the pattern exists in the text content
const match = textContent.match(regex);
if (match) {
let s= code2LatLon(match[1],match[2],match[3]);
addToggle(longDescriptionTag,"Reverse wherigo2", s.lat.toString() + ", " + s.lon.toString(), onClickHandler, false);
}
}
}
})();