-
Notifications
You must be signed in to change notification settings - Fork 6
/
iNat_places.html
261 lines (245 loc) · 13.1 KB
/
iNat_places.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
<!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 Place Details" />
<title>iNaturalist Place Details</title>
<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); }
</style>
<script src='https://unpkg.com/@turf/turf@6.3.0/turf.min.js'></script>
</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_place_id = winurlparams.get('place_id');
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');
show.density = stats.includes('density');
winurlparams.delete('stats');
winurlparams.delete('place_id'); // will be set to individual countries
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 furl(url,txt=url) { return '<a href="'+url+'">'+txt+'</a>'; };
function famp(str) { return str.replace(/&/g,'&'); };
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); }; };
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...')
});
};
// 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.
async function fgetstats(places) {
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<places.length; i++) {
for (let j=0; j<datatoget.length; j++) {
await delay(1000);
Promise.all([
Promise.resolve(places[i].id),
Promise.resolve(datatoget[j].tdid),
ffetch('https://api.inaturalist.org/v1/observations'+datatoget[j].urlsuffix+'?'+winurlparams+'&place_id='+places[i].id)
])
.then(results=>{
document.getElementById(results[1]+'_'+results[0]).innerText = (results[2]?fcomnum(results[2].total_results):'N/A');
if (show.density) {
document.getElementById(results[1]+'_density_'+results[0]).innerText = (results[2]?fcomnum(fround(results[2].total_results/document.getElementById('area_'+results[0]).innerText.replaceAll(',',''),2)):'N/A');
};
});
};
};
};
function fresults(xobj) {
//faddelem('p',document.body,{innerHTML:furl(famp(apiurl))});
let results = xobj.results;
if (results) {
let total_results = xobj.total_results;
faddelem('p',document.body,{innerHTML:'total records: '+fcomnum(total_results)});
// sort the list alphabetically by name
results.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:'Name'},
// {innerText:'display_name'},
// {innerText:'slug'},
// {innerText:'Ancestor Place IDs'},
{innerText:'Admin Level'},
{innerText:'Place Type'},
{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'},
{classList:'tar',innerText:'Area (sq km)'},
];
if (show.observations) { labels.push({classList:'tar',innerText:'Observations'}); }
if (show.observations && show.density) { labels.push({classList:'tar',innerText:'Observations / Area'}); }
if (show.species) { labels.push({classList:'tar',innerText:'Species'}); }
if (show.species && show.density) { labels.push({classList:'tar',innerText:'Species / Area'}); }
if (show.identifiers) { labels.push({classList:'tar',innerText:'Identifiers'}); }
if (show.identifiers && show.density) { labels.push({classList:'tar',innerText:'Identifiers / Area'}); }
if (show.observers) { labels.push({classList:'tar',innerText:'Observers'}); }
if (show.observers && show.density) { labels.push({classList:'tar',innerText:'Observers / Area'}); }
faddelems('th',hrow,labels);
// table rows
let tbody = faddelem('tbody',table);
for (let i=0; i<results.length; i++) {
let brow = faddelem('tr',tbody);
let rec = results[i];
let values = [
{innerText:i+1},
{innerHTML:furl('https://www.inaturalist.org/places/'+rec.id,rec.id)},
{innerHTML:furl('https://www.inaturalist.org/observations?place_id='+rec.id,rec.name)},
// {innerText:rec.display_name},
// {innerText:rec.slug},
// {innerText:rec.ancestor_place_ids?rec.ancestor_place_ids:''},
{innerText:rec.admin_level},
{innerText:rec.place_type},
{classList:'tar',innerText:fround(rec.location.split(',')[0],4)},
{classList:'tar',innerText:fround(rec.location.split(',')[1],4)},
{classList:'tar',innerText:fround(rec.bounding_box_geojson.coordinates[0][2][1],4)},
{classList:'tar',innerText:fround(rec.bounding_box_geojson.coordinates[0][2][0],4)},
{classList:'tar',innerText:fround(rec.bounding_box_geojson.coordinates[0][0][1],4)},
{classList:'tar',innerText:fround(rec.bounding_box_geojson.coordinates[0][0][0],4)},
{classList:'tar',id:'area_'+rec.id,innerText:fcomnum(fround(turf.area(rec.geometry_geojson)/1000000,2))},
];
if (show.observations) { values.push({classList:'tar',id:'observations_'+rec.id}); }
if (show.observations && show.density) { values.push({classList:'tar',id:'observations_density_'+rec.id}); }
if (show.species) { values.push({classList:'tar',id:'species_'+rec.id}); }
if (show.species && show.density) { values.push({classList:'tar',id:'species_density_'+rec.id}); }
if (show.identifiers) { values.push({classList:'tar',id:'identifiers_'+rec.id}); }
if (show.identifiers && show.density) { values.push({classList:'tar',id:'identifiers_density_'+rec.id}); }
if (show.observers) { values.push({classList:'tar',id:'observers_'+rec.id}); }
if (show.observers && show.density) { values.push({classList:'tar',id:'observers_density_'+rec.id}); }
faddelems('td',brow,values);
};
fgetstats(results);
}
else { faddelem('p',document.body,{innerText:'No results returned.'}); };
};
let apibase = 'https://api.inaturalist.org/v1/places/';
let apiurl = apibase+p_place_id;
let apirefurl = 'https://api.inaturalist.org/v1/docs/#!/Places/get_places_id';
let apirefname = 'iNaturalist Place Details';
let apiref = furl(apirefurl,apirefname);
let apirefstat = {};
apirefstat.base = 'https://api.inaturalist.org/v1/docs/#!/Observations/';
apirefstat.observations = apirefstat.base+'get_observations';
apirefstat.species = apirefstat.base+'get_observations_species_counts';
apirefstat.identifiers = apirefstat.base+'get_observations_identifiers';
apirefstat.observers = apirefstat.base+'get_observations_observers';
faddelem('h1',document.body,{innerText:apirefname});
if (!p_place_id) {
let instructions = [
{innerHTML:'This page gets iNaturalist place details via the '+apiref+' API endpoint. It also will calculate area (using Turf.js), based on the simplified place polygon returned by iNaturalist. It can also optionally return counts of observations, species, identifiers, and observers for each place, along with an associated density for each of these counts.'},
{innerHTML:'To use this page, filter for at least one place (either ID or slug) using the place_id parameter. To get the extra statistics, add a "stats" parameter to the URL, set it to a comma-separated list of values including "observations", "species", "identifiers", and/or "observers", and also add "density" if you want associated density. These extra stats can also be filtered in the URL using the parameters available in the '+furl(apirefstat.observations,'observations')+', '+furl(apirefstat.species,'species')+', '+furl(apirefstat.identifiers,'identifiers')+', and '+furl(apirefstat.observers,'observers')+' API endpoints.'},
{innerHTML:'For example, to get details for San Francisco, Hong Kong, and DC, along with counts of verifiable observations and density, open '+furl(winurlexsearchstr+'?place_id=san-francisco-county,district-of-columbia-us,hong-kong&stats=observations,density&verifiable=true')+' in your browser.'},
{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.'},
];
faddelems('p',document.body,instructions);
}
else {
faddelem('p',document.body,{innerHTML:'This is the base query: '+furl(famp(apiurl))+'.'});
fetch(apiurl)
.then((response) => {
if (!response.ok) { throw new Error(response.status+' ('+response.statusText+') returned from '+response.url); };
return response.json();
})
.then((data) => { fresults(data); })
.catch((err) => {
console.error(err.message);
faddelem('p',document.body,{innerText:'There was a problem retrieving data. Error '+err.message+'.'});
});
};
</script>
</body>
</html>