// ==UserScript==
// @name Copy/paste Geoguessr map data
// @namespace SlashP
// @version 2.4.1
// @description Copy latitude, longitude, heading, pitch and zoom information from Geoguessr maps as JSON data. Add or replace locations in maps by pasting JSON data or Google Maps link(s) in map maker.
// @author SlashP
// @require https://greasyfork.org/scripts/460322-geoguessr-styles-scan/code/Geoguessr%20Styles%20Scan.js?version=1151668
// @match https://www.geoguessr.com/*
// @license MIT
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
let buttonClassName = '';
let smallButtonClassName = '';
let primaryButtonClassName = '';
let secondaryButtonClassName = '';
let dangerButtonClassName = '';
const copyDownloadMapDataButtonHtml = () => `
`;
const replaceLocationsButtonHtml = () => `
`;
const buttons = [
{ html: copyDownloadMapDataButtonHtml },
{ html: importLocationsFromClipboardButtonHtml },
{ html: importLocationsFromFileInputHtml },
{ html: addLocationsButtonHtml },
{ html: replaceLocationsButtonHtml },
];
const newMapMakerContainerSelector = "[class*='sidebar_container']";
const mapId = () => location.href.split('/').pop();
const getExistingMapData = () => {
const url = `https://www.geoguessr.com/api/v4/user-maps/drafts/${mapId()}`;
return fetch(url)
.then(response => response.json())
.then(map => ({
id: map.id,
name: map.name,
description: map.description,
avatar: map.avatar,
highlighted: map.highlighted,
published: map.published,
customCoordinates: map.coordinates
}));
}
const uniqueBy = (arr, selector) => {
const flags = {};
return arr.filter(entry => {
if (flags[selector(entry)]) {
return false;
}
flags[selector(entry)] = true;
return true;
});
};
const intersectionCount = (arr1, arr2, selector) => {
var setB = new Set(arr2.map(selector));
var intersection = arr1.map(selector).filter(x => setB.has(x));
return intersection.length;
}
const exceptCount = (arr1, arr2, selector) => {
var setB = new Set(arr2.map(selector));
var except = arr1.map(selector).filter(x => !setB.has(x));
return except.length;
}
const latLngSelector = x => `${x.lat},${x.lng}`;
const latLngHeadingPitchSelector = x => `${x.lat},${x.lng},${x.heading},${x.pitch}`;
const pluralize = (text, count) => count === 1 ? text : text + "s";
const copyMapData = () => {
getExistingMapData()
.then(map => {
const setMapFeedbackText = text => { document.getElementById("copyMapDataFeedback").innerText = text; }
navigator.clipboard.writeText(JSON.stringify(map)).then(() => {
setMapFeedbackText("Map data copied to clipboard.");
setTimeout(() => setMapFeedbackText(""), 8000);
});
});
}
let locations = [];
let existingMap = null;
const isFullStreetViewUrl = url => url.match(/https:\/\/www\.google\.[a-z.]+\/maps\/@/) !== null;
const isLineValidCsv = line => {
const commaSeparatedValues = line?.split(',') || [];
return commaSeparatedValues.length >= 2 && parseFloat(commaSeparatedValues[0]) > -90 && parseFloat(commaSeparatedValues[0]) < 90 && parseFloat(commaSeparatedValues[1]) > -180 && parseFloat(commaSeparatedValues[1]) < 180;
}
const isCsvTextWithLocations = lines => {
const firstLine = lines?.split('\n')[0];
return isLineValidCsv(firstLine);
}
const importLocationsFromClipboard = () => {
navigator.clipboard.readText().then(text => {
if (isFullStreetViewUrl(text) || isCsvTextWithLocations(text)) {
importLocationsFromLinesInClipboard(text);
} else {
importLocations(text);
}
});
}
const csvLocation = l => ({
lat: parseFloat(l.split(',')[0]),
lng: parseFloat(l.split(',')[1])
});
const getLocationsFromLines = linkText => {
const lines = linkText.split('\n');
const extractNumberFromParameter = (url, param) => parseFloat(url.split(",").slice(2).filter(x => x.indexOf(param) !== -1)[0]);
let coordinates = [];
for (let line of lines) {
if (isLineValidCsv(line)) {
coordinates.push(csvLocation(line));
continue;
}
if (!isFullStreetViewUrl(line)) {
continue;
}
const lng = parseFloat(line.split(",")[1]);
const lat = parseFloat(line.split(",")[0].split("@")[1]);
const heading = extractNumberFromParameter(line, "h");
const zoom = (90 - extractNumberFromParameter(line, "y")) / 90 * 2.75; // guesstimated "max" value.
const pitch = extractNumberFromParameter(line, "t") - 90;
const panoId = line.split("!1s")[1].split("!2e")[0];
if (!lng || !lat || !heading || !panoId) {
continue;
}
const coordinate = {
heading: heading,
pitch: pitch || 0,
zoom: zoom || 0,
panoId: panoId,
countryCode: null,
stateCode: null,
lat: lat,
lng: lng
};
coordinates.push(coordinate);
}
return coordinates;
}
const getLocationsFromCsv = linkText => linkText.split('\n')?.filter(isLineValidCsv).map(csvLocation);
const importLocationsFromLinesInClipboard = (linkText) => {
const coordinates = getLocationsFromLines(linkText);
const mapText = JSON.stringify({
customCoordinates: coordinates
});
importLocations(mapText);
}
const setImportLocationsFeedbackText = text => { document.getElementById("importLocationsFeedback").innerText = text; }
const arrayOrCustomCoordinates = obj => Array.isArray(obj) ? obj : obj?.customCoordinates;
const importLocations = (text, mapOrLocationArray) => {
try {
getExistingMapData()
.then(map => {
existingMap = {
...map,
customCoordinates: map.customCoordinates || []
};
locations = arrayOrCustomCoordinates(mapOrLocationArray) || arrayOrCustomCoordinates(JSON.parse(text));
if (!locations?.length) {
setImportLocationsFeedbackText("Invalid map data.");
return;
}
const uniqueExistingLocations = uniqueBy(existingMap.customCoordinates, latLngSelector);
const uniqueImportedLocations = uniqueBy(locations, latLngSelector);
const uniqueLocations = uniqueBy([...uniqueExistingLocations, ...uniqueImportedLocations], latLngSelector);
const numberOfLocationsBeingAdded = uniqueLocations.length - uniqueExistingLocations.length;
const numberOfUniqueLocationsImported = uniqueImportedLocations.length;
const numberOfExactlyMatchingLocations = intersectionCount(uniqueExistingLocations, uniqueImportedLocations, latLngHeadingPitchSelector);
const numberOfLocationsWithSameLatLng = intersectionCount(uniqueExistingLocations, uniqueImportedLocations, latLngSelector);
const numberOfLocationEditions = numberOfLocationsWithSameLatLng - numberOfExactlyMatchingLocations;
const numberOfLocationsNotInImportedList = exceptCount(uniqueExistingLocations, uniqueImportedLocations, latLngSelector);
const numberOfLocationsNotInExistingMap = exceptCount(uniqueImportedLocations, uniqueExistingLocations, latLngSelector);
if (numberOfExactlyMatchingLocations === uniqueExistingLocations.length && uniqueExistingLocations.length === uniqueImportedLocations.length) {
setImportLocationsFeedbackText("All locations are exactly the same.");
return;
}
if (numberOfExactlyMatchingLocations === uniqueExistingLocations.length && uniqueExistingLocations.length === uniqueImportedLocations.length) {
setImportLocationsFeedbackText("All locations are exactly the same.");
return;
}
const maximumNumberOfLocations = 105000;
if (numberOfUniqueLocationsImported > maximumNumberOfLocations) {
setImportLocationsFeedbackText(`You can't import more than ${maximumNumberOfLocations} locations.`);
return;
}
if (numberOfLocationsBeingAdded > 0 && uniqueLocations.length < maximumNumberOfLocations) {
document.getElementById("saveExplanation").innerText = `Add ${numberOfLocationsBeingAdded} locations to this map. New count: ${uniqueLocations.length}. Any manual changes applied after this page was loaded will be lost.`;
document.getElementById("addLocationsSection").style.display = "block";
}
document.getElementById("replaceLocationsExplanation").innerHTML = `Replace the locations in the map. New count: ${numberOfUniqueLocationsImported}. Any manual changes applied after this page was loaded will be lost.