-
Notifications
You must be signed in to change notification settings - Fork 6
/
iNat_UTFgrid_based_custom_density_map.html
681 lines (642 loc) · 46.6 KB
/
iNat_UTFgrid_based_custom_density_map.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
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="description" content="Custom iNaturalist Observation Density Maps" />
<title>Custom iNaturalist Observation Density Maps</title>
<style>
body { font:10pt Sans-Serif; margin:0px; padding:0px; border:0px; background:darkgray; }
#nav { width:384px; height:1024px; position:absolute; top:0px; left:0px; }
h3 { font:600 11pt Sans-Serif; margin:0px; padding:15px 10px 0px 10px; }
p { font:10pt Sans-Serif; margin:0px; padding:10px 10px 0px 10px; }
#examplelist { width:364px; margin:10px 10px 0px 10px; }
#keytable { display:table; margin:0px 10px; }
#keybody { display:table-row-group; }
.keyentry { width:384px; display:table-row; }
.keyvis { width:4px; display:table-cell; }
.keyicon { width:4px; height:4px; display:table-cell; }
.keytext { width:350px top:4px; display:table-cell; padding-top:10px; padding-left:5px; }
.keyvis input { border-style:solid; border:10px; border-color:blue; background-color:blue; }
#map { width:1024x; height:1024px; position:absolute; top:0px; left:384px; }
.layer { width:1024x; height:1024px; position:absolute; opacity:0.9999999; }
.tile { width:256px; height:256px; position:absolute; background-size:contain; }
.cell { width:4px; height:4px; position:absolute; }
a { text-decoration:none; color:darkblue; }
</style>
</head>
<body>
<div id='nav'>
<h3>Examples of Custom iNaturalist Observation Density Maps Based on UTFGrids</h3>
<p>Note: This was originally created before the introduction of iNaturalist's Grid style tiles, which are conceptually the same kind of visualization as provided here. Unless you have a need for customization not available with the Grid tiles, it will be better to use those tiles in most cases.</p>
<p>iNaturalist provides <a href="https://api.inaturalist.org/v1/docs/#/Observation_Tiles">3 built-in types</a> of observation maps: point maps, circle heatmaps, and regular heatmaps. While useful, these maps have limited formatting options. It's possible to create custom maps using downloaded observation data, but that approach can be resource-intensive, especially for large datasets.</p>
<p>But there's an alternative. iNaturalist provides <a href="https://api.inaturalist.org/v1/docs/#/UTFGrid">UTFgrids</a> which usually are paired with its map tiles to enhance user interactions. But these UTFgrids also contain observation counts per unique UTFgrid cell (aka cellcount) which can be used to produce ad hoc observation density maps.</p>
<p>This page provides examples of such maps, representing only the tip of the iceberg of possibilities. Select examples from the list below or by specifying an example in the URL.</p>
<p>For simplicity, the example maps are static (no zoom / pan). The maps seem to load fine in Chrome and Firefox, but the more complex ones may overwhelm browsers like Edge. Finally, these UTFgrid data may not be precise enough in certain ways for use cases requiring lots of precision.</p>
<select id='examplelist' onchange='fgetexample(this.value);'>
<option selected='true' value='nosel' disabled='disabled'>Select an Example:</option>
</select>
<h3>Example Map Details</h3>
</div>
<script>
let map = faddelem('div',document.body,null,'map');
let nav = document.getElementById('nav');
let mddescr = faddelem('div',nav,null,'mddescr');
let keytable = faddelem('div',nav,null,'keytable');
let keybody = faddelem('div',keytable,null,'keybody');
var pcc = faddelem('p',nav);
var pcc1 = faddelem('span',pcc,null,null,'Total Cell Count: ');
var pcc2 = faddelem('span',pcc);
var cellcount = 0;
//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));
var example = winurlparams.get('example')
example = (example===null?1:example.split(',')[0]); // gets first value passed in the url "example" parameter, or else default to 1
//function to create new elements
function faddelem(etype,eparent=null,eclass=null,eid=null,ehtml=null) {
var eobj = document.createElement(etype);
if (eclass!==null) { eobj.classList = eclass };
if (eid!==null) { eobj.id = eid };
if (ehtml!==null) { eobj.innerHTML = ehtml };
if (eparent!==null) { eparent.appendChild(eobj) };
return eobj;
};
//function to get a JSON file
function futfgrid(url) {
return fetch(url)
.then((response) => {
if (!response.ok) { throw new Error(response.status+': '+response.statusText); };
return response.json();
})
// .then((data) => { })
.catch((err) => { console.error(err); });
};
//function to return the correct x, y, and zoom values to a given map tile or UTFgrid url
function freplacexyz(url,x,y,z) {
url = url.replace('{x}',x);
url = url.replace('{y}',y);
url = url.replace('{z}',z);
return url;
};
//function to add ids to layer, tile, and cell divs
//this does not have to be used, but it helps to identify specific cells and may be useful for future development
function fltcid(lseq,tx=null,ty=null,cx=null,cy=null) { return 'l'+lseq+((tx!==null&&ty!==null)?('tx'+tx+'ty'+ty):'')+((cx!==null&&cy!==null)?('cx'+cx+'cy'+cy):''); };
//start to set some basic variables to create layers, tiles, and cells
//one or more layer divs will be placed on the 1024px x 1024px map div. each layer div will be the same size as the map div. multiple layer divs will be stacked.
//each layer div will contain 16 256px x 256px tiles (in 4 columns and 4 rows).
//each tile may contain up to 4096 4px x 4px cells (in 64 columns and 64 rows).
//because maps at zoom 1 will contain only 4 tiles, we will set the default (lowest) zoom level 2 for simplicity.
var zoom = 2;
var txmax = Math.pow(2,zoom)-1;
var tymax = Math.pow(2,zoom)-1;
var txnstart = 0;
var tynstart = 0;
let txnum = tynum = 4;
let txpx = typx = 256;
let cxnum = cynum = 64;
let cxpx = cypx = 4;
//need to do some geometry to figure out the vertical position of a point at given lattitude on a Mercator projection
//this is not necessary for longitudes
function latidx(lat) { return (1-Math.log(Math.tan(lat*Math.PI/180)+1/Math.cos(lat*Math.PI/180))/Math.PI)/2; };
//function to identify the right tiles to pull, given a bounding box defined by a SW point and NE point in GPS coordinates
//this is only somewhat tested, but it seems to help pull in the right tiles (most of the time?).
function fmapextent(bbox={swlat:-90,swlng:0,nelat:89.999999,nelng:-0.00001}) {
var w = bbox.swlng;
var e = bbox.nelng;
var n = bbox.nelat;
var s = bbox.swlat;
// get the height and width of the bounding box as proportions (0.0 to 1.0) of the height and width of the total world map, respectively
dw = ((e>=w)?(e-w):(360-w+e))/360;
dh = latidx(s)-latidx(n);
// estimate the maximum zoom level needed to get the entire bounding box in the map, with min zoom 2 and max zoom 20
// once zoom is determined, calculate tile x and y ranges for that zoom level
zoom = (dw<=0 && dh<=0) ? 20
: (dw<dh) ? Math.round( Math.log((tynum+1)/dh) / Math.log(2) + 0.25 ) - 1
: Math.round( Math.log((txnum+1)/dw) / Math.log(2) + 0.25 ) - 1;
zoom = (zoom>20)?20:(zoom<2)?2:zoom;
txmax = Math.pow(2,zoom)-1;
tymax = Math.pow(2,zoom)-1;
// at this zoom level, determine the number of tiles high and wide are needed to contain the bounding box
var txw = Math.floor((180+(w<180?w:-180))/360*(txmax+1))%(txmax+1);
var txe = Math.floor((180+(e<180?e:-180))/360*(txmax+1))%(txmax+1);
var tyn = Math.floor(latidx(n)*(tymax+1));
var tys = Math.floor(latidx(s)*(tymax+1));
tyn = (tyn>tymax)?tymax:tyn;
tys = (tys>tymax)?tymax:tys;
var tw = ((e>=w)?(txe-txw):(txmax+1-txw+txe))+1;
var th = tys-tyn+1;
// determine the top-left corner tile for this tileset.
// this will try to center the bounding box tiles in the tileset as much as possible, unless:
// 1. it makes more sense to display the whole world map
// 2. or the bounding box is near the N or S poles
txnstart = (tw===(txmax+1))?0:(tw===txnum)?txw:txw-Math.floor((txnum/2-tw/2));
txnstart = (txnstart>=0)?txnstart:txmax+txnstart+1;
tynstart = (th===(tymax+1))?0:(th===tynum)?tyn:tyn-Math.floor((tynum/2-th/2));
tynstart = (tynstart<0)?0:(tynstart+tynum>=tymax)?(tymax-tynum+1):tynstart;
};
//this defines some different styling options for cells
//the styling will help these cells look like markers on a map
function fstylecell(cell,cxy=[0,0],cvalue=1,marker={type:'square_full',scaleType:'opacity',scaleFactor:1,colorRGB:[255,0,0]}) {
//examples of markers
//note the attributes for markers of different scaleTypes is different
//marker={type:'square_full',scaleType:'opacity',scaleFactor:1,colorRGB:[255,0,0]} // scaled using opacity
//marker={type:'circle_med',scaleType:'grad_rainbow',scaleFactor:5,opacity:0.7}; // scaled using a color gradient from blue to magenta
var ccolor = '#000'; // default to black;
scaleFactor = marker.scaleFactor||1;
if (marker.scaleType==='grad_rainbow') {
var opacity = marker.opacity||0.7;
var f = cvalue/scaleFactor;
f = (f<0)?0:(f>1)?1:f;
ccolor = "hsla("+(240+f*-300)+","+(f*70+30)+"%,50%,"+opacity+")"; //hsla format
}
else { // default = marker.scaleType==='opacity'
var opacity = cvalue/scaleFactor;
ccolor = 'rgba('+marker.colorRGB+','+opacity+')'; //rgba format
};
var cstyle = marker.type||'square_full';
//var cxyspx = [4,4];
var cxyspx = [8,8];
var cxyopx = [0,0];
if (cstyle==='square') {
//cxyspx = [2,2];
//cxyopx = [1,1];
cxyspx = [4,4];
cxyopx = [2,2];
}
else if (cstyle==='square_med') {
//cxyspx = [3,3];
cxyspx = [6,6];
cxyopx = [1,1];
}
else if (cstyle==='square_full') {
//cxyspx = [4,4];
cxyspx = [8,8];
}
else if (cstyle==='square_overlap') {
//cxyspx = [6,6];
//cxyopx = [-1,-1];
cxyspx = [12,12];
cxyopx = [-2,-2];
}
else if (cstyle==='square_tl') {
//cxyspx = [2,2];
cxyspx = [4,4];
cxyopx = [0,0];
}
else if (cstyle==='square_tr') {
//cxyspx = [2,2];
//cxyopx = [2,0];
cxyspx = [4,4];
cxyopx = [4,0];
}
else if (cstyle==='square_bl') {
//cxyspx = [2,2];
//cxyopx = [0,2];
cxyspx = [4,4];
cxyopx = [0,4];
}
else if (cstyle==='square_br') {
//cxyspx = [2,2];
//cxyopx = [2,2];
cxyspx = [4,4];
cxyopx = [4,4];
}
else if (cstyle==='square_sm_tl') {
cxyspx = [3,3];
cxyopx = [1,1];
}
else if (cstyle==='square_sm_tr') {
cxyspx = [3,3];
cxyopx = [4,1];
}
else if (cstyle==='square_sm_bl') {
cxyspx = [3,3];
cxyopx = [1,4];
}
else if (cstyle==='square_sm_br') {
cxyspx = [3,3];
cxyopx = [4,4];
}
else if (cstyle==='pixel') {
//cxyspx = [1,1];
//cxyopx = [1,1];
cxyspx = [2,2];
cxyopx = [3,3];
}
else if (cstyle==='pixel_tl') {
//cxyspx = [1,1];
//cxyopx = [1,1];
cxyspx = [2,2];
cxyopx = [2,2];
}
else if (cstyle==='pixel_tr') {
//cxyspx = [1,1];
//cxyopx = [3,1];
cxyspx = [2,2];
cxyopx = [4,2];
}
else if (cstyle==='pixel_bl') {
//cxyspx = [1,1];
//cxyopx = [1,3];
cxyspx = [2,2];
cxyopx = [2,4];
}
else if (cstyle==='pixel_br') {
//cxyspx = [1,1];
//cxyopx = [3,3];
cxyspx = [2,2];
cxyopx = [4,4];
}
else if (cstyle==='diamond') {
//cxyspx = [2,2];
cxyspx = [4,4];
cell.style.transform = "rotate(45deg)";
}
else if (cstyle==='diamond_med') {
//cxyspx = [2.5,2.5];
cxyspx = [6,6];
cell.style.transform = "rotate(45deg)";
}
else if (cstyle==='diamond_overlap') {
//cxyspx = [4,4];
cxyspx = [8,8];
cell.style.transform = "rotate(45deg)";
}
else if (cstyle==='circle') {
//cxyspx = [2,2];
//cxyopx = [1,1];
//cell.style.borderRadius = '1px';
cxyspx = [4,4];
cxyopx = [2,2];
cell.style.borderRadius = '2px';
}
else if (cstyle==='circle_med') {
//cxyspx = [3,3];
//cell.style.borderRadius = '1.5px';
cxyspx = [6,6];
cxyopx = [1,1];
cell.style.borderRadius = '3px';
}
else if (cstyle==='circle_overlap') {
//cxyspx = [6,6];
//cxyopx = [-1,-1];
//cell.style.borderRadius = '3px';
cxyspx = [12,12];
cxyopx = [-2,-2];
cell.style.borderRadius = '6px';
}
else if (cstyle==='ring') {
//cxyspx = [2,2];
//cell.style.borderRadius = '2px';
//cell.style.borderWidth = '1px';
cxyspx = [4,4];
cell.style.borderRadius = '4px';
cell.style.borderWidth = '2px';
cell.style.borderColor = ccolor;
cell.style.borderStyle = 'solid';
}
else if (cstyle==='ring_square') {
//cxyspx = [2,2];
//cell.style.borderWidth = '1px';
cxyspx = [4,4];
cell.style.borderWidth = '2px';
cell.style.borderColor = ccolor;
cell.style.borderStyle = 'solid';
}
else if (cstyle==='ring_overlap') {
//cxyspx = [2,2];
//cxyopx = [-1,-1];
//cell.style.borderRadius = '3px';
//cell.style.borderWidth = '2px';
cxyspx = [4,4];
cxyopx = [-2,-2];
cell.style.borderRadius = '6px';
cell.style.borderWidth = '4px';
cell.style.borderColor = ccolor;
cell.style.borderStyle = 'solid';
}
else if (cstyle==='circle_big_blur') {
//cxyspx = [6,6];
//cxyopx = [-1,-1];
//cell.style.borderRadius = '3px';
//cell.style.filter = "blur(4px)";
cxyspx = [12,12];
cxyopx = [-2,-2];
cell.style.borderRadius = '6px';
cell.style.filter = "blur(8px)";
};
cell.style.left=(cxpx*(cxy?cxy[0]:0)+cxyopx[0])+'px';
cell.style.top=(cypx*(cxy?cxy[1]:0)+cxyopx[1])+'px';
cell.style.width=cxyspx[0]+'px';
cell.style.height=cxyspx[1]+'px';
cell.style.background=(['ring','ring_overlap','ring_square'].includes(cstyle))?'none':ccolor;
return cell;
};
function fsetvis(layer,visible) { layer.style.visibility = visible?'visible':'hidden'; }
function ftogglelayervis(checkbox) {
var layer = document.getElementById(checkbox.value);
fsetvis(layer,checkbox.checked);
};
//this is the main function that creates tiles and cells and puts them into the document
//note that this code was originally written to assume a 64x64 UTFgrid.
//although the UTFgrid is still 64x64, the associated "grid tile" is actually only 32x32.
//theoretically, a 2x2 set of cells from the UTFgrid should correspond to a single cell from the "grid tile"; however, that is not actually the case (see https://forum.inaturalist.org/t/open-test-of-map-tile-improvements/7833/88).
//so this code attempts to mimic a 32x32 "grid tile" by using the bottom-right cell from each 2x2 set of UTFgrid cells.
function fcreatelayer(lseq,ltype,lurl,marker=null) {
var layer = faddelem('div',map,'layer',fltcid(lseq));
layer.style.left='0px';
layer.style.top='0px';
for (ty=0;ty<tynum;ty++) {
var tya = tynstart+ty;
for (tx=0;tx<txnum;tx++) {
var txa = txnstart+tx;
txa = (txa>txmax)?txa-txmax-1:txa;
var tile = faddelem('div',layer,'tile',fltcid(lseq,txa,tya));
tile.style.left=(txpx*tx)+'px';
tile.style.top=(typx*ty)+'px';
if (['basemap','rtoverlay'].includes(ltype)) {
tile.style.backgroundImage = "url('"+freplacexyz(lurl,txa,tya,zoom)+"')";
}
else if (ltype==='utfoverlay') {
var prom0 = Promise.resolve([tile,tx,ty]);
var prom1 = futfgrid(freplacexyz(lurl,txa,tya,zoom));
Promise.all([prom0,prom1])
.then(data => {
var tile = data[0][0];
var tx = data[0][1];
var ty = data[0][2];
var utfgrid = data[1];
//for (cy=0;cy<=(cynum-1);cy++) {
for (cy=0;cy<cynum/2;cy++) {
//for (cx=0;cx<=(cxnum-1);cx++) {
for (cx=0;cx<cxnum/2;cx++) {
//for details about decoding the UTFgrid, see https://github.com/mapbox/utfgrid-spec/blob/master/1.2/utfgrid.md
//var i = utfgrid.grid[cy].charCodeAt(cx);
var i = utfgrid.grid[(cy*2+1)].charCodeAt(cx*2+1);
i = i-((i>=93)?34:(i>=35)?33:32);
var d = utfgrid.data[utfgrid.keys[i]];
if (d!=null) {
cellcount++;
pcc2.innerHTML = cellcount;
//Note that I chose to use divs to create the markers in this example because it seems to be the most flexible way to create the markers.
//Specifically, a cell from one tile can overlap another tile. Also, I could (in the future) allow the user to click on the div to trigger an action.
//However, the downside of the divs is that they are resource hungry, and maps with lots of these divs with complicated styles will easily overwhelm browsers like Edge.
//I have another page that has an example of UTFgrid-based density maps which uses canvas. That example integrates with Leaflet.js.
//var cell = faddelem('div',tile,'cell',fltcid(lseq,tx,ty,cx,cy));
var cell = faddelem('div',tile,'cell',fltcid(lseq,tx,ty,cx*2,cy*2));
//fstylecell(cell,[cx,cy],d.cellCount,marker);
fstylecell(cell,[cx*2,cy*2],d.cellCount,marker);
};
};
};
});
};
};
};
return layer;
};
//function to create basemap layers
//it includes some functionality to apply filter styling on the basemap layers, which will allow for some color transformations on the basemaps. (note that filter stlying is applied in the order that it is specified. also note that there is a opacity:0.99999 value on the css .layer class styling which is necessary for some reason on Edge to display the filter styling correctly in some cases.)
//it also creates a key entry on in the map details section.
//this includes a bgcolor parameter this is handled but not used by any of the examples.
function fcreatebaselayer(lseq,lurl,ldescr=null,lattr=null,visible=true,stylefilter=null,bgcolor=null) {
var layer = fcreatelayer(lseq,'basemap',lurl);
if (stylefilter!==null) { layer.style.filter = stylefilter; };
if (bgcolor!==null) { layer.style.bgcolor = bgcolor; };
var entry = faddelem('div',keybody,'keyentry');
var vis = faddelem('div',entry,'keyvis',null,'<input type="checkbox" value="'+layer.id+'"'+(visible?' checked':'')+' onchange="ftogglelayervis(this);"/>');
var icon = faddelem('div',entry,'keyicon',null,'🗺️');
var text = faddelem('div',entry,'keytext',null,'Base Map: '+ldescr+(stylefilter?(' → '+stylefilter.replace(/ /g,' → ')):'')+'<br />Attribution: '+lattr);
fsetvis(layer,visible);
return layer;
};
//function to create raster tile layer
//basically the same as function to create basemap layer, with a few minor changes
function fcreatertlayer(lseq,lurl,ldescr=null,lattr=null,visible=true,stylefilter=null,bgcolor=null) {
var layer = fcreatelayer(lseq,'rtoverlay',lurl);
if (stylefilter!==null) { layer.style.filter = stylefilter; };
if (bgcolor!==null) { layer.style.bgcolor = bgcolor; };
var entry = faddelem('div',keybody,'keyentry');
var vis = faddelem('div',entry,'keyvis',null,'<input type="checkbox" value="'+layer.id+'"'+(visible?' checked':'')+' onchange="ftogglelayervis(this);"/>');
var icon = faddelem('div',entry,'keyicon',null,'🌐');
var text = faddelem('div',entry,'keytext',null,'Layer '+lseq+': '+ldescr+(stylefilter?(' → '+stylefilter.replace(/ /g,' → ')):'')+'<br />Attribution: '+lattr);
fsetvis(layer,visible);
return layer;
};
//function to create UTFGrid-based density map overlay layers
//it also creates a key entry on in the map details section. the icon in the key will match the styling of the markers (cells) used in the layer at 100% opacity.
function fcreateutflayer(lseq,lurl,ldescr,lattr,visible=true,marker={type:'square_full',scaleType:'opacity',scaleFactor:1,colorRGB:[0,255,0]}) {
var layer = fcreatelayer(lseq,'utfoverlay',lurl,marker);
var entry = faddelem('div',keybody,'keyentry');
var vis = faddelem('div',entry,'keyvis',null,'<input type="checkbox" value="'+layer.id+'"'+(visible?' checked':'')+' onchange="ftogglelayervis(this);"/>');
var icon = faddelem('div',entry,'keyicon');
var text = faddelem('div',entry,'keytext',null,'Layer '+lseq+': '+ldescr+'<br />Attribution: '+lattr+'<br />Density Factor: '+((marker)?(marker.scaleFactor||1):1));
var cell = faddelem('div',icon,'cell');
cell.style.position = 'relative';
fstylecell(cell,[1.5,-1],(marker?(marker.scaleFactor||1):1),marker);
fsetvis(layer,visible);
return layer;
};
//function to create point layers (untiled)
//it also creates a key entry on in the map details section. the icon in the key will match the styling of the markers (cells) used in the layer at 100% opacity.
function fcreateptlayer(lseq,lpoints,ldescr=null,lattr=null,visible=true,marker={type:'diamond_overlap',scaleType:'opacity',scaleFactor:1,colorRGB:[255,0,0]}) {
var layer = faddelem('div',map,'layer',fltcid(lseq));
layer.style.left='0px';
layer.style.top='0px';
for (p=0;p<lpoints.length;p++) {
var mapx = (lpoints[p].lng+180)/360*(txmax+1);
var mapy = latidx(lpoints[p].lat)*(tymax+1);
var tilex = Math.trunc(mapx) % (txmax+1);
var tiley = Math.trunc(mapy) % (tymax+1);
for (tsx=0;tsx<txnum;tsx++) {
var tilesetx = (txnstart+tsx) % (txmax+1);
if (tilex===tilesetx) {
for (tsy=0;tsy<tynum;tsy++) {
var tilesety = (tynstart+tsy) % (tymax+1);
if (tiley===tilesety) {
var mappt = faddelem('div',layer,'cell');
fstylecell(mappt,[(((tsx+(mapx%1))*txpx/cxpx)-0.5),(((tsy+(mapy%1))*typx/cypx)-0.5)],1,marker);
};
};
};
};
};
var entry = faddelem('div',keybody,'keyentry');
var vis = faddelem('div',entry,'keyvis',null,'<input type="checkbox" value="'+layer.id+'"'+(visible?' checked':'')+' onchange="ftogglelayervis(this);"/>');
var icon = faddelem('div',entry,'keyicon');
var text = faddelem('div',entry,'keytext',null,'Layer '+lseq+': '+(ldescr||((lpoints.length!=1)?'Untitled':(lpoints[0].descr||'Untitled')))+((lattr!=null)?('<br />Attribution: '+lattr):''));
var cell = faddelem('div',icon,'cell');
cell.style.position = 'relative';
fstylecell(cell,[1.5,-1],(marker?(marker.scaleFactor||1):1),marker);
fsetvis(layer,visible);
return layer;
};
//some basemaps
//note that Stamen Toner is black and white, and Toner Lite is grayscale. in their default forms, they have limited usefulness, but because of b/w/grayscale, they are easily customized via style filters.
let s_stamen_copyright = 'Map tiles by <a href="https://stamen.com">Stamen Design</a>, under <a href="https://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="https://openstreetmap.org">OpenStreetMap</a>, under <a href="https://www.openstreetmap.org/copyright">ODbL</a>.'; // used for all sets except Watercolor
let bm_s_toner = {url:'https://stamen-tiles.a.ssl.fastly.net/toner/{z}/{x}/{y}.png',description:'Stamen Toner',attribution:s_stamen_copyright};
let bm_s_toner_lite = {url:'https://stamen-tiles.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png',description:'Stamen Toner Lite',attribution:s_stamen_copyright};
let bm_s_terrain = {url:'https://stamen-tiles.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png',description:'Stamen Terrain',attribution:s_stamen_copyright};
//the main iNat UTFGrid API
// note that iNat provides 4 UTFGrid endpoints. the grid and heatmap endpoints are interchangeable and the ones to use for this application.
// this was originally written when the heatmap utfgrid was like the colored_heatmap utfgrid. although it is still possible to use the colored_heatmap utfgrid, the results provided by that utfgrid are less consistently precise.
let utfgridapi = {url:'https://api.inaturalist.org/v1/heatmap/{z}/{x}/{y}.grid.json',attr:'<a href="https://api.inaturalist.org/v1/docs/#!/UTFGrid/get_heatmap_zoom_x_y_grid_json">iNaturalist</a>'};
//various tile layers
let inat_points = {url:'https://api.inaturalist.org/v1/points/{z}/{x}/{y}.png',description:'iNaturalist Observations (Points)',attribution:'<a href="https://api.inaturalist.org/v1/docs/#!/Observation_Tiles/get_points_zoom_x_y_png">iNaturalist</a>'};
let inat_circles = {url:'https://api.inaturalist.org/v1/colored_heatmap/{z}/{x}/{y}.png',description:'iNaturalist Observations (Density Circles)',attribution:'<a href="https://api.inaturalist.org/v1/docs/#!/Observation_Tiles/get_colored_heatmap_zoom_x_y_png">iNaturalist</a>'};
let inat_heat = {url:'https://api.inaturalist.org/v1/heatmap/{z}/{x}/{y}.png',description:'iNaturalist Observations (Heatmap)',attribution:'<a href="https://api.inaturalist.org/v1/docs/#!/Observation_Tiles/get_heatmap_zoom_x_y_png">iNaturalist</a>'};
let inat_grid = {url:'https://api.inaturalist.org/v1/grid/{z}/{x}/{y}.png',description:'iNaturalist Observations (Grid)',attribution:'<a href="https://api.inaturalist.org/v1/docs/#!/Observation_Tiles/get_grid_zoom_x_y_png">iNaturalist</a>'};
let gbif_density_point_py = {url:'https://api.gbif.org/v2/map/occurrence/density/{z}/{x}/{y}@1x.png?srs=EPSG:3857&style=purpleYellow.point&publishingOrg=28eb1a3f-1c15-4a95-931a-4af90ecb574d',description:'iNaturalist Observations in GBIF',attribution:'<a href="https://www.gbif.org/developer/maps">GBIF</a>'};
/*
//examples of how to set map extent based on a bounding box:
//(I saved them here in for easy copying and pasting for use in future maps.)
//note that bounding box coordinates can be easily obtained by using the iNaturalist Explore page in map view, setting the desired map view, and clicking the "Redo Search in Map" button, and then getting the coordinates from the updated URL. it's also possible to get bounding box coordinates in various ways using the iNaturalist API.
fmapextent({swlat:5.504044778645039,swlng:166.60386750474572,nelat:83.53583411313593,nelng:-16.36688232421875}); // North America
fmapextent({swlat:18.803882491774857,swlng:172.7116527222097,nelat:71.39943192712963,nelng:-66.80360281839967}); // US (including AK and HI)
fmapextent({swlat:13.290435858313753,swlng:-125.83712134009516,nelat:52.407256269196154,nelng:-63.551673623382214}); // Continental US + Mexico
fmapextent({swlat:25.800311593338847,swlng:-106.78545135073364,nelat:36.567753315903246,nelng:-93.43606950715184}); // Texas
fmapextent({swlat:28.779301713220775,swlng:-96.61523133516312,nelat:30.609263330698013,nelng:-94.20131103135645}); // Greater Houston
fmapextent({swlat:29.75417166016996,swlng:-95.45843155123293,nelat:29.7783586056903,nelng:-95.42147004045546}); // Memorial Park (Houston, TX)
fmapextent({swlat:29.24411828132679,swlng:-12.73389295962346,nelat:61.69814729146579,nelng:49.55155475708949}); // Partial Europe, Central Asia, Middle East, and North Africa
fmapextent({swlat:-48.287651245912166,swlng:109.47315585603962,nelat:9.033917886632137,nelng:179.99}); // Australia & New Zealand
fmapextent({swlat:-5.663220447316494,swlng:-141.97235941886902,nelat:66.91145404319461,nelng:-48.80829691886902}); // Partial North America
fmapextent({swlat:-5.316705319907759,swlng:-128.29067492857575,nelat:67.04758656179122,nelng:-3.719779495149851}); // Partial North America + Partial Europe
fmapextent({swlat:3.0309559101643595,swlng:67.44079047157487,nelat:45.61413812981794,nelng:150.64420693828788}); // East Asia, Southeast Asia, and South Asia
fmapextent({swlat:-36.24407547872301,swlng:-19.965530715489194,nelat:38.65127535425379,nelng:54.68348971793671}); // Africa
fmapextent({swlat:42.669869630725884,swlng:-5.488857263954401,nelat:50.91996456792671,nelng:10.082504665223837}); // France
fmapextent({swlat:21.82346690013718,swlng:119.31561939040277,nelat:25.357987038672206,nelng:122.10253345290277}); // Taiwan
fmapextent({swlat:-54.86843200395375,swlng:158.6226083792327,nelat:-54.43143711703443,nelng:159.1334726370452}); // Macquarie Island (small island south of Australia between Aus and NZ)
fmapextent({swlat:-68.61560619019023,swlng:-84.11365377733978,nelat:-42.599747509562214,nelng:-51.4183412773397}); // S tip of S America + tip of Antarctica
fmapextent({swlat:29.213602706283854,swlng:29.435832732502376,nelat:34.34711534926469,nelng:37.23612570125238}); // Cairo to Damascus
fmapextent({swlat:-21.536290595226412,swlng:176.38660654698475,nelat:-13.0437465364643,nelng:-168.79308095301525}); // Fiji, Tonga, Samoa (crosses 180 deg long)
*/
//function to add a description and notes to the map details section
function fmapdescr(descr,notes=null) {
faddelem('p',mddescr,null,null,descr);
if (notes) { faddelem('p',mddescr,null,null,'Notes: '+notes); };
};
var elist = document.getElementById('examplelist');
function fexample(eseq,descr,notes=null) {
var option = faddelem('option',elist,null,null,'Example '+eseq+': '+descr);
option.value = eseq;
var displaymap = (example==eseq);
if (displaymap) {
fmapdescr(descr,notes);
elist.value = eseq;
};
return displaymap;
};
function fgetexample(seq) {
if (seq!=null) { window.location.assign(winurlexsearchstr+'?example='+seq); };
};
//this is where the set of example maps are defined. each example is provided a unique example ID, which allows the example map to be called by adding "?example=[id]" to the page URL.
//each definition can include:
// a map description and notes
// a map extent. (if not included, the map will default to the whole world)
// a base map definition. (technically you can define many basemap layers, but these examples each only have one.)
// one or more overlay layers
if (fexample(1,'All Observations Worldwide','A basic map with green markers on a dark basemap. The opacity of the markers is scaled for a density factor. In this example, the density factor of Layer 1 is 100,000, which means that the cell that a given marker represents should have at least 100,000 observations in it if the marker is 100% opaque. Cells with fewer observations will be proportionately less opaque. The stock iNaturalist maps and a density map of iNaturalist observations from GBIF are included as layer options for comparison. (Click on the checkboxes below to toggle layers on/off.) This map also has a red point that marks iNaturalist HQ just to show that points can also be mapped.')) {
fcreatebaselayer(0,bm_s_toner.url,bm_s_toner.description,bm_s_toner.attribution,true,'brightness(25%)');
fcreatertlayer(2,inat_points.url,inat_points.description,inat_points.attribution,false);
fcreatertlayer(3,inat_circles.url,inat_circles.description,inat_circles.attribution,false);
fcreatertlayer(4,inat_heat.url,inat_heat.description,inat_heat.attribution,false);
fcreatertlayer(5,inat_grid.url,inat_grid.description,inat_grid.attribution,false);
fcreatertlayer(6,gbif_density_point_py.url,gbif_density_point_py.description,gbif_density_point_py.attribution,false);
fcreateutflayer(1,utfgridapi.url,'All Observations',utfgridapi.attr,true,{type:'circle_med',scaleType:'opacity',scaleFactor:100000,colorRGB:[0,255,0]});
var lpoints = [
{lat:37.769772,lng:-122.466126,descr:'California Academy of Sciences (iNaturalist HQ)'},
// {lat:0,lng:0,descr:'center'},
];
fcreateptlayer(7,lpoints,null,null,true,{type:'diamond_overlap',scaleType:'opacity',scaleFactor:1,colorRGB:[255,0,0]});
};
if (fexample(2,'Worldwide Observations by Year (2016-2019)','This shows observations for 4 different years using markers of 4 different colors on a dark basemap. The markers for the different years appear in different corners of a given cell so that all 4 years can be visually represented together without overlapping.')) {
fcreatebaselayer(0,bm_s_toner_lite.url,bm_s_toner_lite.description,bm_s_toner_lite.attribution,true,'brightness(25%) contrast(125%)');
// fcreateutflayer(1,utfgridapi.url+'?year=2019','Observations in 2019',utfgridapi.attr,true,{type:'square_tl',scaleType:'opacity',scaleFactor:20000,colorRGB:[255,0,0]});
// fcreateutflayer(2,utfgridapi.url+'?year=2018','Observations in 2018',utfgridapi.attr,true,{type:'square_tr',scaleType:'opacity',scaleFactor:20000,colorRGB:[255,255,0]});
// fcreateutflayer(3,utfgridapi.url+'?year=2017','Observations in 2017',utfgridapi.attr,true,{type:'square_bl',scaleType:'opacity',scaleFactor:20000,colorRGB:[0,255,0]});
// fcreateutflayer(4,utfgridapi.url+'?year=2016','Observations in 2016',utfgridapi.attr,true,{type:'square_br',scaleType:'opacity',scaleFactor:20000,colorRGB:[0,0,255]});
fcreateutflayer(1,utfgridapi.url+'?year=2019','Observations in 2019',utfgridapi.attr,true,{type:'square_sm_tl',scaleType:'opacity',scaleFactor:20000,colorRGB:[255,0,0]});
fcreateutflayer(2,utfgridapi.url+'?year=2018','Observations in 2018',utfgridapi.attr,true,{type:'square_sm_tr',scaleType:'opacity',scaleFactor:20000,colorRGB:[255,255,0]});
fcreateutflayer(3,utfgridapi.url+'?year=2017','Observations in 2017',utfgridapi.attr,true,{type:'square_sm_bl',scaleType:'opacity',scaleFactor:20000,colorRGB:[0,255,0]});
fcreateutflayer(4,utfgridapi.url+'?year=2016','Observations in 2016',utfgridapi.attr,true,{type:'square_sm_br',scaleType:'opacity',scaleFactor:20000,colorRGB:[0,0,255]});
};
if (fexample(3,'2019 Observations in Western Europe (and Nearby Areas), Highlighting City Nature Challenge 2019 Observations','The previous map (Example 2) seemed to show high numbers of observations in a lot of new places Western Europe in 2019. This map uses 2 different markers to show where CNC 2019 observations coincided with all 2019 observations. The markers use the same opacity scale (density factor). Where CNC 2019 obervations were not associated with one of the high-observations areas, the map may give an idea of places to promote CNC in 2020.')) {
fmapextent({swlat:29.24411828132679,swlng:-12.73389295962346,nelat:61.69814729146579,nelng:49.55155475708949});
fcreatebaselayer(0,bm_s_toner.url,bm_s_toner.description,bm_s_toner.attribution,true,'brightness(70%)');
fcreateutflayer(1,utfgridapi.url+'?year=2019','Observations in 2019',utfgridapi.attr,true,{type:'square',scaleType:'opacity',scaleFactor:5000,colorRGB:[0,255,192]});
fcreateutflayer(2,utfgridapi.url+'?project_id=city-nature-challenge-2019','City Nature Challenge 2019 Observations',utfgridapi.attr,true,{type:'ring_square',scaleType:'opacity',scaleFactor:5000,colorRGB:[255,128,128]});
};
if (fexample(4,'Observations in South / East Asia','This map shows the same dataset using 2 different markers at 2 different opacity scales (density factor). That allows for better visualization of both places with relatively few observations and places with lots of observations, while showing the gradations in both scales. This map also shows how to use color filters on a black and white basemap to produce a colored basemap. The stock iNaturalist maps and a density map of iNaturalist observations from GBIF are included as layer options for comparison. (Click on the checkboxes below to toggle layers on/off.)')) {
fmapextent({swlat:3.0309559101643595,swlng:67.44079047157487,nelat:45.61413812981794,nelng:150.64420693828788});
fcreatebaselayer(0,bm_s_toner.url,bm_s_toner.description,bm_s_toner.attribution,true,'brightness(40%) sepia(40%) hue-rotate(60deg) saturate(400%) contrast(200%)');
fcreateutflayer(1,utfgridapi.url,'All Observations',utfgridapi.attr,true,{type:'circle',scaleType:'opacity',scaleFactor:1000,colorRGB:[255,192,64]});
fcreateutflayer(2,utfgridapi.url,'All Observations',utfgridapi.attr,true,{type:'ring_overlap',scaleType:'opacity',scaleFactor:25000,colorRGB:[255,192,64]});
fcreatertlayer(3,inat_points.url,inat_points.description,inat_points.attribution,false);
fcreatertlayer(4,inat_circles.url,inat_circles.description,inat_circles.attribution,false);
fcreatertlayer(5,inat_heat.url,inat_heat.description,inat_heat.attribution,false);
fcreatertlayer(6,inat_grid.url,inat_grid.description,inat_grid.attribution,false);
fcreatertlayer(7,gbif_density_point_py.url,gbif_density_point_py.description,gbif_density_point_py.attribution,false);
};
if (fexample(5,'Monarch Observations by Month (July 2019 to October 2019), aka Fall 2019 Monarch Migration','This applies some concepts from previous example maps, along with a filter on a specific taxon, to show geographic shifts in observations for that taxon over time.')) {
fmapextent({swlat:13.290435858313753,swlng:-125.83712134009516,nelat:52.407256269196154,nelng:-63.551673623382214}); // Continental US + Mexico
fcreatebaselayer(0,bm_s_toner.url,bm_s_toner.description,bm_s_toner.attribution,true,'invert(100%) brightness(60%) sepia(100%) hue-rotate(180deg) saturate(150%)');
fcreateutflayer(1,utfgridapi.url+'?taxon_id=48662&year=2019&month=10','Observations of Monarchs in October 2019',utfgridapi.attr,true,{type:'square_tl',scaleType:'opacity',scaleFactor:10,colorRGB:[255,0,0]});
fcreateutflayer(2,utfgridapi.url+'?taxon_id=48662&year=2019&month=9','Observations of Monarchs in September 2019',utfgridapi.attr,true,{type:'square_tr',scaleType:'opacity',scaleFactor:10,colorRGB:[255,255,0]});
fcreateutflayer(3,utfgridapi.url+'?taxon_id=48662&year=2019&month=8','Observations of Monarchs in August 2019',utfgridapi.attr,true,{type:'square_bl',scaleType:'opacity',scaleFactor:10,colorRGB:[0,255,0]});
fcreateutflayer(4,utfgridapi.url+'?taxon_id=48662&year=2019&month=7','Observations of Monarchs in July 2019',utfgridapi.attr,true,{type:'square_br',scaleType:'opacity',scaleFactor:10,colorRGB:[0,0,255]});
};
if (fexample(6,'Observations of Different Kinds of Organisms in the Greater Houston Area (Houston, TX, USA)','This uses the 4-corners-of-a-cell technique from previous example maps to visualize observations for 4 different kinds of organisms. It also uses a terrain basemap just for fun. This is similar to the pin maps that iNaturalist provides (included below as a layer option), but instead of each marker representing a single random observation, they represent a group of observations in a given cell on the map, with opacity scaled for observation density. (Click on the checkboxes below to toggle layers on/off.)')) {
fmapextent({swlat:28.779301713220775,swlng:-96.61523133516312,nelat:30.609263330698013,nelng:-94.20131103135645}); // Greater Houston
fcreatebaselayer(0,bm_s_terrain.url,bm_s_terrain.description,bm_s_terrain.attribution,true,'brightness(80%) saturate(50%)');
// fcreateutflayer(1,utfgridapi.url+'?taxon_id=47126','Observations of Plants',utfgridapi.attr,true,{type:'square_tl',scaleType:'opacity',scaleFactor:75,colorRGB:[0,255,0]}); // plants
// fcreateutflayer(2,utfgridapi.url+'?taxon_id=47170','Observations of Fungi',utfgridapi.attr,true,{type:'square_bl',scaleType:'opacity',scaleFactor:75,colorRGB:[255,0,255]}); // fungi
// fcreateutflayer(3,utfgridapi.url+'?taxon_id=1&without_taxon_id=355675','Observations of Invertebrates (Animals not Vertebrates)',utfgridapi.attr,true,{type:'square_tr',scaleType:'opacity',scaleFactor:75,colorRGB:[255,0,0]}); // invertebrates (non-vertebrate animals)
// fcreateutflayer(4,utfgridapi.url+'?taxon_id=355675','Observations of Vertebrates',utfgridapi.attr,true,{type:'square_br',scaleType:'opacity',scaleFactor:75,colorRGB:[0,0,255]}); // vertebrates
fcreateutflayer(1,utfgridapi.url+'?taxon_id=47126','Observations of Plants',utfgridapi.attr,true,{type:'pixel_tl',scaleType:'opacity',scaleFactor:75,colorRGB:[0,255,0]}); // plants
fcreateutflayer(2,utfgridapi.url+'?taxon_id=47170','Observations of Fungi',utfgridapi.attr,true,{type:'pixel_bl',scaleType:'opacity',scaleFactor:75,colorRGB:[255,0,255]}); // fungi
fcreateutflayer(3,utfgridapi.url+'?taxon_id=1&without_taxon_id=355675','Observations of Invertebrates (Animals not Vertebrates)',utfgridapi.attr,true,{type:'pixel_tr',scaleType:'opacity',scaleFactor:75,colorRGB:[255,0,0]}); // invertebrates (non-vertebrate animals)
fcreateutflayer(4,utfgridapi.url+'?taxon_id=355675','Observations of Vertebrates',utfgridapi.attr,true,{type:'pixel_br',scaleType:'opacity',scaleFactor:75,colorRGB:[0,0,255]}); // vertebrates
fcreatertlayer(5,inat_points.url+'?taxon_id=47126,47170,1',inat_points.description+' – Plants, Fungi, and Animals',inat_points.attribution,false);
fcreatertlayer(6,inat_grid.url+'?taxon_id=47126,47170,1',inat_grid.description+' – Plants, Fungi, and Animals',inat_grid.attribution,false);
};
if (fexample(7,"A Single User's Contribution to Observations in Memorial Park (Houston, TX, USA)","This overlays a single user's observations on top of all observations in an area to show how that user's observations contributed to overall observations in the area. Relatively large markers with dark colors are used in this map to allow for maximum visibility on top of a white basemap. This particular marker will overlap with neighboring markers, providing extra emphasis to cells that have neighboring cells with observations.")) {
fmapextent({swlat:29.75417166016996,swlng:-95.45843155123293,nelat:29.7783586056903,nelng:-95.42147004045546}); // Memorial Park (Houston, TX)
fcreatebaselayer(0,bm_s_toner_lite.url,bm_s_toner_lite.description,bm_s_toner_lite.attribution);
fcreateutflayer(1,utfgridapi.url,'All Observations',utfgridapi.attr,true,{type:'ring_overlap',scaleType:'opacity',scaleFactor:15,colorRGB:[0,128,128]});
fcreateutflayer(2,utfgridapi.url+'?user_id=pisum','Observations by @pisum',utfgridapi.attr,true,{type:'ring_overlap',scaleType:'opacity',scaleFactor:15,colorRGB:[255,128,128]});
};
if (fexample(8,"Observations in Australia, New Zealand, and Nearby Nations","This map is different from previous examples in that its markers are linearly scaled along a color gradient (in this case, dull blue=low to bright magenta=high) instead of using opacity. The color gradient scaling is a little better than opacity scaling for visualizing wide ranges, but gradients are a little less flexible and more complex to code / configure. This is the only example created just to show what it looks like and to provide a little code to build on for those who might want to use gradients. The stock iNaturalist maps and a density map of iNaturalist observations from GBIF are included as layer options for comparison. (Click on the checkboxes below to toggle layers on/off.)")) {
fmapextent({swlat:-48.287651245912166,swlng:109.47315585603962,nelat:9.033917886632137,nelng:179.99}); // Australia & New Zealand
fcreatebaselayer(0,bm_s_toner.url,bm_s_toner.description,bm_s_toner.attribution,true,'brightness(80%)');
fcreateutflayer(1,utfgridapi.url,'All Observations',utfgridapi.attr,true,{type:'circle_med',scaleType:'grad_rainbow',scaleFactor:10000,opacity:0.70});
fcreatertlayer(2,inat_points.url,inat_points.description,inat_points.attribution,false);
fcreatertlayer(3,inat_circles.url,inat_circles.description,inat_circles.attribution,false);
fcreatertlayer(4,inat_heat.url,inat_heat.description,inat_heat.attribution,false);
fcreatertlayer(5,inat_grid.url,inat_grid.description,inat_grid.attribution,false);
fcreatertlayer(6,gbif_density_point_py.url,gbif_density_point_py.description,gbif_density_point_py.attribution,false);
};
if (fexample(9,"Observations in Africa","This is an attempt to achieve a logarithmic scale gradient using 5 increasingly larger overlapping different-colored opacity-scaled markers on the same data set but different linear scales. A density map of iNaturalist observations from GBIF is included as a layer option for comparison. (Click on the checkboxes below to toggle layers on and off.)")) {
fmapextent({swlat:-36.24407547872301,swlng:-19.965530715489194,nelat:38.65127535425379,nelng:54.68348971793671}); // Africa
fcreatebaselayer(0,bm_s_toner_lite.url,bm_s_toner_lite.description,bm_s_toner_lite.attribution,true,'invert(100%) brightness(50%) sepia(100%) hue-rotate(205deg) saturate(160%)');
fcreateutflayer(1,utfgridapi.url,'All Observations',utfgridapi.attr,true,{type:'pixel',scaleType:'opacity',scaleFactor:10,colorRGB:[0,0,255]});
fcreateutflayer(2,utfgridapi.url,'All Observations',utfgridapi.attr,true,{type:'pixel',scaleType:'opacity',scaleFactor:100,colorRGB:[0,255,0]});
fcreateutflayer(3,utfgridapi.url,'All Observations',utfgridapi.attr,true,{type:'square',scaleType:'opacity',scaleFactor:1000,colorRGB:[255,255,0]});
// fcreateutflayer(4,utfgridapi.url,'All Observations',utfgridapi.attr,true,{type:'square',scaleType:'opacity',scaleFactor:10000,colorRGB:[255,0,0]});
fcreateutflayer(4,utfgridapi.url,'All Observations',utfgridapi.attr,true,{type:'square_med',scaleType:'opacity',scaleFactor:10000,colorRGB:[255,0,0]});
fcreateutflayer(5,utfgridapi.url,'All Observations',utfgridapi.attr,true,{type:'diamond_overlap',scaleType:'opacity',scaleFactor:100000,colorRGB:[255,0,255]});
fcreatertlayer(6,gbif_density_point_py.url,gbif_density_point_py.description,gbif_density_point_py.attribution,false);
};
if (fexample(10,"Observations in South America","The markers in this example are blurred oversized circles. This is essentially the first step to creating a heatmap, though I'm not sure how to achieve a proper color gradient within the tiles of this map without distortion around the seams. I'll probably try to create something separately later where the heatmap layer is not tiled. The stock iNaturalist heatmap is included as a layer option for comparison. (Click on the checkboxes below to toggle layers on and off.)")) {
fmapextent({swlat:-52.603048981426824,swlng:-103.18359375,nelat:15.602720931852193,nelng:-24.2578125}); // South America
fcreatebaselayer(0,bm_s_toner_lite.url,bm_s_toner_lite.description,bm_s_toner_lite.attribution,true,'brightness(50%) sepia(100%) hue-rotate(15deg) saturate(160%)');
fcreateutflayer(1,utfgridapi.url,'All Observations',utfgridapi.attr,true,{type:'circle_big_blur',scaleType:'opacity',scaleFactor:7500,colorRGB:[192,64,0]});
fcreatertlayer(2,inat_heat.url,inat_heat.description,inat_heat.attribution,false);
fcreatertlayer(3,inat_circles.url,inat_circles.description,inat_circles.attribution,false);
fcreatertlayer(4,inat_grid.url,inat_grid.description,inat_grid.attribution,false);
};
if (elist.value==='nosel') { faddelem('p',mddescr,null,null,'No example found with the requested ID. Please specify another example ID or choose an example from the list above.'); };
</script>
</body>
</html>