-
Notifications
You must be signed in to change notification settings - Fork 6
/
iNat_countries.html
347 lines (330 loc) · 17 KB
/
iNat_countries.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, minimum-scale=1.0" />
<meta name="description" content="iNaturalist Countries" />
<title>iNaturalist Countries</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap" rel="stylesheet">
<style>
:root {
background: var(--color-base);
color: var(--color-text);
font: 14px Sans-Serif;
--color-base: white;
--color-alt: whitesmoke;
--color-brand: forestgreen;
--color-text: black;
--color-text-invert: white;
--color-text-link: royalblue;
--color-border: lightgray;
--color-hover: lightgray;
--color-base-translucent: rgba(255,255,255,0.85);
}
@media (prefers-color-scheme: dark) {
:root {
--color-base: black;
--color-alt: #171717;
--color-brand: forestgreen;
--color-text: #bababa;
--color-text-invert: black;
--color-text-link: cornflowerblue;
--color-border: #444;
--color-hover: #444;
--color-base-translucent: rgba(0,0,0,0.85);
}
}
#main { width:100%; }
table, td, th { border-collapse:collapse; margin:0; padding:4px; }
th { position:-webkit-sticky /*Safari*/; position:sticky; top:0; z-index:100; font-weight:600; background:var(--color-brand); color:var(--color-text-invert); text-align:left; vertical-align:bottom; }
tbody>tr { border-width:1px 0px; border-style:solid; border-color:var(--color-border); background:var(--color-alt);}
tr:nth-child(even) { background:var(--color-base); }
.tar { text-align:right; }
.icon { height:48px; width:48px; border-radius:50%; }
.photo { height:64px; width:64px; }
img { margin:0; padding:0; border:0; }
a { text-decoration:none; color:var(--color-text-link); }
a:hover { background:var(--color-hover); }
.button_more_less { margin-left:5px; color:var(--color-text-link); background:var(--color-base); border:2px solid var(--color-border); border-radius:3px; }
.button_more_less:hover { background:var(--color-hover); }
.flag { font-family: 'Noto Color Emoji', sans-serif; }
</style>
</head>
<body>
<script>
//get parameters from the url
let winurlstr = window.location.href;
let winurlsearchstr = window.location.search;
let winurlexsearchstr = winurlstr.replace(winurlsearchstr,'');
let winurlparams = new URLSearchParams(winurlsearchstr.substring(1));
let p_country_code = winurlparams.get('country_code');
let p_place_id = winurlparams.get('place_id');
let p_parent_place_id = winurlparams.get('parent_place_id');
if (p_country_code!==null) { p_country_code = p_country_code.toLowerCase().split(','); };
if (p_place_id!==null) { p_place_id = p_place_id.split(','); };
if (p_parent_place_id!==null) { p_parent_place_id = p_parent_place_id.split(','); };
let stats = winurlparams.get('stats')||[];
let show = {};
show.observations = stats.includes('observations');
show.species = stats.includes('species');
show.observers = stats.includes('observers');
show.identifiers = stats.includes('identifiers');
winurlparams.delete('stats');
winurlparams.delete('country_code');
winurlparams.delete('place_id'); // will be set to individual countries
winurlparams.delete('parent_place_id');
winurlparams.delete('page');
winurlparams.append('page',1);
winurlparams.delete('per_page');
winurlparams.append('per_page',0); // we don't actually need to return any detail records. we just need the total_records value returned in the response.
function fdate(str,dateonly=false) {
str = str.replace(/t/i,' '); //replaces T (case insensitive) with a space
if (dateonly) { str = str.split(' ')[0]; }
else {
str = str.replace(/([+-]\d{2}\:?\d{2})/,' ($1)'); //puts parenthesis around time zone offset
str = str.replace(/z/i,' (+00:00)'); //replaces Z (case insensitve) with UTC
str = str.replace('+00:00','±00:00');
};
return str;
};
function furl(url,txt=url) { return '<a href="'+url+'">'+txt+'</a>'; };
function fcomnum(n) { return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g,',') };
function faddelem(etype,eparent=null,eattributes={}) {
let eobj = document.createElement(etype);
for (let [key,value] of Object.entries(eattributes)) {
if ( typeof value === 'object' && value !== null ) {
for (let [subkey,subvalue] of Object.entries(value)) { eobj[key][subkey] = subvalue; };
}
else { eobj[key] = value; };
};
if (eparent) { eparent.appendChild(eobj); };
return eobj;
};
function faddelems(etype,eparent=null,eattributes=[]) { for (let e of eattributes) { faddelem(etype,eparent,e); }; };
const getFlagEmoji = countryCode=>String.fromCodePoint(...[...countryCode.toUpperCase()].map(x=>127397+x.charCodeAt()));
function fgetname(ary,key) {
let obj = ary.find(o=>o.id===key);
return obj?obj.name:key;
};
function fgetwdinfo(ary,key) {
let obj = ary.find(o=>o.isoCode.value===key);
return obj ? {
isoCode : obj.isoCode.value,
population : obj.population?obj.population.value:null,
area : obj.area?obj.area.value:null,
entity: obj.country?obj.country.value.substring(obj.country.value.lastIndexOf('/')+1):null,
url: obj.country?obj.country.value:null
}
: { isoCode:key };
};
function fround(num,places) {
let n = num*1;
return n.toFixed(places);
};
function ffetch(url) {
return fetch(url)
.then((response) => {
if (!response.ok) { throw new Error(response.status+': '+response.statusText); };
return response.json();
})
.then((data) => { return data; })
.catch((err) => { console.error(err); });
};
function delay(time,value) {
return new Promise(function(resolve) {
setTimeout(resolve.bind(null, value), time)
console.log('Pausing '+time+'ms...')
});
}
let apiref = {};
apiref.base = 'https://api.inaturalist.org/v1/docs/#!/Observations/';
apiref.observations = apiref.base+'get_observations';
apiref.species = apiref.base+'get_observations_species_counts';
apiref.identifiers = apiref.base+'get_observations_identifiers';
apiref.observers = apiref.base+'get_observations_observers';
faddelem('h1',document.body,{innerText:'iNaturalist Countries'});
let info = faddelem('p',document.body,{innerHTML:'By default, this page pulls back a list of all countries set up in iNaturalist. It also will get population and area from Wikidata, if available, keying on ISO 3166-1 Alpha-2 Code. Optionally, it can pull back counts of observations, species, identifiers, and observers for each country.'});
let info_more = faddelem('div',document.body,{style:{display:'none'}});
faddelem('p',info_more,{innerHTML:'To get the extra statistics, add a "stats" parameter to the URL, and set it to a comma-separated list of values including "observations", "species", "identifiers", and/or "observers". These extra stats can also be filtered in the URL using the parameters available in the '+furl(apiref.observations,'observations')+', '+furl(apiref.species,'species')+', '+furl(apiref.identifiers,'identifiers')+', and '+furl(apiref.observers,'observers')+' API endpoints. It is also possible to return only specific countries using "country_code" (alpha-2 code), "place_id" (country place id), and "parent_place_id" (continent place id) parameters, each of which can accept a comma-separated list of values.'});
faddelem('p',info_more,{innerHTML:'For example, to get countries in North and South America, along with counts of research-grade bird observations and associated observers, open '+furl(winurlexsearchstr+'?parent_place_id=97389,97394&taxon_id=3&quality_grade=research&stats=observations,observers')+' in your browser.'});
faddelem('p',info_more,{innerHTML:'To adhere to recommended API request limits when getting the additional stats, this page will request one data point per second. Additionally, you should be careful of how often you use this page to get those additional stats so that you do not exceed the recommended maximum 10,000 daily API request limit. See the '+furl('https://www.inaturalist.org/pages/api+recommended+practices','API recommended practices page')+' for more information.'});
let button_more = faddelem('button',info,{classList:'button_more_less',innerHTML:'show more ▼'});
let button_less = faddelem('button',info_more.lastChild,{classList:'button_more_less',innerHTML:'show less ▲'});
button_more.addEventListener("click", function() {
button_more.style.display='none';
info_more.style.display='initial';
});
button_less.addEventListener("click", function() {
button_more.style.display='initial';
info_more.style.display='none';
});
// get places where the place type = country
// note that not all of these are countries. so we separately filter for admin level = 0 (since that filter doesn't exist in the API itself).
let apiurl = 'https://www.inaturalist.org/places.json?place_type=country&per_page=200';
let promiseary = [];
for (i=1;i<=2;i++) { // there shouldn't be more than 2 sets needed
promiseary.push(ffetch(apiurl+'&page='+i));
};
Promise.all(promiseary).then(data=>{
let placeary = [];
for (set of data) {
for (place of set) {
if (place.admin_level!==0) { continue; }
else if (p_country_code && (place.code==null || !p_country_code.includes(place.code.toLowerCase()))) { continue; }
else if (p_place_id && !p_place_id.includes(place.id.toString())) { continue; }
else if (p_parent_place_id && (place.parent_id==null || !p_parent_place_id.includes(place.parent_id.toString()))) { continue; }
else { placeary.push(place); };
};
};
return placeary;
})
// get the continents (parent places) associated with the countries
// also get population and area information from Wikidata. (after writing an appropriate sparql query using the Wikidata query service at https://query.wikidata.org/, the javascript code for below was taken more or less as suggested by the service.)
.then(async countries=>{
let continents = [];
for (country of countries) { if(!continents.includes(country.parent_id)) { continents.push(country.parent_id) }; };
if (!countries || countries.length===0) {
faddelem('p',document.body,{innerText:'No results returned.'});
return; // stop here if no countries are returned
};
let parents = await ffetch('https://api.inaturalist.org/v1/places/'+continents);
class SPARQLQueryDispatcher {
constructor( endpoint ) {
this.endpoint = endpoint;
}
query( sparqlQuery ) {
const fullUrl = this.endpoint + '?query=' + encodeURIComponent( sparqlQuery );
const headers = { 'Accept': 'application/sparql-results+json' };
return fetch( fullUrl, { headers } ).then( body => body.json() );
}
}
const endpointUrl = 'https://query.wikidata.org/sparql';
const sparqlQuery = `SELECT ?isoCode ?countryLabel ?country ?population ?area WHERE {
?country wdt:P297 ?isoCode .
OPTIONAL { ?country wdt:P1082 ?population } .
OPTIONAL { ?country wdt:P2046 ?area } .
# OPTIONAL { ?country p:P2046/psn:P2046/wikibase:quantityAmount ?area } .
SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
}
ORDER BY ?isoCode DESC(?population) ?area`;
const queryDispatcher = new SPARQLQueryDispatcher( endpointUrl );
let wd = await queryDispatcher.query( sparqlQuery );
// .then( console.log );
return [countries,parents,wd];
})
// create the the country list
// if any stats are specified in the parameters, then create the cells to hold the data, but don't populate the cells at this point.
.then(data=>{
if (!data) { return; }; // stop here if no data
let countries = data[0];
let continents = data[1].results;
let wd = data[2].results.bindings;
// sort the list alphabetically by name
countries.sort(function(a, b) { // sort by name
if(a.name < b.name) { return -1; }
if(a.name > b.name) { return 1; }
return 0;
});
// table and headers
let table = faddelem('table',document.body,{id:'main'});
let thead = faddelem('thead',table);
let hrow = faddelem('tr',thead);
let labels = [
{innerText:'#'},
{innerText:'ID'},
{innerText:'Alpha-2 Code'},
{innerText:'Flag'},
{innerText:'Name'},
// {innerText:'display_name'},
// {innerText:'slug'},
{innerText:'Check List ID'},
{innerText:'Create Date'},
{innerText:'Update Date'},
{innerText:'Parent Place'},
{classList:'tar',innerText:'Lat'},
{classList:'tar',innerText:'Long'},
{classList:'tar',innerText:'NE Lat'},
{classList:'tar',innerText:'NE Long'},
{classList:'tar',innerText:'SW Lat'},
{classList:'tar',innerText:'SW Long'},
// {innerText:'source_id'},
// {innerText:'source_identifier'},
// {innerText:'source_name'},
// {innerText:'user_id'},
// {innerText:'woeid'},
// {innerText:'uuid'},
{classList:'tar',innerText:'Wikidata ID'},
{classList:'tar',innerText:'Population'},
{classList:'tar',innerText:'Area (sq km)'},
];
if (show.observations) { labels.push({classList:'tar',innerText:'Observations'}); }
if (show.species) { labels.push({classList:'tar',innerText:'Species'}); }
if (show.identifiers) { labels.push({classList:'tar',innerText:'Identifiers'}); }
if (show.observers) { labels.push({classList:'tar',innerText:'Observers'}); }
faddelems('th',hrow,labels);
// table rows
let tbody = faddelem('tbody',table);
for (let i=0; i<countries.length; i++) {
let brow = faddelem('tr',tbody);
let rec = countries[i];
let wdinfo = fgetwdinfo(wd,rec.code);
let values = [
{innerText:i+1},
{innerHTML:furl('https://www.inaturalist.org/places/'+rec.id,rec.id)},
{innerHTML:(rec.code==null)?'':furl('https://www.iso.org/obp/ui/#iso:code:3166:'+rec.code,rec.code)},
{classList:'flag',innerText:rec.code?getFlagEmoji(rec.code):''},
{innerHTML:furl('https://www.inaturalist.org/observations?place_id='+rec.id,rec.name)},
// {innerText:rec.display_name},
// {innerText:rec.slug},
{innerHTML:rec.check_list_id?furl('https://www.inaturalist.org/check_lists/'+rec.check_list_id,rec.check_list_id):''},
{innerText:fdate(rec.created_at,true)},
{innerText:fdate(rec.updated_at,true)},
{innerHTML:rec.parent_id?furl('https://www.inaturalist.org/places/'+rec.parent_id,fgetname(continents,rec.parent_id)):''},
{classList:'tar',innerText:fround(rec.latitude,4)},
{classList:'tar',innerText:fround(rec.longitude,4)},
{classList:'tar',innerText:fround(rec.nelat,4)},
{classList:'tar',innerText:fround(rec.nelng,4)},
{classList:'tar',innerText:fround(rec.swlat,4)},
{classList:'tar',innerText:fround(rec.swlng,4)},
// {innerText:rec.source_id},
// {innerText:rec.source_identifier},
// {innerText:rec.source_name},
// {innerText:rec.user_id},
// {innerText:rec.woeid},
// {innerText:rec.uuid},
{classList:'tar',innerHTML:wdinfo.entity?furl(wdinfo.url,wdinfo.entity):null},
{classList:'tar',innerText:wdinfo.population?fcomnum(wdinfo.population):null},
{classList:'tar',innerText:wdinfo.area?fcomnum(fround(wdinfo.area,1)):null},
];
if (show.observations) { values.push({classList:'tar',id:'observations_'+rec.id}); }
if (show.species) { values.push({classList:'tar',id:'species_'+rec.id}); }
if (show.identifiers) { values.push({classList:'tar',id:'identifiers_'+rec.id}); }
if (show.observers) { values.push({classList:'tar',id:'observers_'+rec.id}); }
faddelems('td',brow,values);
};
return countries;
})
// if stats are specified then populate them
// recommended API request limits are 1 request per second. so this will wait 1 second between initiating each API request.
.then(async countries=>{
let datatoget = [];
if (show.observations) { datatoget.push({urlsuffix:'',tdid:'observations'}); };
if (show.species) { datatoget.push({urlsuffix:'/species_counts',tdid:'species'}); };
if (show.identifiers) { datatoget.push({urlsuffix:'/identifiers',tdid:'identifiers'}); };
if (show.observers) { datatoget.push({urlsuffix:'/observers',tdid:'observers'}); };
if (datatoget.length===0) { return; }
for (let i=0; i<countries.length; i++) {
for (let j=0; j<datatoget.length; j++) {
await delay(1000);
Promise.all([
Promise.resolve(countries[i].id),
Promise.resolve(datatoget[j].tdid),
ffetch('https://api.inaturalist.org/v1/observations'+datatoget[j].urlsuffix+'?'+winurlparams+'&place_id='+countries[i].id)
])
.then(results=>{ document.getElementById(results[1]+'_'+results[0]).innerText = (results[2]?fcomnum(results[2].total_results):'N/A'); });
};
};
});
</script>
</body>
</html>