Last active
August 6, 2026 14:05
-
-
Save mrummuka/ff97b9e85cf8c3165df3f6b1a8760084 to your computer and use it in GitHub Desktop.
Reversewigo autosolver - Tampermonkey script to automatically solve reverse wherigos on geocaching.com page
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 Reversewigo solver ✓🐵 | |
| // @namespace http://tampermonkey.net/ | |
| // @version 0.40.4 | |
| // @description Reversewigo solver ✓🐵 - solve reverse wherigo automatically on cache page. Solver code originated from Rick Rickhardson's reverse-wherigo source (public domain). Also, creates show source button for mystery cacahes. | |
| // @author mrummuka@hotmail.com | |
| // @homepageURL https://gist.github.com/mrummuka/ff97b9e85cf8c3165df3f6b1a8760084 | |
| // @updateURL https://gist.githubusercontent.com/mrummuka/ff97b9e85cf8c3165df3f6b1a8760084/raw/reversewigo-solver-monkey.user.js | |
| // @downloadURL https://gist.githubusercontent.com/mrummuka/ff97b9e85cf8c3165df3f6b1a8760084/raw/reversewigo-solver-monkey.user.js | |
| // @grant GM_registerMenuCommand | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @grant GM.getValue | |
| // @grant GM.setValue | |
| // @grant GM_setClipboard | |
| // @match https://*.geocaching.com/geocache/* | |
| // @match https://*.geocaching.com/*/cache_details* | |
| // @require https://openuserjs.org/src/libs/sizzle/GM_config.js | |
| // @require https://cdn.jsdelivr.net/gh/xxjapp/xdialog@3.4.0/xdialog.js#sha256=r4yDdWZiML48gILBpDiF+c3/aq7ljwt7wRCtqzl6AvI= | |
| // @resource IMPORTED_CSS https://cdn.jsdelivr.net/gh/xxjapp/xdialog@3/xdialog.css | |
| // @grant GM_getResourceText | |
| // @grant GM_addStyle | |
| // | |
| // ==/UserScript== | |
| let DEBUG = true; | |
| GM_registerMenuCommand("Configure", () => { gmc.open() }, "C"); | |
| GM_registerMenuCommand("Set codes", () => { readCodes() }, "S"); | |
| // escape hatch: run the solver even when this page was not detected as a Wherigo cache | |
| // (cache type detection depends on geocaching.com markup, which changes over time) | |
| GM_registerMenuCommand("Force run (ignore cache type)", () => { runWherigoSolver() }, "F"); | |
| let minmaxRangeObj; | |
| let gmc = new GM_config( | |
| { | |
| 'id': 'MyConfig', // The id used for this instance of GM_config | |
| 'title': 'Script Settings', // Panel Title | |
| 'fields': // Fields object | |
| { | |
| 'OutputDD': | |
| { | |
| 'label': 'Show dd (decimal degrees) format (60.12345 25.4321) coordinates in output', // Appears next to field | |
| 'type': 'checkbox', // Makes this setting a checkbox input | |
| 'default': false // Default value if user doesn't change it | |
| }, | |
| 'OutputCodes': | |
| { | |
| 'label': 'Show wherigo codes in output', // Appears next to field | |
| 'type': 'checkbox', // Makes this setting a checkbox input | |
| 'default': false // Default value if user doesn't change it | |
| } | |
| , | |
| 'OutputDistance': | |
| { | |
| 'label': 'Show distance to bogus in output', // Appears next to field | |
| 'type': 'checkbox', // Makes this setting a checkbox input | |
| 'default': false // Default value if user doesn't change it | |
| }, | |
| 'replaceInfoHTML': | |
| { | |
| 'label': 'Replace HTML element with content? (default: only append)', // Appears next to field | |
| 'type': 'checkbox', // Makes this setting a checkbox input | |
| 'default': false // Default value if user doesn't change it | |
| }, | |
| 'infoHtmlElID': | |
| { | |
| 'label': 'HTML element id for info', // Appears next to field | |
| 'type': 'text', // Makes this setting a checkbox input | |
| 'default': "ctl00_ContentBody_CacheInformationTable" // Default value if user doesn't change it | |
| }, | |
| 'replaceRegexHTML': | |
| { | |
| 'label': 'Replace HTML element with regexp result? (default: only append)', // Appears next to field | |
| 'type': 'checkbox', // Makes this setting a checkbox input | |
| 'default': false // Default value if user doesn't change it | |
| }, | |
| 'regexHtmlElID': | |
| { | |
| 'label': ' HTML element id for regexp results', // Appears next to field | |
| 'type': 'text', // Makes this setting a checkbox input | |
| 'default': "ctl00_ContentBody_detailWidget" // Default value if user doesn't change it | |
| } | |
| , | |
| 'addBogusDDToPage': | |
| { | |
| 'label': 'Show bogus in DD', // Appears next to field | |
| 'type': 'checkbox', // Makes this setting a checkbox input | |
| 'default': true // Default value if user doesn't change it | |
| }, | |
| 'addMinMaxLatLonToPage': | |
| { | |
| 'label': 'Show min/max lon/lat', // Appears next to field | |
| 'type': 'checkbox', // Makes this setting a checkbox input | |
| 'default': true // Default value if user doesn't change it | |
| }, | |
| 'addMinMaxLatLonDDToPage': | |
| { | |
| 'label': 'Show min/max lon/lat also in DD', // Appears next to field | |
| 'type': 'checkbox', // Makes this setting a checkbox input | |
| 'default': true // Default value if user doesn't change it | |
| }, | |
| 'parsedBogusOverride': | |
| { | |
| 'label': 'Manual override for bogus coordinates (use dd syntax eg. 60.1234 24.42134).', // Appears next to field | |
| 'type': 'text', // Makes this setting a text input | |
| 'default': "" // Default value if user doesn't change it | |
| }, | |
| 'writeFinalToClipboard': | |
| { | |
| 'label': 'Write final coordinates directly to clipboard?', // Appears next to field | |
| 'type': 'checkbox', // Makes this setting a checkbox input | |
| 'default': true // Default value if user doesn't change it | |
| }, | |
| 'autoUpdateCoordinates': | |
| { | |
| 'label': 'Automatically write the solved coordinates into the corrected coordinates (the "Set corrected coordinates" button works regardless)', // Appears next to field | |
| 'type': 'checkbox', // Makes this setting a checkbox input | |
| 'default': true // Default value if user doesn't change it | |
| } | |
| }, | |
| 'events': { | |
| 'init': () => { | |
| // initialization complete | |
| // value is now available | |
| gmc.initializedResolve && gmc.initializedResolve(); | |
| gmc.initializedResolve = null; | |
| } | |
| } | |
| }); | |
| gmc.initialized = new Promise(r=>(gmc.initializedResolve=r)); | |
| async function dd2dm(latdd, londd) { | |
| if(DEBUG) console.debug("Starting dd2dm", latdd, londd); | |
| var lat = latdd; | |
| var lon = londd; | |
| var latResult, lonResult, dmsResult; | |
| lat = parseFloat(lat); | |
| lon = parseFloat(lon); | |
| latResult = await lat_dd2dm(lat); | |
| lonResult = await lon_dd2dm(lon); | |
| // Joining both variables and separate them with a space. | |
| dmsResult = latResult + ' ' + lonResult; | |
| // Return the resultant string | |
| if(DEBUG) console.debug("Finishing dd2dm", dmsResult, latResult, lonResult); | |
| return { result: dmsResult, lat: latResult, lon: lonResult }; | |
| } | |
| async function lat_dd2dm(val) { | |
| var valDeg, valMin, result; | |
| val = Number(val); | |
| result = (val >= 0)? 'N' : 'S'; | |
| result += " "; | |
| val = Math.abs(val); | |
| valDeg = Math.floor(val); | |
| if( valDeg < 10 ) { | |
| result += "0" + valDeg; | |
| } | |
| else { | |
| result += valDeg; | |
| } | |
| result += "\xB0"; | |
| result += " "; | |
| valMin = ((val - valDeg) * 60).toFixed(3); | |
| if( valMin < 10) { | |
| result += "0" + valMin; | |
| } | |
| else { | |
| result += valMin; | |
| } | |
| return result; | |
| } | |
| async function lon_dd2dm(val) { | |
| var valDeg, valMin, result; | |
| val = Number(val); | |
| result = (val >= 0)? 'E' : 'W'; | |
| result += " "; | |
| val = Math.abs(val); | |
| valDeg = Math.floor(val); | |
| if( valDeg < 10 ) { | |
| result += "00" + valDeg; | |
| } | |
| else if( valDeg < 100) { | |
| result += "0" + valDeg; | |
| } | |
| else { | |
| result += valDeg; | |
| } | |
| result += "\xB0"; | |
| result += " "; | |
| valMin = ((val - valDeg) * 60).toFixed(3); | |
| if( valMin < 10) { | |
| result += "0" + valMin; | |
| } | |
| else { | |
| result += valMin; | |
| } | |
| return result; | |
| } | |
| // convert latitude longitude array [ lat side (N/S), lat deg, lat mindec, lon side (E/W), lon deg, lon mindec ] to dd | |
| // return { lat: latitude dd , lon: longitude dd } | |
| async function bogusStr2DD( coordsStr ) { | |
| let coordsArr = coordsStr.split(" "); | |
| let lat = await dm2dd( coordsArr[0], coordsArr[1], coordsArr[2] ); | |
| let lon = await dm2dd( coordsArr[3], coordsArr[4], coordsArr[5] ); | |
| return { lat: lat, lon: lon }; | |
| } | |
| async function coordStr2DD( coordsStr ) { | |
| let coordsArr = coordsStr.split(" "); | |
| let lat = await dm2dd( "N", coordsArr[0], coordsArr[1] ); | |
| let lon = await dm2dd( "E", coordsArr[2], coordsArr[3] ); | |
| return { lat: lat, lon: lon }; | |
| } | |
| // calculate minimum possible latitude (usually 3200m south from bogus) | |
| // @return latitude in DD | |
| async function minLat(crd, distance) { | |
| console.debug("Starting minLat"); | |
| let res = await haversineProjection( crd.lat, crd.lon, 180, distance ); | |
| console.debug( JSON.stringify(res) ); | |
| return res[0]; | |
| } | |
| // calculate maximum possible latitude (3200m north from bogus) | |
| // @return latitude in DD | |
| async function maxLat(crd, distance) { | |
| console.debug("Starting maxLat"); | |
| let res = await haversineProjection( crd.lat, crd.lon, 0, distance ); | |
| console.debug( JSON.stringify(res) ); | |
| return res[0]; | |
| } | |
| // calculate minimum possible longitude (3200m west from bogus) | |
| // @return longitude in DD | |
| async function minLon(crd, distance) { | |
| console.debug("Starting minLon"); | |
| let res = await haversineProjection( crd.lat, crd.lon, 270, distance ); | |
| console.debug( JSON.stringify(res) ); | |
| return res[1]; | |
| } | |
| // calculate maximum possible longitude (3200m east from bogus) | |
| // @return longitude in DD | |
| async function maxLon(crd, distance) { | |
| console.debug("Starting maxLon"); | |
| let res = await haversineProjection( crd.lat, crd.lon, 90, distance ); | |
| console.debug( JSON.stringify(res) ); | |
| return res[1]; | |
| } | |
| // returns { minLat, minLon, maxLat, maxLon } ie | |
| // minimum and maximum latitude/longitude values with (usually) 3.2km from bogus | |
| async function minmaxRange(crd, distance) { | |
| console.debug("Starting minmaxRange for", crd, " at ", distance); | |
| let minLatVal = await minLat(crd, distance); | |
| let maxLatVal = await maxLat(crd, distance); | |
| let minLonVal = await minLon(crd, distance); | |
| let maxLonVal = await maxLon(crd, distance); | |
| let minmax = { | |
| minLat : minLatVal, | |
| minLon : minLonVal, | |
| maxLat : maxLatVal, | |
| maxLon : maxLonVal, | |
| distance : distance, | |
| origin_lat : crd.lat, | |
| origin_lon : crd.lon } | |
| return minmax; | |
| } | |
| /********* | |
| * calculate min and max possible lat & lon | |
| */ | |
| async function minmaxLatLon2DDM( minmaxObj ) { | |
| console.debug("Starting minmaxLatLon2DDM() for", minmaxObj) | |
| let minLatDDM = await lat_dd2dm(minmaxObj.minLat) | |
| let maxLatDDM = await lat_dd2dm(minmaxObj.maxLat) | |
| let minLonDDM = await lon_dd2dm(minmaxObj.minLon); | |
| let maxLonDDM = await lon_dd2dm(minmaxObj.maxLon); | |
| return { minLat : minLatDDM, minLon : minLonDDM, maxLat : maxLatDDM, maxLon : maxLonDDM } | |
| } | |
| async 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 fix(v, left, right) { | |
| return (v < 0 ? '-' : ' ') + Math.abs(v).toFixed(right).padStart(left + right + 1, '0'); | |
| } | |
| async function haversineDistance(lat1,lon1,lat2,lon2) { | |
| const R = 6371e3; // metres | |
| const φ1 = lat1 * Math.PI/180; // φ, λ in radians | |
| const φ2 = lat2 * Math.PI/180; | |
| const Δφ = (lat2-lat1) * Math.PI/180; | |
| const Δλ = (lon2-lon1) * Math.PI/180; | |
| const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) + | |
| Math.cos(φ1) * Math.cos(φ2) * | |
| Math.sin(Δλ/2) * Math.sin(Δλ/2); | |
| const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); | |
| const d = R * c; // in metres | |
| return d; | |
| } | |
| const degrees_to_radians = deg => (deg * Math.PI) / 180.0; | |
| const radians_to_degrees = rad => (rad * 180.0) / Math.PI; | |
| // φ is latitude, λ is longitude, | |
| async function haversineProjection(lat1, lon1, degr, dist) { | |
| const R = 6371e3; // metres | |
| const φ1 = lat1 * Math.PI/180; // φ, λ in radians | |
| const λ1 = lon1 * Math.PI/180; | |
| let θ = degrees_to_radians(degr); | |
| let d = dist; | |
| const δ = d/R; | |
| const Δφ = δ * Math.cos(θ); | |
| // not const: the pole normalisation below reassigns it | |
| let φ2 = φ1 + Δφ; | |
| const Δψ = Math.log(Math.tan(φ2/2+Math.PI/4)/Math.tan(φ1/2+Math.PI/4)); | |
| const q = Math.abs(Δψ) > 10e-12 ? Δφ / Δψ : Math.cos(φ1); // E-W course becomes ill-conditioned with 0/0 | |
| const Δλ = δ*Math.sin(θ)/q; | |
| const λ2 = λ1 + Δλ; | |
| // check for some daft bugger going past the pole, normalise latitude if so | |
| if (Math.abs(φ2) > Math.PI/2) φ2 = φ2>0 ? Math.PI-φ2 : -Math.PI-φ2; | |
| let lat2 = φ2 / Math.PI * 180; | |
| let lon2 = λ2 / Math.PI * 180; | |
| return [ lat2 , lon2 ] | |
| } | |
| async function dm2dd( nsew, deg, min ) { | |
| let sign; | |
| if( nsew == 'N' || nsew == 'E' ) { | |
| sign = 1; | |
| } | |
| else { | |
| sign = -1; | |
| } | |
| let numdeg = parseInt(deg); | |
| let nummin = parseFloat(min); | |
| let dd = sign * (numdeg + nummin/60); | |
| return dd; | |
| } | |
| function escapeHtml(str) { | |
| return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') | |
| } | |
| async function createShowSourceButton() { | |
| console.debug("Starting createShowSourceButton"); | |
| let mybutton = document.createElement("srcbutton"); | |
| mybutton.innerHTML = "View source"; | |
| mybutton.setAttribute("style", "-webkit-appearance: button;-moz-appearance: button;appearance: button; background-color: green; color: white; border: none; text-align: center; display: inline-block; padding: 3px 10px; border: 1px solid black; cursor: pointer;"); | |
| let noteref = document.getElementsByClassName("minorCacheDetails Clear")[0]; | |
| noteref.appendChild(mybutton); | |
| mybutton.addEventListener("click", function() { | |
| var desc1 = document.getElementById("ctl00_ContentBody_ShortDescription").innerHTML; | |
| var desc2 = document.getElementById("ctl00_ContentBody_LongDescription").innerHTML; | |
| xdialog.open({ | |
| title: 'UserSuppliedContent from source', | |
| body: '\ | |
| <div style="height: auto; border: 1px solid green; padding: 0.5em;">\ | |
| <pre>'+escapeHtml(desc1)+"\n"+escapeHtml(desc2)+ | |
| '</pre>\ | |
| </div>', | |
| style: 'width: 80%;' | |
| }); | |
| }); | |
| } | |
| async function createLaunchRwigoButton() { | |
| console.debug("Starting launchRwigoButton"); | |
| await gmc.initialized.then(); | |
| let mybuttonRW = document.createElement("rwigobutton"); | |
| mybuttonRW.innerHTML = "RWIGO"; | |
| mybuttonRW.setAttribute("style", "-webkit-appearance: button;-moz-appearance: button;appearance: button; background-color: green; color: white; border: none; text-align: center; display: inline-block; padding: 3px 10px; border: 1px solid black; cursor: pointer;"); | |
| let noterefRW = document.getElementsByClassName("minorCacheDetails Clear")[0]; | |
| noterefRW.appendChild(mybuttonRW); | |
| mybuttonRW.addEventListener("click", function() { | |
| rwigoAssistDialog(); | |
| }); | |
| } | |
| async function createShowSolveREReverseButton() { | |
| console.debug("Starting createShowSolveREReverseButton"); | |
| // let mybutton = document.createElement("solvebutton"); | |
| let mybutton = document.createElement("a"); | |
| mybutton.innerHTML = "Solve Regexp"; | |
| mybutton.className = "mybtn" | |
| //css | |
| mybutton.setAttribute("style", "-webkit-appearance: button;-moz-appearance: button;appearance: button; background-color: green; color: white; border: none; text-align: center; display: inline-block; padding: 3px 10px; border: 1px solid black; cursor: pointer;"); | |
| let noteref = document.getElementsByClassName("minorCacheDetails Clear")[0]; | |
| noteref.appendChild(mybutton); | |
| mybutton.addEventListener("click", function() { | |
| solveReverseRE(); | |
| }); | |
| } | |
| async function createDialog( ) { | |
| console.debug("Starting createDialog"); | |
| await gmc.initialized.then(); | |
| let mydialog = document.createElement("div"); | |
| mydialog.id = "dialog"; | |
| mydialog.title = "Results"; | |
| let resultmsg = document.createElement("p"); | |
| resultmsg.id = "resultmsg"; | |
| mydialog.appendChild(resultmsg); | |
| // initialization complete | |
| // value is now available | |
| let reElID = gmc.get('regexHtmlElID'); | |
| console.debug("rexegHtmlElID", reElID); | |
| // regexHtmlElID by default ctl00_ContentBody_detailWidget | |
| let noteref = document.getElementById( reElID ); | |
| let replace = gmc.get('replaceInfoHTML'); | |
| if( replace ) { | |
| noteref = mydialog; | |
| } | |
| else { | |
| noteref.appendChild(mydialog); | |
| } | |
| } | |
| // How long to wait for geocaching.com's coordinate editor to open before giving up. | |
| const COORD_EDITOR_TIMEOUT_MS = 15000; | |
| const COORD_POLL_INTERVAL_MS = 250; | |
| // Wait for the corrected-coordinates editor to appear, with a deadline. | |
| // | |
| // The predecessor of this function retried every second and never stopped. When the editor | |
| // did not open - which is exactly what happens once the edit button has been broken, or | |
| // while the editor's script is still loading - the timer stayed armed for the life of the | |
| // page. It then fired the instant the *user* opened the editor to type their own final | |
| // coordinates, overwrote what they had typed and clicked submit for them. A poller that | |
| // cannot give up eventually fires at the worst possible moment. | |
| // | |
| // returns { field, submit }, or null if the editor never opened | |
| async function waitForCoordinateEditor( timeoutMs = COORD_EDITOR_TIMEOUT_MS ) { | |
| let deadline = Date.now() + timeoutMs; | |
| for(;;) { | |
| let field = document.querySelector('.cc-parse-text'); | |
| let submit = document.getElementsByClassName("btn-cc-parse")[0]; | |
| if( field != null && submit != null ) { | |
| return { field: field, submit: submit }; | |
| } | |
| if( Date.now() >= deadline ) { | |
| return null; | |
| } | |
| await new Promise( r => setTimeout(r, COORD_POLL_INTERVAL_MS) ); | |
| } | |
| } | |
| // Set a framework-controlled field: assign through the native value setter, then announce | |
| // the change so the page's own state tracking sees it. Works for input and textarea alike - | |
| // calling the HTMLInputElement setter on a textarea throws. | |
| function setCoordinateField( field, value ) { | |
| let proto = (typeof window.HTMLTextAreaElement !== "undefined" | |
| && field instanceof window.HTMLTextAreaElement) | |
| ? window.HTMLTextAreaElement.prototype | |
| : window.HTMLInputElement.prototype; | |
| let nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set; | |
| if( nativeSetter ) { | |
| nativeSetter.call(field, value); | |
| } | |
| else { | |
| field.value = value; | |
| } | |
| field.dispatchEvent(new Event('input', { bubbles: true })); | |
| field.dispatchEvent(new Event('change', { bubbles: true })); | |
| } | |
| // Fill geocaching.com's corrected-coordinates editor with newCoords and submit it. | |
| // returns true when the coordinates were submitted, false when the editor could not be used | |
| async function updateCoordinates( newCoords ) { | |
| console.debug("Starting updateCoordinates", newCoords); | |
| let crd = document.getElementById("uxLatLonLink"); | |
| if( crd === null ) { | |
| console.warn("No #uxLatLonLink on the page - cannot set corrected coordinates"); | |
| return false; | |
| } | |
| crd.click(); | |
| let editor = await waitForCoordinateEditor(); | |
| if( editor === null ) { | |
| console.warn("Coordinate editor did not open within " + COORD_EDITOR_TIMEOUT_MS | |
| + "ms - giving up rather than waiting to overwrite it later. " | |
| + "Use the \"Set corrected coordinates\" button to retry."); | |
| return false; | |
| } | |
| setCoordinateField( editor.field, newCoords ); | |
| editor.submit.click(); | |
| console.debug("Submitted corrected coordinates: " + newCoords); | |
| return true; | |
| } | |
| // A button the user can press to write the solved coordinates into the corrected | |
| // coordinates field. This is the path that always works: the page has finished loading, | |
| // geocaching.com's coordinate editor has certainly initialised by then, and it is a real | |
| // click. The automatic write on page load is only a convenience on top of it. | |
| // returns the status element, so the caller can report on the automatic attempt | |
| async function addSetCoordinatesButton( resultDMD ) { | |
| await gmc.initialized.then(); | |
| let docEl = document.getElementById( gmc.get('infoHtmlElID') ); | |
| if( docEl === null ) { | |
| console.error("Nowhere to put the \"Set corrected coordinates\" button"); | |
| return { innerText: "" }; | |
| } | |
| let holder = rwigoOutputContainer( docEl ); | |
| let wrap = document.createElement("div"); | |
| let btn = document.createElement("button"); | |
| btn.type = "button"; | |
| btn.id = "rwigo-set-coords"; | |
| btn.innerText = "Set corrected coordinates"; | |
| btn.setAttribute("style", "background-color: green; color: white; text-align: center; display: inline-block; padding: 3px 10px; border: 1px solid black; cursor: pointer;"); | |
| let status = document.createElement("span"); | |
| status.id = "rwigo-set-coords-status"; | |
| btn.addEventListener("click", async function() { | |
| status.innerText = " setting..."; | |
| let ok = await updateCoordinates( resultDMD ); | |
| status.innerText = ok ? " done" : " the coordinate editor did not open - try again"; | |
| }); | |
| wrap.appendChild(btn); | |
| wrap.appendChild(status); | |
| holder.appendChild(wrap); | |
| return status; | |
| } | |
| // Show the solved coordinates and offer to write them to the cache. | |
| async function presentFinalCoordinates( resultDMD ) { | |
| await addHTMLtoPage("<br><strong> Reversewigo final coordinates = " + resultDMD + " </strong>" ); | |
| // any NaN in the output means the codes did not convert - nothing worth writing | |
| if( resultDMD.indexOf("NaN") >= 0 ) { | |
| console.warn("Not offering to set coordinates, solver produced: " + resultDMD); | |
| return; | |
| } | |
| let status = await addSetCoordinatesButton( resultDMD ); | |
| if( gmc.get('autoUpdateCoordinates') ) { | |
| let ok = await updateCoordinates( resultDMD ); | |
| if( !ok ) { | |
| status.innerText = " automatic update failed - press the button to set them"; | |
| } | |
| } | |
| } | |
| async function getBogusStringFromPage() { | |
| console.debug("Starting getBogusStringFromPage"); | |
| let uxlatlonel = document.getElementById("uxLatLon") | |
| if ( uxlatlonel != null ) { | |
| return uxlatlonel.textContent; | |
| } | |
| console.error("uxLatLon element not found") | |
| throw new Error("uxLatLon element not found"); | |
| } | |
| // retrieve bogus coordinates in ddm | |
| // returns array [ lat side (N/S), lat deg, lat mindec, lon side (E/W), lon deg, lon mindec ]; | |
| async function getBogusCoordsFromPage() { | |
| console.debug("Starting getBogusCoordsFromPage"); | |
| let uxlatlonText = await getBogusStringFromPage(); | |
| if(uxlatlonText != null ) { | |
| let coords = uxlatlonText.split(" "); | |
| if( coords.length != 6 ) { | |
| console.error("Error parsing coordinates"); | |
| throw new Error("uxLatLon element not found"); | |
| } | |
| else { | |
| return coords; | |
| } | |
| } | |
| } | |
| async function getBogusCoordsDD() { | |
| if(DEBUG) console.debug("Starting getBogusCoordsDD"); | |
| await gmc.initialized.then(); | |
| // checking if we have user override for parsed bogus coordinates | |
| let userOverrideBogus = gmc.get('parsedBogusOverride'); | |
| if( userOverrideBogus == "" ) { | |
| if(DEBUG) console.debug("No user override for bogus") | |
| let bogusLatLonArr = await getBogusCoordsFromPage(); | |
| if(DEBUG) console.debug("Got bogus", JSON.stringify(bogusLatLonArr)); | |
| // convert bogus coordinates from deg-min-dec to deg-dec | |
| let bogusDD = await latLonDmArr2dd(bogusLatLonArr); | |
| if(DEBUG) console.debug("Got bogus", JSON.stringify(bogusDD)); | |
| return bogusDD; | |
| } | |
| else { | |
| //bogusDD.lat + " " + bogusDD.lon | |
| if(DEBUG) console.debug("Using parsedBogusOverride", userOverrideBogus); | |
| let userOverrideBoguslat = userOverrideBogus.trim().split(" ")[0]; | |
| let userOverrideBoguslon = userOverrideBogus.trim().split(" ")[1]; | |
| let userOverrideBogusObj = { | |
| lat: userOverrideBoguslat, | |
| lon: userOverrideBoguslon | |
| }; | |
| if(DEBUG) console.debug("Got userOverrideBogusObj", JSON.stringify(userOverrideBogusObj) ); | |
| return userOverrideBogusObj; | |
| } | |
| } | |
| // convert latitude longitude array [ lat side (N/S), lat deg, lat mindec, lon side (E/W), lon deg, lon mindec ] to dd | |
| // return { lat: latitude dd , lon: longitude dd } | |
| async function latLonDmArr2dd( latLonDmArray ) { | |
| console.debug("Starting latLonDmArr2dd", latLonDmArray); | |
| // convert bogus coordinates from deg-mindec to deg-dec | |
| let bogusLat = await dm2dd( latLonDmArray[0], latLonDmArray[1], latLonDmArray[2] ); | |
| let bogusLon = await dm2dd( latLonDmArray[3], latLonDmArray[4], latLonDmArray[5] ); | |
| return { lat : bogusLat, lon : bogusLon } | |
| } | |
| // Our own output container inside the target element. | |
| // | |
| // Never write to the target's innerHTML directly. #uxLatLonLink - the button that opens | |
| // geocaching.com's corrected-coordinates editor - sits inside the default target | |
| // #ctl00_ContentBody_CacheInformationTable. Assigning innerHTML serializes and reparses | |
| // the whole container, so every descendant is replaced by a fresh node and every directly | |
| // bound listener is thrown away. geocaching.com binds that listener from | |
| // GEO_singlepageapp.js, which loads asynchronously from another host, so whether it binds | |
| // before or after us is a race that resolves differently on different machines. Losing it | |
| // leaves the user unable to edit the final coordinates at all. | |
| // | |
| // Appending to a container of our own touches no existing node, so the race disappears. | |
| function rwigoOutputContainer( docEl ) { | |
| let holder = document.getElementById("rwigo-output"); | |
| if( holder === null || !docEl.contains( holder ) ) { | |
| holder = document.createElement("div"); | |
| holder.id = "rwigo-output"; | |
| docEl.appendChild( holder ); | |
| } | |
| return holder; | |
| } | |
| // htmlsnip = html snippet in string | |
| // replaceInfoHTML config replaces *our own* previous output, never the cache page itself | |
| async function addHTMLtoPage( htmlsnip ) { | |
| console.debug("Starting addHTMLtoPage", htmlsnip); | |
| await gmc.initialized.then(); | |
| let docEl = document.getElementById( gmc.get('infoHtmlElID') ); | |
| if( docEl === null ) { | |
| console.error("Cannot add output: no element with id " + gmc.get('infoHtmlElID')); | |
| return; | |
| } | |
| let holder = rwigoOutputContainer( docEl ); | |
| let replace = gmc.get('replaceInfoHTML') | |
| if( replace ) { | |
| holder.innerHTML = htmlsnip; | |
| } | |
| else { | |
| // insertAdjacentHTML parses only the new fragment - existing children are untouched | |
| holder.insertAdjacentHTML('beforeend', htmlsnip); | |
| } | |
| } | |
| function onlyUnique(value, index, self) { | |
| return self.indexOf(value) === index; | |
| } | |
| async function* makeRangeIterator(start = 0, end = 100, step = 1) { | |
| let iterationCount = 0; | |
| for (let i = start; i < end; i += step) { | |
| iterationCount++; | |
| yield i; | |
| } | |
| return iterationCount; | |
| } | |
| async function* makeRangeIterator2(variables = 1, start = 0, end = 100, step = 1) { | |
| let iterationCount = 0; | |
| for (let i = start; i < end; i += step) { | |
| iterationCount++; | |
| yield i; | |
| } | |
| return iterationCount; | |
| } | |
| async function testGenerator() { | |
| const it = await makeRangeIterator(0, 9, 1); | |
| let result = it.next(); | |
| while (!result.done) { | |
| console.log(result.value); // 0 1 2 3 4 5 6 7 8 9 | |
| result = it.next(); | |
| } | |
| } | |
| // Works in Chrome devtools console | |
| async function* codeGenerator ( codes, vars ) { | |
| // check if no variables left => return final codes | |
| console.debug("Vars.length = " + vars.length); | |
| if( vars == null || vars.length == 0 ) { | |
| console.debug("Yield" + JSON.stringify(codes)); | |
| yield codes; | |
| } | |
| else { | |
| // generate regexp to replace VAR => number | |
| let replRE = new RegExp("/"+vars[0]+"/g"); | |
| console.debug("RE " + JSON.stringify(replRE)); | |
| // for each var value [0,9] | |
| for(let i=0; i<10; i++ ) { | |
| let c = []; | |
| c[0] = codes[0].replace(vars[0], i.toString()); | |
| c[1] = codes[1].replace(vars[0], i.toString()); | |
| c[2] = codes[2].replace(vars[0], i.toString()); | |
| console.debug("/"+vars[0]+"/g -> " + JSON.stringify(c)); | |
| // recursively get the other variables processed | |
| yield* await codeGenerator( c, vars.slice(1) ); | |
| } | |
| } | |
| } | |
| // replace char at position index for string with replacement | |
| String.prototype.replaceAt = function(index, replacement) { | |
| return this.substr(0, index) + replacement + this.substr(index + replacement.length); | |
| } | |
| async function solveReverseRE() { | |
| console.debug("Starting solveReverseRE"); | |
| await gmc.initialized.then(); | |
| //TODO: replace with loadCodes() parseCodes() | |
| console.debug("Attempting to parse codes from note."); | |
| let resultmsg = document.getElementById("resultmsg"); | |
| resultmsg.innerText = "Generated coordinates:\n"; | |
| let codes = await parseCodesFromNote(); | |
| // Nothing usable in the Personal Note - fall back to the cache description, allowing | |
| // codes with variables so that partially unknown codes can be brute forced without | |
| // having to copy them into the note first | |
| if( codes === null || codes.length < 3 ) { | |
| console.debug("No codes in Personal Note - trying the cache description"); | |
| codes = await parseCodesFromDescription( true ); | |
| } | |
| if( codes === null || codes.length < 3 ) { | |
| console.error("Error: fewer than 3 codes found:" + JSON.stringify(codes)); | |
| resultmsg.innerText = "Error: no reverse wherigo codes found in Personal Note or cache description."; | |
| return; | |
| } | |
| if( codes.length > 3 ) { | |
| console.warn("More than 3 codes found, using the first three: " + JSON.stringify(codes)); | |
| } | |
| codes = codes.slice(0, 3); | |
| console.debug("Parsed: " + JSON.stringify(codes)); | |
| // TODO: remove vars from non meaningful items : B4 C4 = ALWAYS ignore | |
| console.log("Emptying non-meaninful digits B4 C4"); | |
| codes[1] = codes[1].replaceAt( 6-4, "0"); //reset B4 | |
| codes[2] = codes[2].replaceAt( 6-4, "0"); //reset C4 | |
| console.debug("Cleansed: " + JSON.stringify(codes)); | |
| // TODO end loadCodes() | |
| // TODO start regexp from here | |
| // calculate # of variables/unknowns per code | |
| let vars = []; | |
| for(let i=0; i<3; i++) { | |
| let matching = codes[i].match(/[A-Za-z]/g); | |
| if(matching != null) { | |
| vars = vars.concat( matching ); | |
| } | |
| } | |
| console.debug("Vars: " + JSON.stringify(vars)); | |
| let uniqVars = vars.filter(onlyUnique); | |
| console.log(uniqVars.length + " unique variables"); | |
| console.warn("Possible total combos: " + 10**uniqVars.length); | |
| // Last line of defence: whatever the codes came from, never start a brute force that | |
| // cannot finish. Without this the tab freezes hard and the user loses the page. | |
| if( uniqVars.length > MAX_UNKNOWN_DIGITS ) { | |
| let msg = "Error: " + uniqVars.length + " unknown digits (" + (10**uniqVars.length) | |
| + " combinations) is too many to brute force - check the codes: " | |
| + JSON.stringify(codes); | |
| console.error(msg); | |
| resultmsg.innerText = msg; | |
| return; | |
| } | |
| let bogusDD = await getBogusCoordsDD(); | |
| console.debug("Converted coords: " + bogusDD.lat + " " + bogusDD.lon); | |
| console.debug("Iterating..."); | |
| console.log("code1,code2,code3,lat(dd),lon(dd),lat(gps),lon(gps),dist(m)"); | |
| let counter = 0; | |
| let codeGen = await codeGenerator( codes, uniqVars ); | |
| let nextCode = await codeGen.next(); | |
| console.debug(JSON.stringify(nextCode)); | |
| while (!nextCode.done) { | |
| // convert to dd | |
| let ddlatlon = await code2LatLon( parseInt( nextCode.value[0] ), parseInt( nextCode.value[1] ) , parseInt( nextCode.value[2]) ); | |
| console.debug("Convertion results: " + JSON.stringify(ddlatlon)); | |
| if( isNaN(ddlatlon.lat) || isNaN(ddlatlon.lon) ) continue; | |
| else { | |
| let dist = await haversineDistance( bogusDD.lat, bogusDD.lon, ddlatlon.lat, ddlatlon.lon ); | |
| console.debug("distance:", dist); | |
| if( dist < 3220 ) { | |
| counter = counter+1; | |
| let dmlatlon = await dd2dm( ddlatlon.lat, ddlatlon.lon); | |
| let result = nextCode.value[0] + "," + nextCode.value[1] + "," + nextCode.value[2] + "," + fix(ddlatlon.lat, 2, 6) + "," + fix(ddlatlon.lon, 3, 6) + "," + dmlatlon.lat + "," + dmlatlon.lon + "," + dist.toFixed(0) ; | |
| console.log(result); | |
| let result2 = ""; | |
| // if config output DD on, print also DD coords to output (makes it easier to deduce some variables) | |
| if( gmc.get('OutputDD') ) { | |
| result2 = fix(ddlatlon.lat, 2, 6) + " " + fix(ddlatlon.lon, 3, 6) + " | "; | |
| console.debug("OutputDD", result2); | |
| } | |
| // default output | |
| result2 = result2 + dmlatlon.lat + " " + dmlatlon.lon; | |
| // if config option OutputCodes = true, output also calculated wigo codes | |
| if( gmc.get('OutputCodes') ) { | |
| result2 = result2 + " [" + nextCode.value[0] + "," + nextCode.value[1] + "," + nextCode.value[2] + "]" ; | |
| console.debug("OutputCodes", result2); | |
| } | |
| if( gmc.get('OutputDistance') ) { | |
| result2 = result2 + " ⟼ " + dist.toFixed(0) + " m"; | |
| console.debug("OutputDistance", result2); | |
| } | |
| // TODO: to dialog | |
| resultmsg.innerText = resultmsg.innerText + "\n" + result2; | |
| } | |
| /* else { | |
| let dmlatlon = dd2dm( ddlatlon.lat, ddlatlon.lon); | |
| let result = nextCode.value[0] + "," + nextCode.value[1] + "," + nextCode.value[2] + "," + fix(ddlatlon.lat, 2, 6) + "," + fix(ddlatlon.lon, 3, 6) + "," + dmlatlon.lat + "," + dmlatlon.lon + "," + dist.toFixed(0) + "out of range" ; | |
| console.warn(result); | |
| } */ | |
| } | |
| nextCode = await codeGen.next(); | |
| } | |
| console.log("Total hits in range: " + counter); | |
| } | |
| async function addBogusDDToPage() { | |
| if(DEBUG) console.debug("Starting addBogusDDToPage"); | |
| let bogusDD = await getBogusCoordsDD(); | |
| console.debug("Converted coords: " + bogusDD.lat + " " + bogusDD.lon); | |
| // add bogus dd coordinates on page | |
| await addHTMLtoPage( "<strong> Bogus: " + bogusDD.lat + " " + bogusDD.lon + " </strong><br>" ); | |
| } | |
| async function addMinMaxLatLonToPage() { | |
| if(DEBUG) console.debug("Starting addMinMaxLatLonToPage"); | |
| let rwigoBogus = await getBogusStringFromPage(); | |
| if(DEBUG) console.debug("Parsed bogus", rwigoBogus); | |
| let rwigoBogusDD = await bogusStr2DD(rwigoBogus); | |
| let minmaxRangeObj = await minmaxRange(rwigoBogusDD, 3200); | |
| let mm = await minmaxLatLon2DDM( minmaxRangeObj ); | |
| if(DEBUG) console.debug("Lat/minmax range: " + mm.minLat + " " + mm.maxLat); | |
| if(DEBUG) console.debug("Lon/minmax range: " + mm.minLon + " " + mm.maxLon); | |
| if( gmc.get("addMinMaxLatLonDDToPage") ) { | |
| if(DEBUG) console.debug("Lat/minmax range: " + minmaxRangeObj.minLat + " " + minmaxRangeObj.maxLat); | |
| if(DEBUG) console.debug("Lon/minmax range: " + minmaxRangeObj.minLon + " " + minmaxRangeObj.maxLon); | |
| // add bogus dd coordinates on page | |
| await addHTMLtoPage( "<strong> " + mm.minLat + " " + mm.minLon + " (min) </strong> "+ minmaxRangeObj.minLat.toPrecision(7) + " " + minmaxRangeObj.minLon.toPrecision(8)+ "<br>" ); | |
| await addHTMLtoPage( "<strong> " + mm.maxLat + " " + mm.maxLon + " (max) </strong> "+ minmaxRangeObj.maxLat.toPrecision(7) + " " + minmaxRangeObj.maxLon.toPrecision(8)+ "<br>" ); | |
| } | |
| else { | |
| // add bogus dd coordinates on page | |
| await addHTMLtoPage( "<strong> " + mm.minLat + " " + mm.minLon + " (min) </strong><br>" ); | |
| await addHTMLtoPage( "<strong> " + mm.maxLat + " " + mm.maxLon + " (max) </strong><br>" ); | |
| } | |
| } | |
| // takes wigo codes as strings (may consist not numbers (variables) e.g. 022345 042x21 924ab9) | |
| // returns resulting coordinate (DD) string | |
| function evalPartialReverseWigo(codeA, codeB, codeC) { | |
| console.debug("Starting evalPartialReverseWigo", codeA, codeB, codeC); | |
| // validate that these are numbers | |
| console.debug("Input: " + codeA + ", " + codeB + ", " + codeC); | |
| if( isNaN( codeC[6-5] ) == false && isNaN( codeC[6-2] ) == false ) { | |
| // C5 + C2 = parillinen | |
| if( (Number(codeC[6-5]) + Number(codeC[6-2])) % 2 === 0 ) { | |
| console.debug("parillinen"); | |
| // A4 B2 B5 C3 A6 C2 A1 | |
| let resLat = "" + codeA[6-4] + codeB[6-2] + "." + codeB[6-5] + codeC[6-3] + codeA[6-6] + codeC[6-2] + codeA[6-1]; | |
| // A5 C6 C1 B3 B6 A2 C5 B1 | |
| let resLon = "" + codeA[6-5] + codeC[6-6] + codeC[6-1] + "." + codeB[6-3] + codeB[6-6] + codeA[6-2] + codeC[6-5] + codeB[6-1]; | |
| console.debug(resLat + " " + resLon); | |
| return resLat + " " + resLon; | |
| } | |
| // C (d5 + d2) eli C5+C2= pariton | |
| else if ( (Number(codeC[6-5]) + Number(codeC[6-2])) %2 !== 0 ) { | |
| console.debug("pariton"); | |
| // B6 A1 A4 C6 C3 C2 A6 | |
| let resLat = "" + codeB[6-6] + codeA[6-1] + "." + codeA[6-4] + codeC[6-6] + codeC[6-3] + codeC[6-2] + codeA[6-6]; | |
| // B2 C1 A2 A5 B3 B1 C5 B5 | |
| let resLon = "" + codeB[6-2] + codeC[6-1] + codeA[6-2] + "." + codeA[6-5] + codeB[6-3] + codeB[6-1] + codeC[6-5] + codeB[6-5]; | |
| console.debug(resLat + " " + resLon); | |
| return resLat + " " + resLon; | |
| } | |
| } | |
| else { | |
| console.debug("parillinen tai pariton"); | |
| // TODO cannot deduce is many times not correct - by using range for first digits | |
| // it still may be possible to deduce more automatically ! | |
| console.warn("Cannot deduce which equation to use - showing both results"); | |
| let resLat1 = "" + codeA[6-4] + codeB[6-2] + "." + codeB[6-5] + codeC[6-3] + codeA[6-6] + codeC[6-2] + codeA[6-1]; | |
| // A5 C6 C1 B3 B6 A2 C5 B1 | |
| let resLon1 = "" + codeA[6-5] + codeC[6-6] + codeC[6-1] + "." + codeB[6-3] + codeB[6-6] + codeA[6-2] + codeC[6-5] + codeB[6-1]; | |
| let resLat2 = "" + codeB[6-6] + codeA[6-1] + "." + codeA[6-4] + codeC[6-6] + codeC[6-3] + codeC[6-2] + codeA[6-6]; | |
| // B2 C1 A2 A5 B3 B1 C5 B5 | |
| let resLon2 = "" + codeB[6-2] + codeC[6-1] + codeA[6-2] + "." + codeA[6-5] + codeB[6-3] + codeB[6-1] + codeC[6-5] + codeB[6-5]; | |
| console.debug( resLat1 + " " + resLon1 + "\n OR \n" + resLat2 + " " + resLon2 ); | |
| return resLat1 + " " + resLon1 + " <br>OR<br> " + resLat2 + " " + resLon2; | |
| } | |
| // A3 B4 C4 = ALWAYS ignore | |
| } | |
| async function rwigoAssistDialog() { | |
| if(DEBUG) console.debug("Starting rwigoAssistDialog()") | |
| let rwigoBogus = await getBogusStringFromPage(); | |
| if(DEBUG) console.debug("Parsed bogus", rwigoBogus); | |
| let rwigoBogusDD = await bogusStr2DD(rwigoBogus); | |
| minmaxRangeObj = await minmaxRange(rwigoBogusDD, 3200); | |
| let mm = await minmaxLatLon2DDM( minmaxRangeObj ); | |
| await xdialog.open({ | |
| title: 'Reverse Wherigo Solver', | |
| body: '\ | |
| <style>\ | |
| .demo4-items {display:flex;flex-direction:column;align-items:center;}\ | |
| .demo4-item {display:block;height:70px;width:90%;margin:3px;padding:1px;}\ | |
| .demo4-button {display:block;height:30px;width:30%;margin:3px;padding:1px;}\ | |
| .demo4-results {display:flex;flex-direction:column;align-items:center;}\ | |
| </style>\ | |
| <div id="demo4-header" class="demo4-items">\ | |
| <div class="demo4-items">\ | |
| <p>Min: ' | |
| + mm.minLat + " " + mm.minLon + "<p>Max: " + mm.maxLat + " " + mm.maxLon + | |
| '<p>Enter reversewherigo codes below</p>\ | |
| <textarea id="rwigo-codes-input" class="demo4-item"></textarea>\ | |
| </div>\ | |
| <div id="rwigo-codes-result" class="demo4-results"></div>\ | |
| ', | |
| // buttons: ['ok', 'delete', 'cancel'], | |
| buttons: { | |
| delete: "run", | |
| ok: "Close" | |
| }, | |
| style: 'width: 400px;left: 30%;', | |
| listenEnterKey: false, | |
| beforeshow: function(param) { | |
| [].slice.call(param.element.querySelectorAll('.xd-body *')).forEach(function(el) { | |
| let border = false; | |
| if (el instanceof HTMLInputElement) { | |
| border = true; | |
| } | |
| if (['BUTTON', 'SELECT', 'TEXTAREA'].indexOf(el.tagName) >= 0) { | |
| border = true; | |
| } | |
| if (border) { | |
| el.setAttribute('style', 'border: 2px solid green;'); | |
| } | |
| }); | |
| }, | |
| ondelete: function(param) { | |
| console.debug("Starting ondelete", param); | |
| let codesText = param.element.querySelector('#rwigo-codes-input'); | |
| console.debug("xdialog user input:",codesText); | |
| let parsedCodes = parseCodesDialog( codesText.value ); | |
| if( parsedCodes !== null && parsedCodes.length >= 3 ) { | |
| console.debug("Match: " + parsedCodes[0] + "," + parsedCodes[1] + "," + parsedCodes[2] ); | |
| let reverseRes = evalPartialReverseWigo( parsedCodes[0], parsedCodes[1], parsedCodes[2] ); | |
| console.debug("Partial evaluation result:", reverseRes); | |
| let res = param.element.querySelector('#rwigo-codes-result'); | |
| let resp = document.createElement('p'); | |
| resp.textContent = reverseRes; | |
| res.appendChild(resp); | |
| } | |
| return false; | |
| } | |
| }); | |
| } | |
| // DEPRECATED | |
| async function readCodes() { | |
| console.warn("DEPRECATED readCodes()") | |
| if(DEBUG) console.debug("Starting readCodes()") | |
| let rwigoBogus = await getBogusStringFromPage(); | |
| if(DEBUG) console.debug("Parsed bogus", rwigoBogus); | |
| let rwigoBogusDD = await bogusStr2DD(rwigoBogus); | |
| minmaxRangeObj = await minmaxRange(rwigoBogusDD, 3200); | |
| let mm = await minmaxLatLon2DDM( minmaxRangeObj ); | |
| await xdialog.open({ | |
| title: 'Reverse Wherigo Solver', | |
| body: '\ | |
| <style>\ | |
| .demo4-items {display:flex;flex-direction:column;align-items:center;}\ | |
| .demo4-item {display:block;height:70px;width:90%;margin:3px;padding:1px;}\ | |
| .demo4-button {display:block;height:30px;width:30%;margin:3px;padding:1px;}\ | |
| .demo4-results {display:flex;flex-direction:column;align-items:center;}\ | |
| </style>\ | |
| <div id="demo4-header" class="demo4-items">\ | |
| <div class="demo4-items">\ | |
| <p>Min: ' | |
| + mm.minLat + " " + mm.minLon + "<p>Max: " + mm.maxLat + " " + mm.maxLon + | |
| '<p>Enter reversewherigo codes below</p>\ | |
| <textarea id="rwigo-codes-input" class="demo4-item"></textarea>\ | |
| </div>\ | |
| <div id="rwigo-codes-result" class="demo4-results"></div>\ | |
| ', | |
| // buttons: ['ok', 'delete', 'cancel'], | |
| buttons: { | |
| delete: "run", | |
| ok: "Close" | |
| }, | |
| style: 'width: 400px;left: 30%;', | |
| listenEnterKey: false, | |
| beforeshow: function(param) { | |
| [].slice.call(param.element.querySelectorAll('.xd-body *')).forEach(function(el) { | |
| let border = false; | |
| if (el instanceof HTMLInputElement) { | |
| border = true; | |
| } | |
| if (['BUTTON', 'SELECT', 'TEXTAREA'].indexOf(el.tagName) >= 0) { | |
| border = true; | |
| } | |
| if (border) { | |
| el.setAttribute('style', 'border: 2px solid green;'); | |
| } | |
| }); | |
| }, | |
| ondelete: function(param) { | |
| console.debug("Starting ondelete", param); | |
| let codesText = param.element.querySelector('#rwigo-codes-input'); | |
| console.debug("xdialog user input:",codesText); | |
| let parsedCodes = parseCodesDialog( codesText.value ); | |
| if( parsedCodes !== null && parsedCodes.length >= 3 ) { | |
| console.debug("Match: " + parsedCodes[0] + "," + parsedCodes[1] + "," + parsedCodes[2] ); | |
| let reverseRes = evalPartialReverseWigo( parsedCodes[0], parsedCodes[1], parsedCodes[2] ); | |
| console.debug("Partial evaluation result:", reverseRes); | |
| let res = param.element.querySelector('#rwigo-codes-result'); | |
| let resp = document.createElement('p'); | |
| resp.textContent = reverseRes; | |
| res.appendChild(resp); | |
| } | |
| return false; | |
| } | |
| }); | |
| } | |
| // input: string | |
| // Return: array (match results) | |
| function parseCodesDialog( myText ) { | |
| console.debug("Starting parseCodes", myText ) | |
| let re2 = /[0-9A-Za-z]{6}/g; | |
| console.log("Trying to find codes from user input:"); | |
| // Normalize: trim each line, remove empty/blank lines | |
| let text = myText.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0).join('\n'); | |
| let codes = text.match( re2 ); | |
| console.debug( JSON.stringify( codes ) ); | |
| return codes; | |
| } | |
| // Groundspeak numeric cache type id for Wherigo caches | |
| const WHERIGO_CACHE_TYPE_ID = 1858; | |
| // Read the numeric Groundspeak cache type id from the page. | |
| // The cache type *name* (anchor title next to #uxCacheImage) is localized - | |
| // "Wherigo Cache" in English, "Wherigo-kätkö" in Finnish, etc. - so it must not be | |
| // used for detection. The cache type icon references a sprite whose fragment carries | |
| // the numeric type id and is identical in every language: | |
| // <use xlink:href="/app/ui-icons/sprites/cache-types.svg#icon-1858" /> | |
| // Disabled/archived caches use "#icon-1858-disabled", hence the loose match. | |
| // returns Number type id, or null if it cannot be determined | |
| function getCacheTypeId() { | |
| let useEl = document.querySelector("#uxCacheImage use"); | |
| // xlink:href is a namespaced attribute, so read it directly instead of via a selector | |
| let href = useEl?.getAttribute("xlink:href") ?? useEl?.getAttribute("href") ?? ""; | |
| let spriteMatch = href.match(/#icon-(\d+)/); | |
| if( spriteMatch ) { | |
| return Number(spriteMatch[1]); | |
| } | |
| // fallback for the older markup where #uxCacheImage was (or contained) an <img> | |
| // e.g. /images/WptTypes/1858.gif or /images/WptTypes/sm/1858.gif | |
| let img = document.querySelector("#uxCacheImage img") ?? document.getElementById("uxCacheImage"); | |
| let src = img?.getAttribute?.("src") ?? ""; | |
| let imgMatch = src.match(/WptTypes\/(?:sm\/)?(\d+)\./i); | |
| if( imgMatch ) { | |
| return Number(imgMatch[1]); | |
| } | |
| console.warn("Could not determine cache type id from page"); | |
| return null; | |
| } | |
| // Extract 6 character reverse wherigo codes from a block of text. | |
| // allowVariables = true also accepts letters standing for unknown digits ("12A45B"), | |
| // which only the Solve Regexp brute force knows what to do with. | |
| // \b on both sides so that a 6 character slice of a longer token is never taken for a | |
| // code - "GC1234567" must not yield "GC1234", "1234567890" must not yield "123456". | |
| // returns array of codes, or null when there is no match | |
| function extractCodes( text, allowVariables ) { | |
| let re = allowVariables ? /\b[0-9A-Za-z]{6}\b/g : /\b[0-9]{6}\b/g; | |
| // Normalize: trim each line, remove empty/blank lines | |
| let normalized = text.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0).join('\n'); | |
| return normalized.match( re ); | |
| } | |
| // A published code is mostly digits; an ordinary six letter word is not. Requiring at | |
| // least two digits keeps prose out of the description scan without rejecting real codes | |
| // that carry a few unknown digits ("12A45B"). | |
| function looksLikeCode( token ) { | |
| return (token.match(/[0-9]/g) || []).length >= 2; | |
| } | |
| // Refuse to brute force more than this many unknown digits. 10^6 combinations already | |
| // takes a while; beyond that the tab just hangs, which is exactly what happened when | |
| // prose words were mistaken for codes. | |
| const MAX_UNKNOWN_DIGITS = 6; | |
| // parse reverse wherigo codes from note | |
| async function parseCodesFromNote() { | |
| console.debug("Starting parseCodesFromNote" ) | |
| let re1 = /RWIGO:/; | |
| console.log("Trying to find codes from user note:"); | |
| let text = document.getElementById("cacheNoteText")?.value | |
| ?? document.getElementById("viewCacheNote")?.innerText | |
| ?? ""; | |
| // Normalize: trim each line, remove empty/blank lines | |
| text = text.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0).join('\n'); | |
| let marker = text.match( re1 ); | |
| if( marker ) { | |
| console.debug("Match found (RWIGO:)"); | |
| // Only scan what follows the marker. Anything before it is the user's own prose, | |
| // and six letter words ("puzzle", "solved") would otherwise be read as codes. | |
| let codesText = text.slice( marker.index + marker[0].length ); | |
| let codes = extractCodes( codesText, true ); | |
| console.debug( JSON.stringify( codes ) ); | |
| return codes; | |
| } | |
| else { | |
| console.debug("No codes found from Personal Note"); | |
| return null; | |
| } | |
| } | |
| // async parse codes from cache description | |
| // allowVariables = true also accepts codes containing letters (unknown digits) - only the | |
| // Solve Regexp brute force asks for those, the automatic solve on load stays digits only | |
| async function parseCodesFromDescription( allowVariables = false ) { | |
| console.debug("Starting parseCodesFromDescription", allowVariables ) | |
| let blocks = Array.from( document.getElementsByClassName("UserSuppliedContent") ); | |
| if( blocks.length === 0 ) { | |
| console.warn("No UserSuppliedContent block found on page"); | |
| return null; | |
| } | |
| // With variables allowed the pattern also matches ordinary six letter words, and a | |
| // cache description is full of them. Left unfiltered the solver would treat every | |
| // letter as an unknown digit and brute force 10^10 combinations, hanging the tab. | |
| let scan = function( text ) { | |
| let found = extractCodes( text, allowVariables ); | |
| if( found === null || !allowVariables ) { | |
| return found; | |
| } | |
| let codeLike = found.filter( looksLikeCode ); | |
| return codeLike.length > 0 ? codeLike : null; | |
| }; | |
| // The long description is normally the second block, but a cache without a short | |
| // description only has one - indexing [1] blindly used to throw and kill the script. | |
| let preferred = blocks[1] ?? blocks[0]; | |
| let codes = scan( preferred.innerText ); | |
| if( codes !== null && codes.length >= 3 ) { | |
| return codes; | |
| } | |
| // nothing usable in the long description - try every block instead | |
| console.debug("No codes in the long description - scanning all UserSuppliedContent blocks"); | |
| return scan( blocks.map(b => b.innerText).join('\n') ); | |
| } | |
| // guard so that "Force run" cannot add the solver UI/results to the page twice | |
| let wherigoSolverHasRun = false; | |
| // The actual reverse wherigo solving - split out of the init IIFE so that it can also be | |
| // triggered manually from the "Force run" menu command when cache type detection fails | |
| // (e.g. after a future geocaching.com markup change). | |
| async function runWherigoSolver() { | |
| if( wherigoSolverHasRun ) { | |
| console.warn("Reversewigo solver has already run on this page - skipping"); | |
| return; | |
| } | |
| wherigoSolverHasRun = true; | |
| await gmc.initialized.then(); | |
| let from, result; | |
| await createShowSolveREReverseButton(); | |
| await createDialog(); | |
| let codes = await parseCodesFromNote(); | |
| // Codes found from Personal Note | |
| if( codes !== null && codes.length >= 3 ) { | |
| console.log("Found some codes from UserSuppliedContent"); | |
| console.debug("Match: " + codes[0] + "," + codes[1] + "," + codes[2] ); | |
| let reverseRes = evalPartialReverseWigo( codes[0], codes[1], codes[2] ); | |
| await addHTMLtoPage("<br><strong>Codes with variables converted to coordinates: <br>"+reverseRes+"</strong>" ); | |
| // TODO: how to check if code 2 coords conversion is valid => only show if it is | |
| let ddlatlon = await code2LatLon( codes[0], codes[1], codes[2] ); | |
| let dmlatlon = await dd2dm( ddlatlon.lat, ddlatlon.lon); | |
| from = codes[0] + " " + codes[1] + " " + codes[2] + " => "; | |
| let resultDD = fix(ddlatlon.lat, 2, 6) + " " + fix(ddlatlon.lon, 3, 6); | |
| let resultDMD = dmlatlon.lat + " " + dmlatlon.lon; | |
| result = resultDMD; | |
| console.log( from + " " + resultDD + " = " + resultDMD ); | |
| if( gmc.get("writeFinalToClipboard") ) { | |
| GM_setClipboard( result, "text" ); | |
| } | |
| await presentFinalCoordinates( resultDMD ); | |
| } | |
| // No codes in Personal Note - trying to retrieve directly from description | |
| else { | |
| console.log("Trying to find codes from UserSuppliedContent"); | |
| codes = await parseCodesFromDescription(); | |
| if( codes !== null && (codes.length == 3 || codes.length == 6 || codes.length == 9) ) { | |
| console.debug("Parsed codes from UserSuppliedContent: " + JSON.stringify(codes) ); | |
| let reverseRes = evalPartialReverseWigo( codes[0], codes[1], codes[2] ); | |
| await addHTMLtoPage("<br><strong>Reverse results = "+reverseRes+"</strong>", false); | |
| let ddlatlon = await code2LatLon( codes[0], codes[1], codes[2] ); | |
| let dmlatlon = await dd2dm( ddlatlon.lat, ddlatlon.lon); | |
| from = codes[0] + " " + codes[1] + " " + codes[2] + " => "; | |
| let resultDD = fix(ddlatlon.lat, 2, 6) + " " + fix(ddlatlon.lon, 3, 6); | |
| let resultDMD = dmlatlon.lat + " " + dmlatlon.lon; | |
| result = resultDMD; | |
| console.debug( from + " " + resultDD + " = " + resultDMD ); | |
| if( gmc.get("writeFinalToClipboard") ) { | |
| GM_setClipboard( result, "text" ); | |
| } | |
| await presentFinalCoordinates( resultDMD ); | |
| } | |
| else { | |
| result = "Error: Reverse wherigo codes not found from description/note. <br>Enter codes to Personal Note as shown below, save and reload. <br>RWIGO:<br>123ABC<br>23D45G<br>423444"; | |
| console.log( result ); | |
| await addHTMLtoPage("<br><strong>" + result + " </strong>", false); | |
| } | |
| } | |
| } | |
| (async function() { | |
| 'use strict'; | |
| console.debug("Starting monkey init" ) | |
| await gmc.initialized.then(); | |
| console.debug("monkey init - gmc init done" ) | |
| const my_css = GM_getResourceText("IMPORTED_CSS"); | |
| GM_addStyle(my_css); | |
| console.debug("style added" ) | |
| // Stamp the running version onto the page so it is visible in devtools and can be | |
| // asserted by the end to end test - otherwise there is no way to tell from the page | |
| // which build actually ran. | |
| let runningVersion = (typeof GM_info !== "undefined" && GM_info?.script?.version) || "unknown"; | |
| document.documentElement.setAttribute("data-rwigo-version", runningVersion); | |
| console.debug("Reversewigo solver v" + runningVersion) | |
| let cacheTypeId = getCacheTypeId(); | |
| console.debug("Cache type id:", cacheTypeId, | |
| "(localized name on page:", document.getElementById("uxCacheImage")?.closest('a')?.title ?? "", ")"); | |
| await createShowSourceButton(); | |
| if( gmc.get('addBogusDDToPage') ) { | |
| await addBogusDDToPage(); | |
| } | |
| if( gmc.get('addMinMaxLatLonToPage') ) { | |
| await addMinMaxLatLonToPage(); | |
| } | |
| if( cacheTypeId === WHERIGO_CACHE_TYPE_ID ) { | |
| await runWherigoSolver(); | |
| } | |
| else { | |
| console.debug("Not a Wherigo cache (type id " + cacheTypeId + ") - use the \"Force run\" menu command to run the solver anyway"); | |
| } | |
| })(); | |
Author
Hi,
thanks for your feedback. I've now made some quick fixes to the source
code, feel free to test again.
Note: the updating coordinates to site does not work as it originally did,
but once user clicks on "edit" it still fills in the coordinates there.
Br,
Mika
…On Wed, Mar 15, 2023 at 3:24 AM jamesanderson16 ***@***.***> wrote:
***@***.**** commented on this gist.
------------------------------
Just a Heads up that the code above doesn't work at replacing the Reverse
Wherigo solved numbers as the coordinates at the moment,
I think Tampermonkey has changed some of their allowances (for example,
@include <https://github.com/include> was made obsolete in the v3
manifest). Also, It is saying that version 0.15 DEV is an invalid version.
there are a few other lines that are having an issue too ;)
Thanks for making this though
—
Reply to this email directly, view it on GitHub
<https://gist.github.com/ff97b9e85cf8c3165df3f6b1a8760084#gistcomment-4503126>
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ADRMDD6EM5OECUYOD4H7FITW4EK23BFKMF2HI4TJMJ2XIZLTSKBKK5TBNR2WLJDHNFZXJJDOMFWWLK3UNBZGKYLEL52HS4DFQKSXMYLMOVS2I5DSOVS2I3TBNVS3W5DIOJSWCZC7OBQXE5DJMNUXAYLOORPWCY3UNF3GS5DZVRZXKYTKMVRXIX3UPFYGLK2HNFZXIQ3PNVWWK3TUUZ2G64DJMNZZDAVEOR4XAZNEM5UXG5FFOZQWY5LFVEYTAMZTGU2DMMRUU52HE2LHM5SXFJTDOJSWC5DF>
.
You are receiving this email because you authored the thread.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>
.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Just a Heads up that the code above doesn't work at replacing the Reverse Wherigo solved numbers as the coordinates at the moment,
I think Tampermonkey has changed some of their allowances (for example, @include was made obsolete in the v3 manifest). Also, It is saying that version 0.15 DEV is an invalid version.
there are a few other lines that are having an issue too ;)
Thanks for making this though