Greasy Fork is available in English.

Violent Coords

Exchange Corrected Coordinates.

// ==UserScript==
// @name        Violent Coords
// @namespace   http://violentcoords.com
// @version     0.3
// @description Exchange Corrected Coordinates.
// @author      Violent Cacher
// @resource    coordCSS         https://barnyruilt.alwaysdata.net/style.css
// @resource    homepageHTML     https://barnyruilt.alwaysdata.net/template/homepage.html
// @resource    no_user_infoHTML https://barnyruilt.alwaysdata.net/template/no_user_info.html
// @resource    membershipHTML   https://barnyruilt.alwaysdata.net/template/membership.html
// @resource    user_infoHTML    https://barnyruilt.alwaysdata.net/template/user_info.html
// @resource    script_infoHTML  https://barnyruilt.alwaysdata.net/template/script_info.html
// @match       https://www.geocaching.com/account/settings/membership
// @match       https://www.geocaching.com/geocache/*
// @match       https://www.geocaching.com/map/*
// @match       *://*.barnygeeft.com/*
// @grant       GM_addStyle
// @grant       GM_deleteValue
// @grant       GM_getResourceText
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_setClipboard
// @grant       GM_xmlhttpRequest
// @connect     barnyruilt.alwaysdata.net
// ==/UserScript==


// https://github.com/Tampermonkey/tampermonkey/issues/475
(function() {
	'use strict';

	const link_id = 'linkToCorrectedCoords';
	const state_id = 'elementForStateInformation';
	const url = 'https://barnyruilt.alwaysdata.net';
	const wait_time_increase = 200;

	var button_info = {
		error: {
			class: 'gc-pink-background',
			text: '☠',
		},
		found: {
			class: 'gc-green-background',
			text: '✓',
			title: 'Corrected Coordinate found, click to update',
		},
		loading: {
			class: 'gc-gray-background',
			text: '❄',
			//animate: ['◷', '◶', '◵', '◴'],
			//animate: ['▝', '▗', '▖', '▘' ],
			animate: ['⠈⠉', '⠀⠙', '⠀⠸', '⠀⢰', '⠀⣠', '⢀⣀', '⣀⡀', '⣄⠀', '⡆⠀', '⠇⠀', '⠋⠀', '⠉⠁'],
			timeout: 97,
			title: 'Loading',
		},
		location: {
			class: 'gc-blue-background',
			text: '⊕',
			title: 'Click to get Corrected Coordinate at this location',
		},
		min: {
			class: 'gc-yellow-background',
			text: '?',
			title: 'You\'ve requested this coordinate',
		},
		multipleoff: {
			class: 'gc-blue-background',
			text: '⬀ Bulk submit',
			title: 'Enable bulk submit',
		},
		multiplerunning: {
			class: 'gc-green-background',
			text: '⬈ Bulk submit',
			title: 'Bulk submit in progress',
		},
		notfound: {
			class: 'gc-red-background',
			text: '✗',
			title: 'No Corrected Coordinate Available',
		},
		plus: {
			class: 'gc-yellow-background',
			text: '✓',
			title: 'You\'ve contributed this coordinate',
		},
		unknown: {
			class: 'gc-blue-background',
			text: '?',
			title: 'Click to check for Corrected Coordinates',
		},
		wait: {
			class: 'gc-yellow-background',
			text: '◔',
			title: 'Not so fast cowboy',
		},
	};

	var time_period = [
		{ name: 'seconds', factor: 1000 },
		{ name: 'minutes', factor:   60 },
		{ name: 'hours',   factor:   60 },
		{ name: 'days',    factor:   24 },
		{ name: 'months',  factor:   31 },
		{ name: 'years',   factor:   12 },
	]

	var wait_time = 0;
	var button_timeout = null;
	var multiple_timer = null;

	// Geocaching functions
    function geocachePage() {
		//console.debug('geocachePage');
		if ('undefined' === typeof(userInfo)) {
			setTimeout(geocachePage, 50);
			return;
		}
		GM_setValue('userid', userInfo.ID);
		username = document.getElementsByClassName('username').value;
		//userid = userInfo.ID;
		GM_setValue('username', username);
		//GM_setValue('userid', userInfo.ID);
		createButton();
		waitForCorrected();
    }

	function waitForCorrected() {
		//console.debug('waitForCorrected');
		gccode = document.getElementById('ctl00_ContentBody_CoordInfoLinkControl1_uxCoordInfoCode').innerHTML;
		coords = document.querySelector('span.myLatLon');
		coords = coords ? coords.innerHTML : ''
		if (coords) {
			note = document.getElementById('viewCacheNote');
			note = note ? note.innerHTML : '';
			updateButton('loading');
			sendCoordinate();
		} else {
			setTimeout(waitForCorrected, 150);
		}
	}

	function postData(data) {
		return Object.keys(data).map((key) => encodeURIComponent(key) + '=' + encodeURIComponent(data[key])).join('&');
	}

	function updateButton(state, title = '') {
		if (button_timeout) { clearTimeout(button_timeout); button_timeout = null; }
		if (button_info[state].animate) {
			if (!button_info[state].frame) { button_info[state].frame = 0; }
			if (!button_info[state].timeout) { button_info[state].timeout = 314; }
			button_info[state].text = button_info[state].animate[button_info[state].frame];
			button_info[state].frame = (button_info[state].frame + 1) % button_info[state].animate.length;
			button_timeout = setTimeout(() => updateButton(state, title), button_info[state].timeout);
		}
		if (('' === title) && (button_info[state].title)) { title = button_info[state].title }
		var button = document.getElementById(link_id);
		var old_state = button.getAttribute('data-state');
		if (old_state != state) {
			if (old_state) {
				button.classList.remove(button_info[old_state].class);
				button.classList.remove(button_info[old_state].class + '-hover');
			}
			button.setAttribute('data-state', state);
			button.classList.add(button_info[state].class);
			button.classList.add(button_info[state].class + '-hover');
		}
		button.title = title;
		button.innerHTML = button_info[state].text;
	}

	function createButton() {
		var button = document.createElement('div');
		button.id = link_id;
		button.style = 'margin-right: 0.5em';
		button.classList.add('coords-box');
		button.setAttribute('data-state', 'wait');
		button.addEventListener('click', onClick);

		var link = document.getElementById('uxLatLonLink');
		link.parentNode.insertBefore(button, link)
		updateButton('unknown');
	}

	function createLocationButton() {
		var br = document.createElement('br');
		var button = document.createElement('div');
		button.id = link_id;
		button.style = 'margin-right: 0.5em';
		button.classList.add('coords-box');
		button.setAttribute('data-state', 'wait');
		button.addEventListener('click', onClick);
		var span = document.createElement('span');
		span.id = state_id;

		span.innerHTML = 'Click to get closest corrected coordinates';

		var node = document.getElementById('m_cacheTypes')
		insertAfter(button, node);
		insertAfter(span, button);

		updateButton('location')
	}

	function onClick() {
		var args = {}
		switch (document.getElementById(link_id).getAttribute('data-state')) {
			case 'found': getCoordinate(); break;
			case 'location': locationCoordinate(); break;
			case 'multipleoff': multipleStart(); break;
			case 'multiplerunning': multipleStop(); break;
			case 'unknown': checkCoordinate(); break;
		}
		return false
	}

	function checkCoordinate() {
		updateButton('loading');
		console.log('Checking: ' + gccode);
		GM_xmlhttpRequest({
			url: url,
			method: 'POST',
			headers: {
				'Content-type': 'application/x-www-form-urlencoded',
				'Accept': 'application/json',
			},
			data: postData({ 'gccode': gccode }),
			onload: (data) => {
				if (JSON.parse(data.responseText).hasCorrected) {
					updateButton('found');
				} else {
					updateButton('notfound');
				}
			},
			onerror: (data) => {
				updateButton('error', 'Something went wrong while checking coordinates');
				console.error(data)
			},
		});
	}

	function getCoordinate() {
		updateButton('loading');
		if (requestToken(getCoordinate)) { return; }
		GM_xmlhttpRequest({
			url: url,
			method: 'POST',
			headers: {
				'Content-type': 'application/x-www-forms-urlencoded',
				'Accept': 'application/json',
			},
			data: postData({
                'prid': userInfo.ID,
				'gccode': gccode,
				'token': GM_getValue('token_value'),
			}),
			onload: (resp) => {
				var data = JSON.parse(resp.responseText)
				if (data.error) {
					updateButton('error', data.error);
				} else if (data.wait) {
					waitCoordinate(Date.now() + 1000 * data.wait, 'found')
				} else if (data.latitude && data.longitude) {
					if((Math.abs(data.latitude) > 90) || (Math.abs(data.latitude) > 180)) {
						updateButton('error', 'Invalid coordinates');
					} else {
                        updateCoordinate(data)
                    }
				} else {
					updateButton('notfound', 'No coordinates returned');
					console.error(data);
				}
			},
			onerror: (data) => {
				updateButton('error', 'Something went wrong while retreiving coordinates');
				console.error(data)
			},
		});
	}

	function updateCoordinate(data) {
		$.getJSON({
			url: 'https://www.geocaching.com/seek/cache_details.aspx/SetUserCoordinate',
			type: "POST",
			dataType: "json",
			contentType : "application/json",
			data: JSON.stringify({
				dto: {
					data: {
						lat: data.latitude,
						lng: data.longitude,
					},
					ut: userToken,
				}
			}),
			success: () => location.reload(), // Because groundspeak is also to lazy to use the response to update the page
			error: (data) => {
				updateButton('error', 'Something went wrong while updating coordinate');
				console.error(data);
			},
		});
	}


	function locationCoordinate() {
		updateButton('loading');
		if (requestToken(locationCoordinate)) { return; }
		var center = MapSettings.Map.getCenter();
		GM_xmlhttpRequest({
			url: url,
			method: 'POST',
			headers: {
				'Content-type': 'application/x-www-forms-urlencoded',
				'Accept': 'application/json',
			},
			data: postData({
				// Because sometimes center contains functions and sometimes properties
				'lat': (center.lat instanceof Function) ? center.lat() : center.lat,
				'lng': (center.lng instanceof Function) ? center.lng() : center.lng,
				'token': GM_getValue('token_value'),
			}),
			onload: (resp) => {
				var data = JSON.parse(resp.responseText)
				if (data.error) {
					updateButton('error', data.error);
				} else if (data.wait) {
					waitCoordinate(Date.now() + 1000 * data.wait, 'location')
				} else if (data.latitude && data.longitude) {
					console.log(data)
					updateButton('location');
					document.getElementById(state_id).innerHTML = '<a href="https://coord.info/' +data.gccode + '">' + data.gccode + '</a>: ' + data.distance + ' km';
				} else {
					updateButton('error', 'No coordinates returned');
					console.error(data);
				}
			},
			onerror: (data) => {
				updateButton('error', 'Something went wrong while retreiving coordinates');
				console.error(data)
			},
		});
	}

	function multipleClear() {
		GM_deleteValue('multiple_skip');
		_setById('cc_skip', '?')
		_setById('cc_available', '?')
		return false;
	}

	function multipleCoordinate() {
		if (requestToken(multipleCoordinate)) { return; }
		updateButton('loading');
		GM_xmlhttpRequest({
			url: url,
			method: 'POST',
			headers: {
				//'Content-type': 'application/x-www-forms-urlencoded',
				'Accept': 'application/json',
			},
			data: postData({
				'multiple' : 'm',
				'skip' : GM_getValue('multiple_skip') || 0,
				'take' : 50,
				'token': GM_getValue('token_value'),
			}),
			onload: (resp) => {
				var data = JSON.parse(resp.responseText);
				console.log(data);
				GM_setValue('multiple_skip', (GM_getValue('multiple_skip')*1 || 0) + data['count']);
				_addById('cc_set', data['updates']);
				_addById('cc_fullCallsRemaining', -data['count']);
				_setById('cc_available', data['total']);
				_setById('cc_skip', GM_getValue('multiple_skip'));
				updateButton('multiplerunning');
				multiple_timer = setTimeout(multipleCoordinate, 17659);
			},
			error: (data) => {
				updateButton('error', 'Something went wrong while proccesing multiple');
				console.error(data)
			},
		});
	}

	function multipleStart() {
		GM_setValue('multiple', true);
		multipleCoordinate();
	}

	function multipleStop() {
		if (multiple_timer) {
			clearTimeout(multiple_timer);
			multiple_timer = null;
		}
		updateButton('multipleoff');
		GM_setValue('multiple', false);
	}

	function sendCoordinate() {
		//console.debug('sendCoordinate');
		if (requestToken(sendCoordinate)) { return; }
		GM_xmlhttpRequest({
			url: url,
			method: 'POST',
			headers: {
				//'Content-type': 'application/x-www-forms-urlencoded',
				'Accept': 'application/json',
			},
			data: postData({
				'gccode': gccode,
				'note': note,
                'prid': userInfo.ID,
				'token': GM_getValue('token_value'),
			}),
			onload: (data) => {
				var json = JSON.parse(data.responseText);
				console.log(json);
				updateButton((1 == json['direction']) ? 'plus' : 'min');
			},
			error: (data) => {
				updateButton('error', 'Something went wrong while sending coordinates');
				console.error(data)
			},
		});
	}

	function waitCoordinate(timestamp, next) {
		var delta = timestamp - Date.now();
		if (delta > 0) {
			var i = 1;
			delta /= time_period[0].factor;
			var wait = time_period[0].factor / 4;
			while ((delta > 99) && (i < time_period.length)) {
				delta /= time_period[i].factor;
				wait  *= time_period[i].factor;
				i++;
			}

			updateButton('wait', 'Please wait ' + Math.ceil(delta) + ' ' + time_period[i-1].name);
			setTimeout(() => waitCoordinate(timestamp, next), wait);
			return;
		}
		updateButton(next);
	}

	function requestToken(func = null) {
		//console.log("requestToken()");
		//console.log(Date.now() + ' now')
		//console.log(GM_getValue('token_expires') + ' expires');
		//console.log((GM_getValue('token_requested') || 0) + ' requested');

		if (
			((GM_getValue('token_expires') || 0) < (Date.now() + 5*60*1000)) &&
			(
				((GM_getValue('token_requested') || 0) < (Date.now() - 10*60*1000)) ||
				((GM_getValue('token_expires') || 0) >= (GM_getValue('token_requested') || 0))
			)
		) {
			getToken();
		}
		if (func) {
			if (GM_getValue('token_expires') < Date.now()) {
				console.log(GM_getValue('token_expires') + ' expires');
				console.log(GM_getValue('token_requested') + ' requested');
				wait_time += wait_time_increase;
				console.debug('Waiting for token: ' + wait_time)
				setTimeout(func, wait_time);
			} else {
				return false;
			}
		}
		return true;
	}

	function getToken() {
		console.log('getToken()');
		GM_setValue('token_requested', Date.now());
		GM_xmlhttpRequest({
			url: "https://www.geocaching.com/account/oauth/token",
			method: 'POST',
			headers: {
				//'Content-type': 'application/x-www-forms-urlencoded',
				'Accept': 'application/json',
			},
			onload: (resp) => {
				var result = JSON.parse(resp.responseText);
				GM_setValue('token_value', result.access_token);
				GM_setValue('token_expires', Date.now() + 1000 * (result.expires_in - 60)); // - 60 seconds as a safety
			}
		});
	}

	// Homepage functions

	function template(text, vars) {
		return text.replace(/\{\{(\w*)\}\}/g, (_, x) => vars[x]);
	}

	function insertAfter(node, target) {
		target
			.parentNode
			.insertBefore(node, target.nextSibling);
	}

	function infoClear() {
		GM_deleteValue('username');
		GM_deleteValue('userid');
		GM_deleteValue('token_value');
		GM_deleteValue('token_expires');
		GM_deleteValue('token_requested');
		location.reload();
	}

	function setClipboard(event) {
		GM_setClipboard(event.target.getAttribute('data-set-clipboard'));
	}

	function infoUser() {
		var expires = new Date(GM_getValue('token_expires'));

		return template(GM_getResourceText("user_infoHTML"), {
			'expires_iso'       : expires.toISOString(),
			'token_class'       : GM_getValue('token_expires') < Date.now() ? 'strike' : '',
			'token_value'       : GM_getValue('token_value'),
			'token_value_short' : GM_getValue('token_value').substr(0,8) + '...' + GM_getValue('token_value').substr(-16),
			'userid'            : userid,
			'username'          : username,
		})
	}

	function _resetTimeString(seconds) {
		if (!seconds) { return '-'; }
		var date = new Date(Date.now() + 1000 * seconds);
		return [
			('0'+date.getHours()).slice(-2),
			('0'+date.getMinutes()).slice(-2),
			('0'+date.getSeconds()).slice(-2)].join(':');
	}

	function _addById(id, value) {
		//console.log("addById("+id+", "+_getById(id)+" + "+value+")");
		_setById(id, _getById(id)*1 + value)
	}

	function _getById(id, value) {
		var elem = document.getElementById(id);
		if (!elem) { return elem; }
		return elem.innerHTML;
	}

	function _setById(id, value) {
		//console.log('setById('+id+', '+value+')');
		var elem = document.getElementById(id);
		if (elem) { elem.innerHTML = value; }
	}


	function addToMembershipPage(data) {
		if (GM_getValue('multiple_userid') != data['id']) {
			GM_setValue('multiple_skip', 0);
			GM_setValue('multiple_available', '?');
		}
		GM_setValue('multiple_userid', data['id'])
		data['link_id'] = link_id;
		data['premium_display'] = data['premium'] ? 'block' : 'none';
		data['liteCallsResetTime'] = _resetTimeString(data['liteCallsSecondsToLive']);
		data['fullCallsResetTime'] = _resetTimeString(data['fullCallsSecondsToLive']);
		data['skip'] = GM_getValue('multiple_skip')*1 || '?';
		data['available'] =	GM_getValue('multiple_available');
		data['available'] = '?'
		var div = document.createElement('div');
		div.innerHTML = template(GM_getResourceText("membershipHTML"), data)
		var node = document.getElementById('stateIndex');
		insertAfter(div, document.getElementById('stateIndex'));
		GM_getValue('multiple') ? multipleStart() : multipleStop();
		document.getElementById(link_id).addEventListener('click', onClick);
		document.getElementById('bulk_submit_reset').addEventListener('click', multipleClear);
	}

	function membershipPage() {
		if (requestToken(membershipPage)) { return };
		GM_xmlhttpRequest({
			url: url,
			method: 'POST',
			headers: {
				//'Content-type': 'application/x-www-forms-urlencoded',
				'Accept': 'application/json',
			},
			data: postData({
				'stats': 'x',
				'prid': GM_getValue('userid'),
				'token': GM_getValue('token_value'),
			}),
			onload: (resp) =>  {
				console.log(resp);
				addToMembershipPage(JSON.parse(resp.responseText));
			},
			onerror: (data) => {
				console.error('fout');
				console.error(data);
			},
		});
	}

	function infoNoUser() {
		return GM_getResourceText("user_infoHTML");
	}

	function homepage() {
		document.body.innerHTML = '<div class="content">'
			+ GM_getResourceText("homepageHTML")
			+ '<p>' + template(GM_getResourceText("script_infoHTML"), {
				'script_name'    : GM.info.script.name,
				'script_version' : GM.info.script.version,
			}) + '</p>'
			+ '<p>' + (GM_getValue('userid') ? infoUser() : infoNoUser()) + '</p>'
		+ '</div>';

		var clearLink = document.getElementById('info_clear');
		if (clearLink) {
			clearLink.addEventListener('click', infoClear, false);
		}
		document
			.querySelectorAll('[data-set-clipboard]')
			.forEach((elem) => {
				elem.addEventListener('click', setClipboard, false);
			})
		;
	}

	// Main functions
	var coords = '';
	var gccode = '';
	var note = '';
	var userid = '';
	var username = '';

	//GM_deleteValue('token_expires', null);
	//GM_deleteValue('token_requested', null);

	GM_addStyle(GM_getResourceText("coordCSS"));
	switch (window.location.hostname) {
		case 'www.geocaching.com' :
			requestToken();
			console.debug(window.location.pathname)
			if (window.location.pathname.match(/\/geocache/)) {
				geocachePage();
			}
			if (window.location.pathname.match(/\/map/)) {
				createLocationButton()
			}
			if (window.location.pathname.match(/\/account\/settings\/membership/)) {
				membershipPage();
			}
			break;
		case 'barnygeeft.com':
			userid = GM_getValue('userid');
			username = GM_getValue('username');
			homepage();
			break;
	}
})();