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.
// ==UserScript== // @name Reversewigo solver ✓🐵 DEV // @namespace http://tampermonkey.net/ // @version 0.32 // @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 [email protected] // @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/[email protected]/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== //TODO: copy to clipboard button for final solved coords! // placeholder for calculated reverse wherigo coordinates // used by pollDOM to update them to popup dialog // TODO: definitely not the best practise but a quick hack that seems to work (for now) var newCoordinates = ""; let DEBUG = true; GM_registerMenuCommand("Configure", () => { gmc.open() }, "C"); GM_registerMenuCommand("Set codes", () => { readCodes() }, "S"); 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 } }, '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(θ); const φ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); } } // TODO: definitely not the best practice, but works (for now) function pollDOM() { console.debug("Starting pollDOM"); const newCrd = document.querySelector('.cc-parse-text'); if (newCrd != null) { // React-controlled input: use native setter + dispatch input event so React picks up the change const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; nativeSetter.call(newCrd, newCoordinates); newCrd.dispatchEvent(new Event('input', { bubbles: true })); let submitBtn = document.getElementsByClassName("btn-cc-parse")[0]; submitBtn.click(); } else { setTimeout(pollDOM, 1000); // try again in 300 milliseconds } } async function updateCoordinates( newCoords ) { console.debug("Starting updateCoordinates", newCoords); let crd = document.getElementById("uxLatLonLink"); crd.click(); newCoordinates = newCoords; pollDOM(); console.debug("XX: " + newCoords); } 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 } } // htmlsnip = html snippet in string // replace = boolean; true = replace existing html fully async function addHTMLtoPage( htmlsnip ) { console.debug("Starting addHTMLtoPage", htmlsnip); await gmc.initialized.then(); // let docEl = document.getElementsByClassName("Note Disclaimer")[0]; //let docEl = document.getElementById("ctl00_ContentBody_CacheInformationTable"); let docEl = document.getElementById( gmc.get('infoHtmlElID') ); let replace = gmc.get('replaceInfoHTML') if( replace ) { docEl.innerHTML = htmlsnip; } else { docEl.innerHTML = docEl.innerHTML + 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(); if(codes.length != 3) { console.error("Error: >3 or <3 codes found:" + JSON.stringify(codes)); } // cleanse for(let i=0; i<3; i++) { codes[i] = codes[i].trim(); } 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); 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:"); let text = myText; let codes = text.match( re2 ); console.debug( JSON.stringify( codes ) ); return codes; } // parse reverse wherigo codes from note async function parseCodesFromNote() { console.debug("Starting parseCodesFromNote" ) let re1 = /RWIGO:/g; let re2 = /[0-9A-Za-z]{6}/g; console.log("Trying to find codes from user note:"); let text = document.getElementById("cacheNoteText")?.value ?? document.getElementById("viewCacheNote")?.innerText ?? ""; if( text.match( re1 ) ) { console.debug("Match found (RWIGO:)"); let codes = text.match( re2 ); console.debug( JSON.stringify( codes ) ); return codes; } else { console.debug("No codes found from Personal Note"); return null; } } // async parse codes from cache description async function parseCodesFromDescription() { console.debug("Starting parseCodesFromDescription" ) let re = /[0-9]{6}/g; let codes = document.getElementsByClassName("UserSuppliedContent")[1].innerText.match( re ); return codes; } (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" ) let from, result; let cacheType = document.getElementById("uxCacheImage")?.closest('a')?.title ?? ""; await createShowSourceButton(); if( gmc.get('addBogusDDToPage') ) { await addBogusDDToPage(); } if( gmc.get('addMinMaxLatLonToPage') ) { await addMinMaxLatLonToPage(); } if( cacheType == "Wherigo Cache" ) { 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 addHTMLtoPage("<br><strong> Reversewigo final coordinates = " + result + " </strong>" ); if( result != "S NaN° NaN W NaN° NaN") { await updateCoordinates( 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 addHTMLtoPage("<br><strong> Reversewigo final coordinates = " + result + " </strong>" ); if( result != "S NaN° NaN W NaN° NaN") { await updateCoordinates( 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); } } } })();