├── images ├── powered_by.png └── favicon-32x32.png ├── notes.txt ├── LICENSE ├── old_versions ├── index.html ├── style.css └── script.js ├── js └── seedrandom.min.js ├── help.html ├── README.md ├── index.html ├── style.css └── script2.js /images/powered_by.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonrolph/iNatGuessr/HEAD/images/powered_by.png -------------------------------------------------------------------------------- /images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonrolph/iNatGuessr/HEAD/images/favicon-32x32.png -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Code: 6 | 7 | get random inat observation by selecting random values to things like day and year 8 | Other params: species level, research grade 9 | 10 | do another query to get x (eg. 2) more observations within x radius 11 | 12 | present pictures and species names 13 | 14 | 15 | 16 | Daily? set locations based on the days date? 17 | 18 | 19 | UI 20 | 21 | 3 pictures 22 | 23 | map interface 24 | 25 | click on map 26 | 27 | reveal location and give score 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Simon Rolph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /old_versions/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |Click image to exit
18 |Click an image to get a closer look. Images may be subject to copyright.
23 | 24 |A prototype by Simon Rolph v0.0.4
34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /js/seedrandom.min.js: -------------------------------------------------------------------------------- 1 | !function(f,a,c){var s,l=256,p="random",d=c.pow(l,6),g=c.pow(2,52),y=2*g,h=l-1;function n(n,t,r){function e(){for(var n=u.g(6),t=d,r=0;niNatGuessr is a Guessr-style game where you are given a selection of images from iNaturalist observations. From these images it is your task to work out where they are from. When you make a guess the actual location will be revealed. The game is played over 5 rounds with 5000 points per round. The number of points you receive is scaled depending on the game that you're playing.
17 | 18 |You can specify a taxonomic group in the url by adding '?taxon_id=12345' and replace 12345 with the desired taxonomic ID. Similarly, you can specify a place in the url by by adding '?place_id=12345' and replace 12345 with the desired place ID. You can specify multiple IDs by using comma separated values eg. '?place_id=12345,12346'. You can combine places and taxa using & eg. '?place_id=12345&taxon_id=12345'. Want more than just places and taxa? You can use any of the normal iNaturalist search parameters.
20 |One of the easiest ways to create a game is to go to inaturalist.org/observations and use the filters to subset the observations to a game you want to play, then copy the url parameters into the iNatGuessr url. For example: inaturalist.org/observations?place_id=1&taxon_id=20979 becomes simonrolph.github.io/iNatGuessr/?place_id=1&taxon_id=20979 21 | 22 | 23 |
Endless: this game mode is the default game mode, it just keeps going!
25 |Daily challenge: like Wordle or other daily games, this game mode will select five unique locations which change daily.
26 |Custom seed: by 'seeding' the game you can ensure repeatability if you want to play with friends. If you select the same game type then enter the same seed (which can be any set of characters) then you will all get the same set of locations.
27 | 28 |iNatGuessr is powered by the iNaturalist API.
30 | 31 |You can view the code for iNatGuessr on GitHub: github.com/simonrolph/iNatGuessr. If you find a bug or have suggestions then feel free to contribute via an issue or pull request.
32 |Many thanks to those who contributed to the discussion on the iNaturalist Forum
33 | 34 |By Simon Rolph v0.1.6
35 | 36 |
37 | Click image to exit
24 |Welcome to iNatGuessr. A Guessr-style game where you are given a selection of images from iNaturalist observations. From these images it is your task to work out where they are from. When you make a guess the actual location will be revealed. The game is played over 5 rounds with 5000 points per round. Good luck! New to iNatGuessr? View the guide
34 | 35 | 36 | 37 |Game type: Everything, everywhere 🌍 | Birds 🐦🌍 | Plants 🌳🌹🌻🌍 | Herptiles 🐸🦎🐊🌍 | Fish 🐟🐠🌍 | USA only 🦅 | North American freshwater fish 🐟 | Trees 🌳🌲🌴🌍 | Water birds 🦆🌍 | Molluscs 🐌🐚🌍
39 | 58 |Current game set-up: Everything, everywhere
61 | 62 | 63 | 64 | 87 | 88 |By Simon Rolph v0.1.6
89 | 90 |
91 | Observations by " + attrib.join(", ") + "
" +``; // add attributions 196 | }) 197 | }) 198 | 199 | // SECRET LOCATION ON MAP ----------- 200 | // Define the secret location (latitude and longitude) 201 | var secretLocation = L.latLng(focal_lat, focal_lon); 202 | 203 | // Create a marker for the secret location 204 | var secretMarker = L.marker(secretLocation, { 205 | opacity: 0, // Initially hide the marker 206 | }).addTo(map); 207 | 208 | 209 | 210 | 211 | secretMarker._icon.classList.add("not-clickable"); 212 | 213 | 214 | 215 | // Initialize a variable to keep track of whether the secret is revealed 216 | var secretRevealed = false; 217 | 218 | var clickedMarker; // To store the user's clicked marker 219 | var line; // To store the line between the markers 220 | 221 | 222 | // enable the map 223 | var mapContainer = document.getElementById("map"); 224 | mapContainer.classList.remove("disabled"); 225 | 226 | 227 | // WHAT HAPPENS WHEN YOU CLICK ON THE MAP 228 | // Function to reveal the secret location and distance on click 229 | map.on('click', function (e) { 230 | if (!secretRevealed) { 231 | // Calculate distance between clicked point and secret location 232 | var distance = e.latlng.distanceTo(secretLocation); 233 | 234 | // Update the marker opacity to reveal the secret location 235 | secretMarker.setOpacity(1); 236 | 237 | // Set the secret as revealed 238 | secretRevealed = true; 239 | 240 | 241 | if (clickedMarker) { 242 | map.removeLayer(clickedMarker); // Remove previous clicked marker 243 | map.removeLayer(line); // Remove previous line 244 | } 245 | 246 | // Add a marker where the user clicked 247 | clickedMarker = L.marker(e.latlng).addTo(map); 248 | 249 | // Draw a line between the clicked marker and the secret location 250 | line = L.polyline([e.latlng, secretLocation], { 251 | color: 'blue', 252 | }).addTo(map); 253 | 254 | // Calculate the midpoint between the clicked point and the secret location 255 | var midpoint = L.latLng( 256 | (e.latlng.lat + secretLocation.lat) / 2, 257 | (e.latlng.lng + secretLocation.lng) / 2 258 | ); 259 | 260 | // Display the distance in a popup at the midpoint 261 | var distance = e.latlng.distanceTo(secretLocation)/1000; 262 | L.popup({ closeButton: false, offset: [0, -15] }) 263 | .setLatLng(midpoint) 264 | .setContent('Distance: ' + Math.round(distance) + ' km') 265 | .openOn(map); 266 | 267 | console.log(); 268 | 269 | 270 | // show attribution 271 | document.getElementById("attribContainer").style.display = "block"; 272 | 273 | // add the taxons to the map 274 | //L.tileLayer('https://api.inaturalist.org/v1/grid/{z}/{x}/{y}.png?taxon_id='+taxon_ids.join(","),{ 275 | // maxZoom: 19, 276 | // opacity: 0.8, 277 | // attribution: 'iNaturalist' 278 | //}).addTo(map); 279 | 280 | // add the scores to the board 281 | var scoreContainer = document.getElementById("scoreContainer"); 282 | var roundScore = document.createElement("p"); 283 | 284 | // Add some text to theelement 285 | if (urlParams.includes("place_id")) { 286 | roundScore.innerHTML = `Round ${roundNumber}: ${calculateScorePlace(distance,placeArea)} Points (${Math.round(distance)}km)`; 287 | gameScoreTotal = gameScoreTotal+calculateScorePlace(distance,placeArea); 288 | } else { 289 | roundScore.innerHTML = `Round ${roundNumber}: ${calculateScore(distance)} Points (${Math.round(distance)}km)`; 290 | gameScoreTotal = gameScoreTotal+calculateScore(distance); 291 | } 292 | 293 | scoreContainer.appendChild(roundScore); 294 | 295 | if((roundNumber%5)==0){ 296 | var gameScore = document.createElement("h4"); 297 | gameScore.innerHTML = `Game ${roundNumber/5}: ${gameScoreTotal} / 25000 Points`; 298 | scoreContainer.appendChild(gameScore); 299 | gameScoreTotal = 0; 300 | } 301 | 302 | 303 | roundNumber = roundNumber+1; 304 | 305 | // make the button reappear 306 | document.getElementById("refreshButton").style.display = "inline-block"; 307 | } 308 | }); 309 | 310 | }) 311 | .catch(error => { 312 | console.error('Error fetching data:', error); 313 | }); 314 | 315 | } 316 | 317 | 318 | // NEXT ROUND BUTTON 319 | document.addEventListener("DOMContentLoaded", function () { 320 | var refreshButton = document.getElementById("refreshButton"); 321 | 322 | refreshButton.addEventListener("click", function () { 323 | 324 | 325 | // CLEAN UP -------------- 326 | // Clear the image container before appending new images 327 | var imageContainer = document.getElementById("imageContainer"); 328 | imageContainer.innerHTML = ''; // Empty the container 329 | document.getElementById("attribContainer").style.display = "none"; //hide the attribution 330 | document.getElementById("refreshButton").style.display = "none"; 331 | 332 | var mapContainer = document.getElementById("mapContainer"); 333 | mapContainer.innerHTML = '
'; // Remove the map 334 | 335 | imageContainer.innerHTML="Loading observations..."; 336 | 337 | fetchDataAndProcess(); // Call the function to redo the process 338 | document.body.scrollTop = document.documentElement.scrollTop = 0; 339 | }); 340 | 341 | // Call the function initially to perform the process 342 | fetchDataAndProcess(); 343 | }); 344 | 345 | 346 | // IMAGE MODAL 347 | 348 | // Modal Setup 349 | var modal = document.getElementById('modal'); 350 | 351 | modal.addEventListener('click', function() { 352 | modal.style.display = "none"; 353 | }); 354 | 355 | // global handler 356 | document.addEventListener('click', function (e) { 357 | if (e.target.className.indexOf('modal-target') !== -1) { 358 | var img = e.target; 359 | var modalImg = document.getElementById("modal-content"); 360 | modal.style.display = "block"; 361 | modalImg.src = img.src.replace("medium","large"); 362 | } 363 | }); 364 | -------------------------------------------------------------------------------- /script2.js: -------------------------------------------------------------------------------- 1 | // get things from the page 2 | var imageContainer = document.getElementById("imageContainer"); 3 | var nextRoundButton = document.getElementById("refreshButton"); 4 | var playButton = document.getElementById("playButton"); 5 | var confirmGuessButtonButton = document.getElementById("confirmGuessButton"); 6 | var scoreContainer = document.getElementById("scoreContainer"); 7 | var inatOutwardContainer = document.getElementById("inatOutwardContainer"); 8 | var taxonContainer = document.getElementById("taxonContainer"); 9 | var placeContainer = document.getElementById("placeContainer"); 10 | var scoreFactorContainer = document.getElementById("scoreFactorContainer"); 11 | 12 | // Declare as global variables 13 | var roundNumber = 1; 14 | var gameScoreTotal = 0; 15 | var scoreFactor = 1; 16 | 17 | var roundsPerGame = 5; 18 | var nGames =100; 19 | var secretRevealed = false; 20 | var secretLocation; 21 | var secretMarker; 22 | var map; 23 | var nPhotos = 12; 24 | 25 | // things applied to all queries 26 | var baseParams = `&captive=false&geoprivacy=open&quality_grade=research&photos=true&geo=true&acc_below=150` 27 | // console.log(baseParams) 28 | 29 | // Get the input field 30 | var inputField = document.getElementById("fname"); 31 | 32 | 33 | // Get the value of the "seed" parameter from the URL 34 | var urlParams = new URLSearchParams(window.location.search); 35 | var seedValue = urlParams.get("seed"); 36 | 37 | // console.log(seedValue); 38 | 39 | // Get the radio buttons 40 | var dailyRadio = document.getElementById("dailyRadio"); 41 | var endlessRadio = document.getElementById("endlessRadio"); 42 | var customRadio = document.getElementById("customRadio"); 43 | 44 | // Check the value of the "seed" parameter and set the radio and input field accordingly 45 | if (seedValue === "daily") { 46 | dailyRadio.checked = true; 47 | inputField.value = ""; 48 | } else if (seedValue) { 49 | customRadio.checked = true; 50 | inputField.value = seedValue; 51 | } else { 52 | endlessRadio.checked = true; 53 | inputField.value = ""; 54 | } 55 | 56 | // update the user parameter info 57 | function showParams() { 58 | var urlParams = window.location.search; 59 | if (urlParams.includes("?")) { 60 | var getQuery = urlParams.split('?')[1]; 61 | var params = getQuery.split('&'); 62 | // console.log("Custom URL parameters supplied") 63 | 64 | var paramInfo = document.getElementById("parameter_info"); 65 | paramInfo.innerHTML = params.join(" AND "); 66 | } 67 | } 68 | 69 | showParams(); 70 | 71 | 72 | 73 | // get custom user query parameters 74 | function getCustomParams() { 75 | var urlParams = window.location.search; 76 | if (urlParams.includes("?")) { 77 | var getQuery = urlParams.split('?')[1]; 78 | var params = getQuery.split('&'); 79 | console.log("Custom URL parameters supplied") 80 | 81 | var extra_params = "&"+params.join("&"); 82 | } else { 83 | extra_params = ""; 84 | console.log("No custom URL parameters supplied") 85 | } 86 | return extra_params 87 | } 88 | 89 | 90 | 91 | 92 | 93 | // set up seeding 94 | function getFormattedUtcDate() { 95 | const today = new Date(); 96 | const year = today.getUTCFullYear(); 97 | const month = String(today.getUTCMonth() + 1).padStart(2, '0'); // Month is 0-indexed 98 | const day = String(today.getUTCDate()).padStart(2, '0'); 99 | return `${year}-${month}-${day}`; 100 | } 101 | 102 | function timeUntilNextUtcDay() { 103 | // Get the current UTC date and time 104 | const now = new Date(); 105 | const utcNow = new Date(now.getTime() + now.getTimezoneOffset() * 60000); // Convert to UTC 106 | 107 | // Calculate the UTC time at midnight of the next day 108 | const nextDay = new Date(utcNow); 109 | nextDay.setUTCDate(nextDay.getUTCDate() + 1); 110 | nextDay.setUTCHours(0, 0, 0, 0); 111 | 112 | // Calculate the time difference in milliseconds 113 | const timeDifference = nextDay - utcNow; 114 | 115 | // Convert the time difference to hours, minutes, and seconds 116 | const hours = Math.floor(timeDifference / (1000 * 60 * 60)); 117 | const minutes = Math.floor((timeDifference % (1000 * 60 * 60)) / (1000 * 60)); 118 | const seconds = Math.floor((timeDifference % (1000 * 60)) / 1000); 119 | 120 | return { hours, minutes, seconds }; 121 | } 122 | 123 | 124 | 125 | var customParams = ""; 126 | var seeded = false; 127 | var obvsIdsSeededParams = []; 128 | var daily = false; 129 | 130 | // user supplied parameyers 131 | function getParamsToStartGame() { 132 | customParams = getCustomParams(); 133 | if (customParams.includes("seed")) { 134 | seeded= true; // useful variable as to whether a seed is being used 135 | var maxID = 10000000 136 | var params = customParams.split('&'); 137 | var seed = params[params.findIndex(params => params.includes("seed"))].split("=")[1]; // get the seed 138 | 139 | // daily special seed 140 | if (seed.toLowerCase() == "daily"){ 141 | daily=true; 142 | console.log("DAILY CHALLENGE ACTIVATED!") 143 | seed = getFormattedUtcDate(); 144 | scoreContainer.innerHTML = "Challenge URL: "+window.location.href+"
Challenge date: "+seed+"
"; 145 | 146 | const timeRemaining = timeUntilNextUtcDay(); 147 | console.log(`Time until next UTC day: ${timeRemaining.hours} hours, ${timeRemaining.minutes} minutes, ${timeRemaining.seconds} seconds`); 148 | 149 | } 150 | 151 | myrng = new Math.seedrandom(seed); 152 | 153 | 154 | for (let step = 0; step < 200; step++) { 155 | obvsIdsSeededParams.push("&id_above="+Math.floor(myrng()*maxID)+"&order_by=id&order=asc"); 156 | } 157 | } else { 158 | 159 | obvsIdsSeededParamsUnseeded = "&id_above="+Math.floor(Math.random()*5000)+"&order_by=random"; 160 | } 161 | } 162 | 163 | 164 | 165 | 166 | // add the image to the board 167 | function addImage(result) { 168 | 169 | var imageURL = result.photos[0].url.replace("/square", "/medium"); 170 | 171 | // Create a figure element to contain the image and caption 172 | var figureElement = document.createElement("figure"); 173 | 174 | // Create an img element for the image 175 | var imgElement = document.createElement("img"); 176 | imgElement.src = imageURL; 177 | imgElement.className = "modal-target"; 178 | 179 | // Append the img element to the figure element 180 | figureElement.appendChild(imgElement); 181 | 182 | // Create a figcaption element for the caption 183 | var figcaptionElement = document.createElement("figcaption"); 184 | 185 | var captionText = ""+result.taxon.name + " by " + result.user.login; 186 | 187 | figcaptionElement.innerHTML = captionText; // Set the caption text 188 | figcaptionElement.style.display = "none" 189 | 190 | // Append the figcaption element to the figure element 191 | figureElement.appendChild(figcaptionElement); 192 | 193 | // Append the figure element to the image container 194 | imageContainer.appendChild(figureElement); 195 | } 196 | 197 | // alternative approach for getting a bounding box 198 | // Work in progress TODO 199 | function flngfromgrid(x,z) { return (x/Math.pow(2,z)*360-180); }; 200 | function flatfromgrid(y,z) { 201 | let n = Math.PI-2*Math.PI*y/Math.pow(2,z); 202 | return (180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n)))); 203 | }; 204 | 205 | async function createBoundingBoxQueryGrid(){ 206 | var apiUrl = `https://api.inaturalist.org/v1/heatmap/0/0/0.grid.json?per_page=0`+baseParams+customParams; 207 | const response = await fetch(apiUrl); 208 | const data = await response.json(); 209 | 210 | // GET A ROW 211 | var rows = [] 212 | 213 | // create an array with the row values that contain data 214 | for (let i = 0; i < 64; i++) { 215 | var nInRow = data.grid[i].trim().length 216 | if (nInRow>0){ 217 | rows = rows.concat(Array.from({ length: nInRow }, () => i)); 218 | } 219 | } 220 | 221 | // select a row with data in it 222 | if (seeded) { 223 | var randomRow = rows[Math.floor(myrng() * rows.length)]; 224 | } else { 225 | var randomRow = rows[Math.floor(Math.random() * rows.length)]; 226 | } 227 | 228 | 229 | 230 | // GET A COL 231 | var rowData = data.grid[randomRow]; 232 | const nonWhitespacePositions = []; 233 | for (let ii = 0; ii < rowData.length; ii++) { 234 | if (rowData[ii] !== ' ') { 235 | nonWhitespacePositions.push(ii); 236 | } 237 | } 238 | 239 | // select a colmn with data in it 240 | if (seeded) { 241 | var randomCol = nonWhitespacePositions[Math.floor(myrng() * nonWhitespacePositions.length)]; 242 | } else { 243 | var randomCol = nonWhitespacePositions[Math.floor(Math.random() * nonWhitespacePositions.length)]; 244 | } 245 | 246 | 247 | randomRow = Math.floor(randomRow/2); 248 | randomCol = Math.floor(randomCol/2); 249 | 250 | // console.log("x = " + randomCol); 251 | // console.log("y = " + randomRow); 252 | 253 | const z=5; 254 | const neLat = flatfromgrid(randomRow,z); 255 | const neLng = flngfromgrid(randomCol+1,z); 256 | const swLat = flatfromgrid(randomRow+1,z); 257 | const swLng = flngfromgrid(randomCol,z); 258 | 259 | const boundingBoxString = `&nelat=${neLat}&nelng=${neLng}&swlat=${swLat}&swlng=${swLng}`; 260 | // console.log(boundingBoxString); 261 | return boundingBoxString; 262 | } 263 | 264 | // create boundingbox 265 | function createGlobalBoundingBoxString() { 266 | if (seeded) { 267 | var lat = myrng() * 130 - 65; 268 | var long = myrng() * 340 - 170; 269 | } else { 270 | var lat = Math.random() * 130 - 65; 271 | var long = Math.random() * 340 - 170; 272 | } 273 | 274 | 275 | 276 | const latRange = 30 + Math.floor(20*(Math.abs(lat)/65)); // bigger lat range nearer the poles 277 | const longRange = 30; 278 | 279 | const neLat = lat + latRange; 280 | const neLng = long + longRange; 281 | const swLat = lat - latRange; 282 | const swLng = long - longRange; 283 | 284 | const boundingBoxString = `&nelat=${neLat}&nelng=${neLng}&swlat=${swLat}&swlng=${swLng}`; 285 | 286 | 287 | return boundingBoxString; 288 | } 289 | 290 | // function to get a random observation 291 | async function getRandomObvs() { 292 | // console.log(baseParams); 293 | // console.log(customParams); 294 | 295 | if(seeded){ 296 | var seedParams = obvsIdsSeededParams[roundNumber-1]; 297 | } else { 298 | var seedParams = obvsIdsSeededParamsUnseeded; 299 | } 300 | 301 | var boundingBox = await createGlobalBoundingBoxString() 302 | 303 | // if any custom parameter have been assigned 304 | if (customParams.includes("place_id")) { 305 | var apiUrl = `https://api.inaturalist.org/v1/observations?per_page=1&page=${roundNumber}${baseParams}${customParams}${seedParams}`; 306 | } else { 307 | var apiUrl = `https://api.inaturalist.org/v1/observations?per_page=1&page=${roundNumber}${baseParams}${customParams}${seedParams}`+boundingBox; 308 | } 309 | // console.log( apiUrl); 310 | 311 | 312 | const response = await fetch(apiUrl); 313 | const data = await response.json(); 314 | //// console.log(data); 315 | imageContainer.innerHTML="" 316 | addImage(data.results[0]); 317 | 318 | return data; 319 | } 320 | 321 | // shuffle array 322 | function shuffleArray(array) { 323 | for (let i = array.length - 1; i > 0; i--) { 324 | const j = Math.floor(Math.random() * (i + 1)); 325 | [array[i], array[j]] = [array[j], array[i]]; 326 | } 327 | } 328 | 329 | // get n supporting observations 330 | async function getSupportingObvs(nObvs, lat, lng,idIgnore) { 331 | var radius = 10; 332 | 333 | if (seeded){ // if there's a seed then we can't rely on order_by=random so we get top 200(or less) ordered by ID then randomly pick 7 from that 200 334 | // get ids of 200 (not there is a createed at d2 to try to avoid someone uploading an obvservation mid day and messing it up) 335 | var apiUrl1 = `https://api.inaturalist.org/v1/observations?lat=${lat}&lng=${lng}&radius=${radius}&per_page=200&only_id=true&order_by=id${baseParams}${customParams}¬_id=${idIgnore}&created_d2=2023-09-06&order=asc`; 336 | var response1 = await fetch(apiUrl1); 337 | var data1 = await response1.json(); 338 | 339 | // get nObvs IDs from the list 340 | var nResults = data1.results.length; 341 | var selectedIDs = []; 342 | for (let i = 0; i < nObvs; i++) { 343 | selectedIDs.push(data1.results[Math.round(i / nObvs * nResults)].id); 344 | } 345 | 346 | // console.log(selectedIDs); 347 | 348 | // do another API call for those IDs 349 | var apiUrl2 = 'https://api.inaturalist.org/v1/observations?id='+selectedIDs.join(","); 350 | var response2 = await fetch(apiUrl2); 351 | var data = await response2.json(); 352 | 353 | } else { 354 | var apiUrl = `https://api.inaturalist.org/v1/observations?lat=${lat}&lng=${lng}&radius=${radius}&per_page=${nObvs}&order_by=random${baseParams}${customParams}¬_id=${idIgnore}`; 355 | var response = await fetch(apiUrl); 356 | var data = await response.json(); 357 | } 358 | 359 | 360 | data.results.forEach(function(element) { 361 | addImage(element); 362 | }); 363 | 364 | return data; 365 | } 366 | 367 | 368 | 369 | function clearPage(){ 370 | imageContainer.innerHTML="Loading observations..." 371 | inatOutwardContainer.style.display = "none"; 372 | nextRoundButton.style.display = "none"; 373 | confirmGuessButtonButton.style.display = "inline-block"; 374 | var mapContainer = document.getElementById("mapContainer"); 375 | mapContainer.innerHTML = ''; // Remove the map 376 | secretRevealed = false; 377 | 378 | scoreFactorContainer.innerHTML = `Score guide: 1000km = ${calculateScore(1000)} points, 500km = ${calculateScore(500)} points, <${Math.ceil(100*Math.sqrt(scoreFactor))}km = 5000 points
` 379 | } 380 | 381 | 382 | 383 | 384 | function createMap(focal_lat,focal_lng,imageUrl){ 385 | // MAP -------------- 386 | // Initialize the map 387 | map = L.map('map',{minZoom: 1}).setView([0, 0], 1); 388 | 389 | // Create a tile layer using OpenStreetMap tiles 390 | L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 391 | maxZoom: 19, 392 | }).addTo(map); 393 | 394 | // SECRET LOCATION ON MAP ----------- 395 | // Define the secret location (latitude and longitude) 396 | secretLocation = L.latLng(focal_lat, focal_lng); 397 | 398 | 399 | // Create a custom icon for the marker 400 | const obsvsMarkerIcon = L.icon({ 401 | iconUrl: imageUrl, // URL of the custom image 402 | iconSize: [32, 32], // Size of the icon image (width, height) 403 | iconAnchor: [16, 16], // Anchor point of the icon (centered at the bottom) 404 | popupAnchor: [0, -32] // Popup anchor point relative to the icon (above the icon) 405 | }); 406 | 407 | 408 | // Create a marker for the secret location 409 | secretMarker = L.marker(secretLocation, { 410 | opacity: 0, // Initially hide the marker 411 | icon: obsvsMarkerIcon // add the custom marker type 412 | }).addTo(map); 413 | 414 | secretMarker._icon.style.borderRadius = '50%'; 415 | secretMarker._icon.style.boxShadow = '0px 0px 10px rgba(0, 0, 0, 0.4)'; // Add a drop shadow 416 | 417 | secretMarker._icon.classList.add("not-clickable"); 418 | 419 | // Attach a click event listener to the map 420 | map.on('click', handleMapClick); 421 | 422 | // stop going round and round 423 | map.setMaxBounds([ 424 | [-90, -270], // South-West corner of the bounding box 425 | [90, 270], // North-East corner of the bounding box 426 | ]); 427 | 428 | } 429 | 430 | let clickedMarker; 431 | let whereClicked 432 | // Add this function to your code to handle map clicks 433 | function handleMapClick(e) { 434 | if (!secretRevealed) { 435 | // Check if there's already a marker on the map 436 | if (clickedMarker) { 437 | // Remove the current marker from the map 438 | map.removeLayer(clickedMarker); 439 | } 440 | 441 | // Add a marker where the user clicked 442 | clickedMarker = L.marker(e.latlng).addTo(map); 443 | whereClicked = e; 444 | } 445 | } 446 | 447 | 448 | // Add this function to your code to handle map clicks 449 | function theReveal(e) { 450 | 451 | if (!secretRevealed && whereClicked) { 452 | // Calculate distance between clicked point and secret location 453 | var distance = e.latlng.distanceTo(secretLocation); 454 | 455 | // Update the marker opacity to reveal the secret location 456 | secretMarker.setOpacity(1); 457 | 458 | // Set the secret as revealed 459 | secretRevealed = true; 460 | 461 | 462 | // move the users guess in longitude if they are far away 463 | let angularDifference = e.latlng.lng - secretMarker._latlng.lng; 464 | // Adjust longitude2 to be closer to longitude1 by wrapping around 465 | if (angularDifference > 180) { 466 | e.latlng.lng-= 360; 467 | } else if (angularDifference < -180) { 468 | e.latlng.lng += 360; 469 | } 470 | 471 | // Draw a line between the clicked marker and the secret location 472 | var line = L.polyline([e.latlng, secretLocation], { 473 | color: '#45a049', 474 | }).addTo(map); 475 | 476 | // Calculate the midpoint between the clicked point and the secret location 477 | var midpoint = L.latLng( 478 | (e.latlng.lat + secretLocation.lat) / 2, 479 | (e.latlng.lng + secretLocation.lng) / 2 480 | ); 481 | 482 | // Display the distance in a popup at the midpoint 483 | var distance = e.latlng.distanceTo(secretLocation)/1000; 484 | L.popup({ closeButton: false, offset: [0, -15] }) 485 | .setLatLng(midpoint) 486 | .setContent('Distance: ' + Math.round(distance) + ' km') 487 | .openOn(map); 488 | 489 | // zoom to the map 490 | var group = new L.featureGroup([clickedMarker, secretMarker]); 491 | map.fitBounds(group.getBounds()); 492 | 493 | // Get all figure elements within #imageContainer 494 | const figureElements = document.querySelectorAll('#imageContainer figure'); 495 | 496 | // Loop through each figure and reveal its caption 497 | figureElements.forEach(function (figureElement) { 498 | const caption = figureElement.querySelector('figcaption'); 499 | if (caption) { 500 | caption.style.display = 'block'; // or 'inline' as needed 501 | } 502 | }); 503 | 504 | 505 | addScore(distance); 506 | 507 | // hide comfirm guess button 508 | confirmGuessButtonButton.style.display = "none" 509 | 510 | // make the button reappear 511 | if(((roundNumber-1)/roundsPerGame)