Violent Coords

Exchange Corrected Coordinates.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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;
	}
})();