您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Display Wikipedia summary of the Geoguessr locations. Works with streaks, single player 5 round games and challenges.
// ==UserScript== // @name Wiki Summary // @include /^(https?)?(\:)?(\/\/)?([^\/]*\.)?geoguessr\.com($|\/.*)/ // @version 0.6.0 // @description Display Wikipedia summary of the Geoguessr locations. Works with streaks, single player 5 round games and challenges. // @author semihM (aka rhinoooo_), MiniKochi // @source https://github.com/semihM/GeoGuessrScripts/blob/main/WikiSummary // @supportURL https://github.com/semihM/GeoGuessrScripts/issues // @require http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js // @require http://code.jquery.com/jquery-3.4.1.min.js // @grant GM_addStyle // @namespace https://greasyfork.org/users/851187 // ==/UserScript== /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // API KEYS : Get your keys from following sites // - https://www.bigdatacloud.com/ // - https://opentripmap.io/ // // After every update, these values will be reset. But since the script stores them as "cookies", they will still be replaced internally // API Keys are not required to be updated in here after they get removed because of an auto-update // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // BigDataCloud for location information let BigDataCloud_APIKEY = 'ENTER_API_KEY_HERE'; //Replace ENTER_API_KEY_HERE with yours from https://www.bigdatacloud.com/ // OpenTripMap for places nearby let OpenTripMap_APIKEY = 'ENTER_API_KEY_HERE'; //Replace ENTER_API_KEY_HERE with yours from https://opentripmap.io/ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // SETTINGS: Make sure UseSettingsBelow_InsteadOfCookies is set to true to use settings written here instead of previous one in cookies // After every update, settings will be reset. But since the script stores them as "cookies", they will still be replaced internally // There will be alerts prompted in the site after updates, make sure you read them! /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// let Settings = { // Maximum fact text length, may exceed the limit if last sentence is long enough "MaximumFactMessageLength": 420, // Maximum amount of famous place facts to display "MaximumPlaceFactCountToDisplay": 5, // Maximum amount of facts(geographical + famous place) to display "MaximumFactCountToDisplay": 10, // Categories for nearby places, check https://opentripmap.io/catalog for other categories. Seperate categories with ',' commas "PlaceCategoriesToSearchFor": "historic,cultural,natural,architecture,religion", // Radius in meters to search for places nearby "PlaceSearchRadiusInMeters": 2000, // true: Display facts under the main green continue button; false: Display facts before continue button "DisplayFactsBelowButtons": true, // Fact's wiki title color for both geographical and famous place facts "FactWikiTitleColor": "lime", // Fact's wiki text color for both geographical and famous place facts "FactWikiTextColor": "white", // Geographical fact title "GeographyFactTitle": "Geographical", // Geographical fact title color name, lowercase "GeographyFactTitleColorName": "orange", // Famous place fact title "FamousPlaceFactTitle": "Famous Place", // Famous place fact text color name, lowercase "FamousPlaceFactTitleColorName": "cyan", // Source link color name, lowercase "SourceLinkColorName": "darkgray", // true: Display fact number after the title; false: Don't display fact number "DisplayFactNumber": true, // true: Open wiki links in new tab; false: Open in current tab "OpenInNewTab": true, // Exclude given wiki id's from facts "ExcludedWikiPageIds": [ // Remove the first '//' before the wiki ids ( //12345, -> 12345, ) to exclude the wiki page from results // Add more by adding a ',' comma after the previous wiki id //83759, // USA //13530298, // UK ] } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const DEBUG_ENABLED = false // true: Console print enabled for debugging; false: Don't print any debug information /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const API_URL = "https://api.bigdatacloud.net/data/reverse-geocode?localityLanguage=en&" const WIKI_URL = "https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro&explaintext&redirects=1&origin=*&titles=" const WIKIDATA_URL = "https://www.wikidata.org/w/api.php?action=wbgetentities&format=json&origin=*&props=sitelinks&sitefilter=enwiki&ids=" const OPENTRIP_URL = `https://api.opentripmap.com/0.1/en/places/radius?radius=${Settings.PlaceSearchRadiusInMeters}&limit=${Settings.MaximumPlaceFactCountToDisplay}&src_attr=wikidata&kinds=${encodeURIComponent(Settings.PlaceCategoriesToSearchFor)}&apikey=` /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const SettingsVersion = 6; // ONLY UPDATE WHILE RELEASING A NEW VERSION const EMPTYAPIKEY = "ENTER_API_KEY_HERE" const INVALIDLINK = "#"; const BIGDATACLOUD_APICOOKIE = "geoguessr_script_semihM_bigdatacloudkey" const OPENTRIPMAP_APICOOKIE = "geoguessr_script_semihMopentripmapkey" const SETTINGS_COOKIE = "geoguessr_script_semihM_WikiSummarySettings" const SETTINGS_ASKED_COOKIE = "geoguessr_script_semihM_WikiSummarySettingsAsked" const _id_fact_div = "location-fact" const _class_roundResult_5roundGame = "round-result_actions__5j26U" const _class_roundResult_streakGame = "streak-round-result_root__WxUU9" const _class_roundResult_Bullseye = "round-score_container__avps2" const _class_nextButton_Bullseye = "button_button__CnARx" const _class_correct_loc = 'styles_circle__2tw8L styles_variantFloating__mawbd styles_colorWhite__2QcUQ styles_borderSizeFactorOne__2Di08' const SummaryLoadingPlaceHolderInnerHtml = `<div id="${_id_fact_div}" style="text-align:center"><br><br>Loading wikipedia summaries...</div><br>` const CookieDays = 365 let checked = parseInt(sessionStorage.getItem("FactLocationChecked"), 10); let facts = [] let placeWikidataTitles = [] let needsWiki = true; if (sessionStorage.getItem("FactLocationChecked") == null) { sessionStorage.setItem("FactLocationChecked", 0); checked = 0; }; ///////////////////////// // Cookies ///////////////////////// CheckCookiesForAPIKeys() CheckCookiesForSettings() ///////////////////////// function debug(obj) { if (DEBUG_ENABLED) console.log(obj) } function setCookie(name, value, days) { var expires = ""; if (days) { var date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); expires = "; expires=" + date.toUTCString(); } document.cookie = name + "=" + (value || "") + expires + "; path=/"; } function getCookie(name) { var nameEQ = name + "="; var ca = document.cookie.split(';'); for (var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0) == ' ') c = c.substring(1, c.length); if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); } return null; } function GetSettingsString() { return JSON.stringify(Settings) } function CheckCookiesForAPIKeys() { let key = "" if (BigDataCloud_APIKEY == EMPTYAPIKEY) { if (key = getCookie(BIGDATACLOUD_APICOOKIE)) BigDataCloud_APIKEY = key else if ((key = prompt("Couldn't find bigdatacloud.com API key, please enter your key")) != "") setCookie(BIGDATACLOUD_APICOOKIE, BigDataCloud_APIKEY = key, CookieDays); else return alert("Failed to initialize WikiSummary script. Make sure to add your key manually!") } else setCookie(BIGDATACLOUD_APICOOKIE, BigDataCloud_APIKEY, CookieDays) if (OpenTripMap_APIKEY == EMPTYAPIKEY) { if (key = getCookie(OPENTRIPMAP_APICOOKIE)) OpenTripMap_APIKEY = key else if ((key = prompt("Couldn't find opentripmap.io API key, please enter your key")) != "") setCookie(OPENTRIPMAP_APICOOKIE, OpenTripMap_APIKEY = key, CookieDays); else return alert("Failed to initialize WikiSummary script. Make sure to add your key manually!") } else setCookie(OPENTRIPMAP_APICOOKIE, OpenTripMap_APIKEY, CookieDays) } function CheckCookiesForSettings() { let settings = getCookie(SETTINGS_COOKIE) let asked = getCookie(SETTINGS_ASKED_COOKIE); if (settings == null || asked == null) // First time { setCookie(SETTINGS_COOKIE, JSON.stringify(Settings), CookieDays) setCookie(SETTINGS_ASKED_COOKIE, SettingsVersion, CookieDays) } else { let cookieSettings = JSON.parse(settings) if (asked != SettingsVersion) // There was an update { let restore = window.confirm("There was an update to WikiSummary(by rhino). Would you like to restore the old settings ? Click \"Cancel\" if you havent made any changes and use default settings."); setCookie(SETTINGS_ASKED_COOKIE, SettingsVersion, CookieDays) if (restore) { for (const [key, value] of Object.entries(Settings)) { if (!(key in cookieSettings)) { cookieSettings[key] = value } } let cookiesets = JSON.stringify(cookieSettings) setCookie(SETTINGS_COOKIE, cookiesets, CookieDays) // Use from cookies let jsonframe = document.createElement("pre") jsonframe.innerHTML = "<pre>// Setting start around line 40\n// v COPY STARTING FROM THE LINE BELOW v\nlet Settings = " + JSON.stringify(cookieSettings, undefined, 2) + "</pre>" let myDialog = document.createElement("dialog"); document.body.appendChild(myDialog) myDialog.appendChild(jsonframe); let closeBtn = document.createElement("p") closeBtn.style = "background-color: red; color: white; font-size:18px; border: 2px solid black; width: auto; text-align:center;" closeBtn.textContent = "Click here to close this frame" closeBtn.onclick = () => myDialog.remove() myDialog.appendChild(closeBtn); myDialog.appendChild(jsonframe); myDialog.showModal(); alert("Old settings will be shown in a small window for copying, paste them into the Wiki Summary script!") } else { setCookie(SETTINGS_COOKIE, JSON.stringify(Settings), CookieDays) // Store new update's settings } } else { setCookie(SETTINGS_COOKIE, JSON.stringify(Settings), CookieDays) // Store currently written settings } } } function cleanPages(pages) { // Missing or invalid if (-1 in pages) delete pages[-1] if (-2 in pages) delete pages[-2] Settings.ExcludedWikiPageIds.forEach(idx => idx in pages ? delete pages[idx] : null); } function setNameToPostal(obj, name) { obj.name = name obj.description = name } function styleFact(name, desc) { if (desc.startsWith(". ")) desc = desc.substring(2); return `<h3 style="color: ${Settings.FactWikiTitleColor}">${name}</h3><br>${desc}` } function getTitlesFromLocation() { return getLocationObject() .then(async loc => { debug(loc) needsWiki = true; placeWikidataTitles = []; if (loc == null || !("localityInfo" in loc)) return null let infos = loc.localityInfo.informative.concat(loc.localityInfo.administrative.filter(o => o.adminLevel >= 3)) .sort((firstEl, secondEl) => firstEl.order > secondEl.order ? 1 : -1) if (infos.length == 0) return null let maxorder = infos[infos.length - 1].order debug("]infos") debug(infos) return await getNearByLocationsFromLatLng(loc.latitude, loc.longitude) .then(locs => { debug("]locs") debug(locs) return locs.features.map(place => "wikidata" in place.properties ? { "order": Math.floor(maxorder + (Math.random() * maxorder) / 2.0), "name": place.properties.name, "description": place.properties.name + "(" + place.properties.kinds + ")", "wikidataId": place.properties.wikidata, "isPlaceFact": true } : null).filter(o => o != null) }) .then(async places => { debug("]places") debug(places) infos = infos.concat(places) .sort((firstEl, secondEl) => firstEl.order > secondEl.order ? 1 : -1); debug("]infos") debug(infos) let len = Object.keys(infos).length; if (len == 1) { let info = infos[0] if (info.order < 3) { needsWiki = false; return styleFact(info.name, info.description) } return info; } else { let filtered = infos.filter(o => o.order >= 3 && "wikidataId" in o); if (filtered.length < Settings.MaximumFactCountToDisplay) { filtered = infos.filter(o => o.order >= 2 && "wikidataId" in o); } filtered.forEach(obj => obj.description == "postal code" ? setNameToPostal(obj, loc.city == "" ? loc.principalSubdivision : loc.city) : null); debug("]filtered infos") debug(filtered) if (filtered.length == 0) { if (infos.length >= 1) { facts = infos.map(obj => { return { "text": styleFact(obj.name, obj.description), "link": INVALIDLINK, "isGeoFact": true } }) } needsWiki = false; return null; } let red = [] let i = 0; while (i < filtered.length) { let curr = filtered[i]; let t = await fetch(WIKIDATA_URL + curr.wikidataId) .then(res => res.json()) .then(out => (out.success != 1 || "error" in out || !("enwiki" in out.entities[curr.wikidataId].sitelinks)) ? "" : processAndGetWikidataTitle(out, curr)) if (t != "" && red.indexOf(t) == -1) red.push(t); i++; } return red.reverse().join("|") } }) }) } function processAndGetWikidataTitle(data, obj) { let title = data.entities[obj.wikidataId].sitelinks.enwiki.title; if ("isPlaceFact" in obj) { if (placeWikidataTitles.indexOf(title) == -1) { debug("]place title processed: " + title) placeWikidataTitles.push(title) } } else debug("]geographic title processed: " + title) return encodeURIComponent(title) } async function btnClick(btn) { return new Promise(resolve => btn.onclick = () => resolve()); } function getCorrectLocationDivForChallenge() { return document.querySelector('[alt="Correct location"]').parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement } async function getLocationObjectGame() { const tag = window.location.href.substring(window.location.href.lastIndexOf('/') + 1) const game_endpoint = "https://www.geoguessr.com/api/v3/games/" + tag const challenge_endpoint = "https://www.geoguessr.com/api/v3/challenges/" + tag + "/game" const api_url = isInChallange() ? challenge_endpoint : game_endpoint return fetch(api_url) .then(res => res.json()) .then(out => { let guess_counter = out.player.guesses.length let lat = out.rounds[guess_counter - 1].lat; let lng = out.rounds[guess_counter - 1].lng; return getLocationFromLatLng(lat, lng); }) } async function getLocationObjectBullseye() { const tag = window.location.href.substring(window.location.href.lastIndexOf('/') + 1) const api_url = "https://game-server.geoguessr.com/api/bullseye/" + tag return fetch(api_url, { credentials: "include" }) .then(res => res.json()) .then(out => { let guess_counter = out.players[0].guesses.length let lat = out.rounds[guess_counter - 1].panorama.lat; let lng = out.rounds[guess_counter - 1].panorama.lng; return getLocationFromLatLng(lat, lng); }) } async function getLocationObject() { return isInBullseye() ? getLocationObjectBullseye() : getLocationObjectGame() } function getNearByLocationsFromLatLng(lat, lng) { let api = OPENTRIP_URL + OpenTripMap_APIKEY + "&lat=" + lat + "&lon=" + lng return fetch(api) .then(res => res.json()) } function getLocationFromLatLng(lat, lng) { let api = API_URL + "latitude=" + lat + "&longitude=" + lng + "&key=" + BigDataCloud_APIKEY return fetch(api) .then(res => res.json()) } function getFactFromTitles(titles) { return fetch(WIKI_URL + titles) .then(res => res.json()) .then(result => { facts = [] let pages = result.query.pages; cleanPages(pages); debug("]cleaned pages"); debug(pages) let keys = Object.keys(pages); if (keys.length == 0) return null let del = 0; keys.forEach(k => { if (facts.length >= Settings.MaximumFactCountToDisplay) return let fact = pages[k]; if (fact == null) return let reduced = fact.extract; reduced = reduced.trimEnd("\n") if (reduced.endsWith("refer to:")) return if (reduced.length > Settings.MaximumFactMessageLength) { let paragraphs = reduced.split(". ") let j = 0 reduced = paragraphs.length != 0 ? "" : reduced while (j < paragraphs.length && reduced.length <= Settings.MaximumFactMessageLength) { reduced = reduced + ". " + paragraphs[j] j++; } // TO-DO: Add "read more" button //reduced = reduced.slice(0,Settings.MaximumFactMessageLength) + "..." } let f = { "text": styleFact(fact.title, reduced), "link": "https://en.wikipedia.org/?curid=" + fact.pageid, "isGeoFact": placeWikidataTitles.indexOf(fact.title) == -1 } //debug(f) facts.push(f) }) return facts; }); } function SetDisplayFact() { getTitlesFromLocation() .then(titles => { debug("]reduced titles result: " + titles) if (needsWiki) { getFactFromTitles(titles).then(facts => { if (facts == null) { facts = [{ "text": styleFact(titles.name, titles.description), "link": INVALIDLINK, "isGeoFact": true }] } debug(facts) setFactInnerHtml(); }) } else { needsWiki = true; if (titles != null) { facts = [{ "text": titles, "link": INVALIDLINK, "isGeoFact": true }] } setFactInnerHtml(); } }) } function getFactTitleColor(fact) { return fact.isGeoFact ? Settings.GeographyFactTitleColorName : Settings.FamousPlaceFactTitleColorName; } function getFactTitle(fact) { return fact.isGeoFact ? Settings.GeographyFactTitle : Settings.FamousPlaceFactTitle; } function getFactTextHtml(fact) { return `<div style="color: ${Settings.FactWikiTextColor}">` + fact.text.split(". ").reduce((prev, curr) => prev + "<br>" + curr) + "</div>"; } function setFactInnerHtml() { const new_tab = Settings.OpenInNewTab ? ` target="_blank" rel="noopener noreferrer"` : '' const style_source_text = `color: ${Settings.SourceLinkColorName}; font-size: 12px;`; let str = facts .map((fact, i) => { return `<br><h2 style="color: ${getFactTitleColor(fact)}">${getFactTitle(fact)} Fact ${Settings.DisplayFactNumber ? (i+1) : ""}</h2><span style="${style_source_text}">(</span><u><a href="${fact.link}"${new_tab}; style="${style_source_text}"><i>source</i></a></u><span style="${style_source_text}">)</span><br><div style="text-align: justify;text-justify: inter-word;">${getFactTextHtml(fact)}</div>` }) .join("<hr style='background: var(--ds-color-white-20, hsla(0,0%,100%,0.2)) ; height: .0625em ; border: 0 ; margin: 1rem 0 0.5rem 0'>") try { document.getElementById(_id_fact_div).innerHTML = str; } catch (error) { console.log(error); } } // 5 round game round summary div or null function get5RoundGameSummaryDiv() { let div = document.getElementsByClassName(_class_roundResult_5roundGame); if (div.length == 0) return null else return div[0] } function set5RoundGameSummaryDivPlaceHolder() { let newDiv1 = document.createElement("div") let parent = get5RoundGameSummaryDiv(); if (Settings.DisplayFactsBelowButtons) parent.parentElement.appendChild(newDiv1); else parent.insertBefore(newDiv1, parent.lastElementChild); newDiv1.innerHTML = SummaryLoadingPlaceHolderInnerHtml; } // Streak game round summary div or null function getStreakGameSummaryDiv() { let div = document.getElementsByClassName(_class_roundResult_streakGame); if (div.length == 0) return null else return div[0] } function setStreakGameSummaryDivPlaceHolder() { let newDiv1 = document.createElement("div") let parent = getStreakGameSummaryDiv(); if (Settings.DisplayFactsBelowButtons) parent.parentElement.appendChild(newDiv1); else parent.insertBefore(newDiv1, parent.lastElementChild); newDiv1.innerHTML = SummaryLoadingPlaceHolderInnerHtml; } // Bullseye round summary div or null function getBullseyeGameSummaryDiv() { let div = document.getElementsByClassName(_class_roundResult_Bullseye); if (div.length == 0) return null else return div[0] } function setBullseyeGameSummaryDivPlaceHolder() { let newDiv1 = document.createElement("div") let parent = getBullseyeGameSummaryDiv(); if (Settings.DisplayFactsBelowButtons) parent.appendChild(newDiv1); else parent.insertBefore(newDiv1, parent.lastElementChild); newDiv1.innerHTML = SummaryLoadingPlaceHolderInnerHtml; } function getBullseyeButtonDiv() { let div = document.getElementsByClassName(_class_nextButton_Bullseye); if (div.length == 0) return null else return div[0] } function setBullseyeButtonStyle() { let button = getBullseyeButtonDiv(); button.style.padding = "var(--vertical-padding, 0.75rem) var(--horizontal-padding, 1.5rem)" } function factCheckStateAttempt(newDiv1) { if (document.getElementById(_id_fact_div) || !isValidGame() || !isInRoundResultPage()) return if (get5RoundGameSummaryDiv()) set5RoundGameSummaryDivPlaceHolder() else if (getStreakGameSummaryDiv()) setStreakGameSummaryDivPlaceHolder() else if (getBullseyeGameSummaryDiv()) setBullseyeGameSummaryDivPlaceHolder() if (getBullseyeButtonDiv()) setBullseyeButtonStyle() }; function isInChallange() { return location.pathname.startsWith("/challenge/"); } function isInBullseye() { return location.pathname.startsWith("/bullseye/"); } function isInClassicGame() { return location.pathname.startsWith("/game/") || isInChallange(); } function isValidGame() { return isInClassicGame() || isInBullseye(); } function isInRoundResultPage() { if (isInClassicGame()) return !!document.querySelector('.result-layout_root__NfX12') else if (isInBullseye()) return !!document.querySelector('.round-score_container__avps2') return false } function isFactAlreadyChecked() { return sessionStorage.getItem("FactLocationChecked") != 0 } function factCheckState() { if (isValidGame() && isInRoundResultPage() && !isFactAlreadyChecked()) { SetDisplayFact(); checked = checked + 1; sessionStorage.setItem("FactLocationChecked", checked); } else if (isValidGame() && !isInRoundResultPage() && isFactAlreadyChecked()) { checked = 0; sessionStorage.setItem("FactLocationChecked", checked) }; } function tryFactCheck() { factCheckState(); setTimeout(factCheckState, 250); setTimeout(factCheckState, 500); setTimeout(factCheckState, 1200); setTimeout(factCheckState, 2000); setTimeout(factCheckStateAttempt, 300); setTimeout(factCheckStateAttempt, 500); setTimeout(factCheckStateAttempt, 1200); setTimeout(factCheckStateAttempt, 2000); }; document.body.addEventListener('transitionend', () => { if (isValidGame() && isInRoundResultPage() != isFactAlreadyChecked()) tryFactCheck() });