Violent Coords

Exchange Corrected Coordinates.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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;
	}
})();