Add print feature

When I collect a dead specimen, I like to print the iNat observation from the desktop (browser) app and include it in the baggy before I send it to the museum. If I use the print feature of the browser it prints about 6 pages of extraneous stuff. So I mark the portion I’m interested in first. Marking the exact information is kind of tricky since I want the name of the organism, my name, the place and date found and the description. First I have to edit the location to include the latitude and longitude since these don’t print otherwise. Then I have to click “show” next to the location in order to print the entire location. It would be nice to have settings where I can choose what I would like to print, for example my real name instead of my login name. Thanks.

This would be a pretty niche feature, since you can already export all your data for formatting in whatever way you want. Can you just print screenshots instead? e.g.

1 Like

I didn’t think of printing screenshots. I’ll have to try that next time. How do I export the observation? Thanks.

1 Like

You can export all of your observations by going to Your Observations (in header), then clicking Filters>Download (bottom right corner). It will bring you to:
https://www.inaturalist.org/observations/export?verifiable=any&page=1&spam=&place_id=any&user_id=dellfalconer&project_id=

You can choose what you want to export, scroll to bottom, and click Create Export.

@dellfalconer, does printing the screenshot work for you? Do you think we should close this feature request?

Printing a screenshot works better than the browser’s print function, but a print feature or print “view” of the observation would still be much preferable. I would still like to see this feature implemented. Thanks.

even if this was a 3rd party thing that grabbed the data for the obs through the API and then formatted up for printing (for me A4). Would be cool to be able to select or exclude “sections” like the DQA or CID parts. Would be low impact. If I ever get time to learn how to use the API, I’ll make it my hello world project!

@dellfalconer, @kiwifergus – just for grins, i was thinking of writing something to do this, if i get a few hours over the next week or so. (I’ve never tried to write anything for printing purposes, but i thnk it should be doable.) if successful, i’ll provide the code (html + js + css probably) so you can tweak and use yourself.

some questions:

  1. what sections / elements of the observation are most important to you? (what must absolutely be included, or alternatively, are there sections that you don’t care about?)
  2. ideally, how many pages should a typical observation span? (if more than one page, what should definitely show up on the first page, and what can get pushed to successive pages?)
  3. paper size? (@kiwifergus says A4) portrait or landscape?
  4. in a footer, i was going to put things like print date, page numbering, and observation id/link. would you want to see anything else there?
  5. i’m probably not going to make it super pretty like the website – mostly just text, other than photos and maps. what kind of font would you like for the main text, and how big? 8pt? 12pt?
  6. how would you like to see the photos laid out? a big one on top with smaller ones below if extra? all the same size? ideally, how big do you want the photos to be on the page?
  7. do you have a preference for maps? street maps? topo? aerial imagery? (i’m not sure all those kinds are available for free in all geographies, but i’ll see what i can find, and of course, you can always tweak and choose your own.) do you have a preferred map provider? how big do you want the map to be on the page?
  8. do you envision a situation where you would pull in more than one observation at a time? if so, how would you typically pull these in? by project? by date range / user id? (i’m not sure of the capabilities of the API. so i’m not sure how well it’ll work if there are a whole lot of observations pulled back in one batch.)

@jdmore – you mentioned something about mail merge in another thread. so i wonder if you might have some thoughts, too?

Awesome!

For my use case, I would be looking to print an archive page to file away with my reference collection. I pin moths and have spiders in alcohol, and the traditional labelling side of things is finicky, so I was considering just including the iNat number on the label, and having that as the lookup into the paper archive. Of course, I would mostly use iNat proper to look them up, as it is dynamic, but I do have an ingrained desire for hardcopy “backup” version (grew up with computers through the 80’s so have experienced many data losses in my time!)

Another use case was for after bioblitzes, and being able to “export a booklet” of all the observations made (and/or taxa encountered), and then edit it to customise it for the audience involved.

Hi. My case is printing something to include in the baggie when I send a specimen to the museum.

  1. I need the name of the specimen (common name in the chosen language such as English or German plus scientific name if possible), my name (real name if possible, otherwise screen name), the observation date, place including latitude and longitude, and the description.
  2. It needn’t be more than one or two pages.
  3. A4 where I live.
  4. Sounds good.
  5. At least 10 pt; 12 pt would be better.
  6. Photos aren’t that important.
  7. Maps aren’t important either. I prefer Open Street Map. The place description including latitude and longitude are important. Ideally also with country and first level beneath country, e.g. state or province.
  8. My case only requires one observation at a time.

Thanks.

  1. For permanent documentation, I would want it to include everything relevant to the Who, What, When, Where, How, and Why of an observation, including the photographic or other visual evidence (and notation of any non-visual evidence), description, IDs and comments, etc. Especially since just about every piece of an Observation is changeable over time, I would want a “snapshot in time” for that observation that captures everything that could potentially be modified later. Things like project or place membership are probably not important, but others may disagree. The top 3 levels of standard places (Country, State-equivalent, County-equivalent) would probably be good to list.
  2. As few pages as possible, while still capturing the relevant data. First page should include the first photo, and the Community ID (and Observer ID if possible). Any additional photos could be smaller thumbnails in a grid at the end of the document.
  3. As long as it is convertible to PDF (or even better, native PDF!), I’m not too worried about exact size, as it can be fit to A4, American 8.5x11 inches, or whatever is needed. Portrait format would definitely be my preference.
  4. Sounds good.
  5. 10pt font would probably be a good compromise, something universal like Helvetica or Times Roman that is most likely to be supported on any system.
  6. (see # 1 above)
  7. Most universally usable would probably be topo. But personally, if I have the map coordinates, accuracy, and obscuration status, I don’t need the map too.
  8. Would sure be nice if a filtered batch could be printed/exported to PDF with page breaks between each observation. But really, just having this capability for one observation at a time would be a huge help! Thanks for working on it!
1 Like

ok. i have an early prototype of something that will pull back observations in what will hopefully be a more printer friendly format. a couple of screenshots are attached, and the code is pasted below. you can copy the code, paste it into a text editor like Notepad or an HTML editor, and save it as an .html file. then you’ll be able to open up the resulting file in any web browser. i’m going to continue to make a few improvements, but if you have any questions or feedback on this so far, let me know.

a couple of fundamental things that i’m not planning to address:

  1. the iNat API doesn’t appear to allow an authentication flow from a simple local web page like this. because of that, this will return only information that any rando would be able to see on the website without logging in. (i think it would be possible to do the authentication if i hosted this, but i’m not planning to do that.)
  2. this doesn’t support multiple languages. I’ve written it in English only.


(see newer post for code)

Your HTML file works great when I enter a User ID, for example dellfalconer. However, nothing seems to happen when I enter an Observation ID, for example 831040. Am I doing something wrong? Thanks.

oops. it’s fixed now. try the updated code from the earlier post. there was a loop that got called only if the result set was >1 instead of >0.

Works great now! This does exactly what it should do. I will be using this in the future for the purpose I mentioned above. Thanks!

1 Like

you’re welcome. i’m glad this works for you. i’ll continue to refine this as i get time, and i’ll update here.

1 Like

@dellfalconer - as I don’t think we’ll be adding this feature, do you mind if I split @pisum’s code and the responses to it into a separate thread and close this request?

No problem :-)

here’s the latest version. it displays and sorts dates a little more elegantly, allows you to include/exclude certain sections, and adds maps. you’re welcome to use and tweak without attribution (except that you should keep attribution for the third party stuff i included). i probably won’t make more refinements in the near future (unless bugs) since the bones are all there at this point. but i’ll probably add some improvements further down the road.

<!DOCTYPE html>
<html lang="en">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="description" content="Observation for Print" />
<title>Observation for Print</title>

<style>
  @media print {
    .no-print, .no-print * { display:none !important; height:0px; }
    .obsdiv { page-break-after:always; }
  }
  @media screen {
    .obsdiv { margin: 1em 1em 0em 1em; }
    .no-print { background: yellow; }
  }
  body { margin:0px; padding:0px; font:10pt sans-serif; }
  h1 { margin:0px; padding:0px; font-size:16pt; }
  p { margin:0px; padding:0px; font-size:10pt; padding-left:1.5em; text-indent:-1.5em; }
  ul, dl { font-size:10pt; margin:0px; padding-left:1.5em; }
  a { text-decoration:none; color:green; }
  .label { font-size:10pt; }
  .withdrawn { text-decoration:line-through; }
  .intext { width:100%; }
  .insmtext { width:2.5em; text-align:right; }
  figure { margin:0.25em 0em 0em 0em; page-break-inside:avoid; border:1px dotted black; padding:0.25em; }
  #settings { padding:0.25em; }
  .grade { font-size:10pt; font-weight:500; }
  .obsthumb { position:absolute; right:1em; margin:0px; }
  dt { display:list-item; list-style-type:disc; }
  dd { display:list-item; list-style-type:circle; margin-left:1em; }
  .mapdiv { height:3.5in; width:5in; }
</style>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js" integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og==" crossorigin=""></script>
<script>
/*
https://github.com/mapbox/corslite
BSD 2-Clause License

Copyright (c) 2017, Mapbox
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

function corslite(url, callback, cors) {
   var sent = false;

   if (typeof window.XMLHttpRequest === 'undefined') {
      return callback(Error('Browser not supported'));
   }

   if (typeof cors === 'undefined') {
      var m = url.match(/^\s*https?:\/\/[^\/]*/);
      cors = m && (m[0] !== location.protocol + '//' + location.hostname +
           (location.port ? ':' + location.port : ''));
   }

   var x = new window.XMLHttpRequest();

   function isSuccessful(status) {
      return status >= 200 && status < 300 || status === 304;
   }

   if (cors && !('withCredentials' in x)) {
      // IE8-9
      x = new window.XDomainRequest();

      // Ensure callback is never called synchronously, i.e., before
      // x.send() returns (this has been observed in the wild).
      // See https://github.com/mapbox/mapbox.js/issues/472
      var original = callback;
      callback = function() {
        if (sent) {
           original.apply(this, arguments);
        } else {
           var that = this, args = arguments;
           setTimeout(function() {
              original.apply(that, args);
           }, 0);
        }
      }
   }

   function loaded() {
      if (
        // XDomainRequest
        x.status === undefined ||
        // modern browsers
        isSuccessful(x.status)) callback.call(x, null, x);
      else callback.call(x, x, null);
   }

   // Both `onreadystatechange` and `onload` can fire. `onreadystatechange`
   // has [been supported for longer](http://stackoverflow.com/a/9181508/229001).
   if ('onload' in x) {
      x.onload = loaded;
   } else {
      x.onreadystatechange = function readystate() {
        if (x.readyState === 4) {
           loaded();
        }
      };
   }

   // Call the callback with the XMLHttpRequest object as an error and prevent
   // it from ever being called again by reassigning it to `noop`
   x.onerror = function error(evt) {
      // XDomainRequest provides no evt parameter
      callback.call(this, evt || true, null);
      callback = function() { };
   };

   // IE9 must have onprogress be set to a unique function.
   x.onprogress = function() { };

   x.ontimeout = function(evt) {
      callback.call(this, evt, null);
      callback = function() { };
   };

   x.onabort = function(evt) {
      callback.call(this, evt, null);
      callback = function() { };
   };

   // GET is the only supported HTTP Verb by XDomainRequest and is the
   // only one supported here.
   x.open('GET', url, true);

   // Send the request. Sending data is not supported.
   x.send(null);
   sent = true;

   return x;
}

if (typeof module !== 'undefined') module.exports = corslite;
</script>
</head>

<body>
<div class="no-print" id="settings">
<p>
This will pull back observations from iNaturalist given the parameters input below.
The results are *intended* to be in a format that will be easy to print.
Sections in yellow will be shown on the screen but will not be printed.
</p><br />
<p>
Because of the way the iNaturalist API works, this page will pull back records in sets of up to 15 records at a time.
To get an additional set of 15 records, scroll to the end of the page. (But don't scroll too fast, or iNaturalist might refuse to send some data.)
To clear the results so that you can query a new set of records, refresh the page using your browser controls.
</p><br />
<p>
To do in future versions of this page:
<br />1. add a count of records returned
<br />2. add additional query parameter fields
<br />3. improve some formatting
<br />4. *try* to implement some page numbering and other page footers/headers
</p>
<br /><br />
<form action="javascript:fquery()">
  <fieldset>
    <legend>Query Parameters (separate multiple values with commas)</legend> 
    Observation IDs:<br /><input type="text" name="inobs" id="inobs" class="intext" /><br />
    User IDs:<br /><input type="text" name="inuser" id="inuser" class="intext" /><br />
    Place IDs:<br /><input type="text" name="inplace" id="inplace" class="intext" /><br />
    Project IDs:<br /><input type="text" name="inproj" id="inproj" class="intext" /><br />
    Observed From YYYY/MM/DD:<br /><input type="text" name="inobsdtfrom" id="inobsdtfrom" class="intext" /><br />
    Observed To YYYY/MM/DD:<br /><input type="text" name="inobsdtto" id="inobsdtto" class="intext" />
    Sort by ID:
      <input type="radio" name="inorder" value="asc" id="inorderasc" class="inradio" checked />Asc
      <input type="radio" name="inorder" value="desc" id="inorderdesc" class="inradio" />Desc<br /><br />
    Include:
      <input type="checkbox" name="inget" value="ic" id="ingetic" class="incheckbox" checked />Identifications and Comments
      <input type="checkbox" name="inget" value="tag" id="ingettag" class="incheckbox" checked />Tags
      <input type="checkbox" name="inget" value="photo" id="ingetphoto" class="incheckbox" checked />Photos
      <input type="checkbox" name="inget" value="sound" id="ingetsound" class="incheckbox" checked />Sounds
      <input type="checkbox" name="inget" value="proj" id="ingetproj" class="incheckbox" checked />Projects
      <input type="checkbox" name="inget" value="annot" id="ingetannot" class="incheckbox" checked />Annotations
      <input type="checkbox" name="inget" value="obsfld" id="ingetobsfld" class="incheckbox" checked />Observation Fields
      <input type="checkbox" name="inget" value="ic" id="ingetdqa" class="incheckbox" checked />Data Quality Assessments
      <input type="checkbox" name="inget" value="fave" id="ingetfave" class="incheckbox" checked />Faves
      <input type="checkbox" name="inget" value="map" id="ingetmap" class="incheckbox" />Maps
    <br /><br />
    <input type="submit" name="submit" id="submit" value="Query" />
  </fieldset>
</form>
</div>
<script>
var g_page = 1; // page to pull back; start at 1 and increment after each query
var g_perset = 15; // records returned per page
var g_enable_query = true;
var g_enable_query_addtl = false;
var g_recs_expected = -1;
var g_recs_remaining = 0;
var g_sort_order = 'asc'; // set to asc by default
var g_query_string = '';

var g_get_photo = true;
var g_get_sound = true;
var g_get_proj = true;
var g_get_annot = true;
var g_get_dqa = true;
var g_get_obsfld = true;
var g_get_ic = true;
var g_get_fave = true;
var g_get_tag = true;
var g_get_map = true;

function flabel(label) { return '<span class="label">'+label+':</span> '; };
function fidlink(suburl,typeid) { return ' (#<a href="https://www.inaturalist.org/'+suburl+'/'+typeid+'">'+typeid+'</a>)'; };
function furl(url) { return '<a href="'+url+'">'+url+'</a>'; };
//translate date string to date value (for date comparison)
function fdateval(str) {
  var x = new Date(str);
  return x;
};
//reformat an ISO formatted date string
function fdate(str) {
  str = str.replace(/t/i,' '); //replaces T (case insensitive) with a space
  str = str.replace(/z/i,' GMT'); //replaces Z (case insensitve) with GMT
  str = str.replace(/([+-]\d{2}\:?\d{2})/,' GMT$1'); //adds GMT prefix to timezone offset
  return str;
};
function ftaxon(obj,addinfo=false) { 
  var s = ''
  if (obj==null) { s = '[Unknown]'; }
  else {
    if (obj.rank!=null) { s += obj.rank+' / '; };
    s += obj.name+fidlink('taxa',obj.id);
    if (obj.preferred_common_name!=null) { s += ' / '+obj.preferred_common_name; };
    if (addinfo) { s += (obj.native?' (Native)':'')+(obj.endemic?' (Endemic)':'')+(obj.introduced?' (Introduced)':'')+(obj.threatened?' (Threatened)':''); };
  };
  return s;
};
function fuser(obj) {
  var s = ''
  if (obj==null) { s = '[Unknown]'; }
  else {
    s += obj.login+fidlink('users',obj.id);
    if (obj.name!=null&&obj.name!='') { s += ' / '+obj.name; };
  };
  return s;
};
function faddelem(etype,eparent,eclass,eid,ehtml) {
  var eobj = document.createElement(etype);
  if (eclass!==null) { eobj.classList = eclass };
  if (eid!==null) { eobj.id = eid };
  if (ehtml!==null) { eobj.innerHTML = ehtml };
  eparent.appendChild(eobj);
  return eobj;
};
//Encompassing Places (Country, State, County only)
function faddplaces(eparent,ary) {
  if (ary.length==0) {return;};
  corslite('https://api.inaturalist.org/v1/places/'+ary, function(err, response) {
    if (err) {
      self.fire('error', {error: err});
      return;
      };
    var placedata = JSON.parse(response.responseText);
    if (placedata.results!=null && placedata.results.length>0) {
      var adminlevels = ['Country','State','County'];
      for (k=0;k<=2;k++) {
        for (m=0;m<placedata.results.length;m++) {
          if (placedata.results[m].admin_level!=null && placedata.results[m].admin_level==k) {
            faddelem('p',eparent,adminlevels[k],null,flabel(adminlevels[k])+placedata.results[m].name+fidlink('places',placedata.results[m].id));
          };
        };
      };
    };
  }, true);
};
//Projects
function faddprojs(eparent,ary) {
  if (ary.length==0) {return;};
  corslite('https://api.inaturalist.org/v1/projects/'+ary, function(err, response) {
    if (err) {
      self.fire('error', {error: err});
      return;
      };
    var projdata = JSON.parse(response.responseText);
    if (projdata.results!=null && projdata.results.length>0) {
      var projul = faddelem('ul',eparent,'projlist',null,null);
      for (m=0;m<projdata.results.length;m++) {
        faddelem('li',projul,null,null,projdata.results[m].title+fidlink('projects',projdata.results[m].id));
      };
    };
  }, true);
};
//Identifications + Comments
function faddic(eparent,obj) {
  var obji = obj.identifications;
  var objc = obj.comments;
  var ary = [];
  if (obji!=null && obji.length!=0) {
    for (j=0;j<obji.length;j++) {
      ary.push({
        created_at: obji[j].created_at,
        type: 'id',
        id: obji[j].id,
        user: obji[j].user,
        body: (obji[j].body==null?null:obji[j].body),
        current: (obji[j].current==null?null:obji[j].current),
        category: (obji[j].category==null?null:obji[j].category),
        disagreement: (obji[j].disagreement==null?null:obji[j].disagreement),
        taxon: obji[j].taxon,
        taxon_change: (obji[j].taxon_change==null?null:obji[j].taxon_change),
        vision: (obji[j].vision==null?null:obji[j].vision)
      });
    };
  };
  if (objc!=null && objc.length!=0) {
    for (k=0;k<objc.length;k++) {
      ary.push({
        created_at: objc[k].created_at,
        type: 'comment',
        id: objc[k].id,
        user: objc[k].user,
        body: (objc[k].body==null?null:objc[k].body),
        current: null,
        category: null,
        disagreement: null,
        taxon: null,
        taxon_change: null,
        vision: null
      });
    };
  };
  ary.sort(function(a, b) {
     if(fdateval(a.created_at) < fdateval(b.created_at)) { return -1; }
     if(fdateval(a.created_at) > fdateval(b.created_at)) { return 1; }
     return 0;
  });
  if (ary!=null && ary.length!=0) {
    var icdl = faddelem('dl',eparent,'iclist',null,null);    
    for (m=0;m<ary.length;m++) {
      if (ary[m].type=='id') {
        faddelem('dt',icdl,null,null,fdate(ary[m].created_at)+ ' Identification #'+ary[m].id+' by '+fuser(ary[m].user));
        faddelem('dd',icdl,(ary[m].current?null:'withdrawn'),null,ftaxon(ary[m].taxon)+(ary[m].category==null?'':(' ('+ary[m].category+')'))+(ary[m].disagreement==true?' (disagreement)':'')+(ary[m].vision?' (computer vision assisted)':'')+(ary[m].taxon_change?' (taxon change)':''));
        if (ary[m].body != null && ary[m].body.trim() !='' ) { faddelem('dd',icdl,null,null,ary[m].body); };
      }
      else if (ary[m].type=='comment') {
        faddelem('dt',icdl,null,null,fdate(ary[m].created_at)+' Comment #'+ary[m].id+' by '+fuser(ary[m].user));
        faddelem('dd',icdl,null,null,ary[m].body);
      };				
    };
  };
};
//Observation Fields
function faddobsflds(eparent,obj) {
  if (obj!=null && obj.length>0) {
    var ofdl = faddelem('dl',eparent,'oflist',null,null);
    for (m=0;m<obj.length;m++) {
      faddelem('dt',ofdl,null,null,'Observation Field Entry #'+obj[m].id+' by '+fuser(obj[m].user));
      faddelem('dd',ofdl,null,null,flabel(obj[m].name+fidlink('observation_fields',obj[m].field_id))+(obj[m].datatype=='taxon'?ftaxon(obj[m].taxon):obj[m].value));
    };
  };
};
//Annotations
function faddannot(eparent,obj) {
  if (obj!=null && obj.length>0) {
    var annotdl = faddelem('dl',eparent,'oflist',null,null);
    for (m=0;m<obj.length;m++) {
      faddelem('dt',annotdl,null,null,obj[m].controlled_attribute.label+' > '+obj[m].controlled_value.label+' by '+fuser(obj[m].user));
      if (obj[m].votes!=null && obj[m].votes.length>0) {
        var agree = [];
        var disagree = [];
        for (n=0;n<obj.length;n++) {
          var s = fuser(obj[m].votes[n].user);
          if (obj[m].votes[n].vote_flag == true) { agree.push(s); }
          else if (obj[m].votes[n].vote_flag == false) { disagree.push(s); };
        };
        if (agree.length>0) { faddelem('dd',annotdl,null,null,flabel('Agree')+agree); };
        if (disagree.length>0) { faddelem('dd',annotdl,null,null,flabel('Disagree')+disagree); };
      };
    };
  };
};
//Data Quality Assessment
function fadddqa(eparent,obj) {
  obj.sort(function(a, b) {
     if(a.id < b.id) { return -1; }
     if(a.id > b.id) { return 1; }
     return 0;
  });
  obj.sort(function(a, b) {
     if(a.metric < b.metric) { return -1; }
     if(a.metric > b.metric) { return 1; }
     return 0;
  });
  if (obj!=null || obj.length>0) {
    var dqaul = faddelem('ul',eparent,'dqalist',null,null);
    for (m=0;m<obj.length;m++) {
      faddelem('li',dqaul,null,null,'DQA #'+obj[m].id+' '+obj[m].metric+'='+obj[m].agree+' by '+fuser(obj[m].user));
    };
  };
};
//Favorites
function faddfaves(eparent,obj) {
  if (obj!=null && obj.length>0) {
    var s = '';
    for (m=0;m<obj.length;m++) {
      if (m>0) { s+=', '; };
      s+= fuser(obj[m].user);
    };
    if (s!='') { faddelem('p',eparent,'faves',null,flabel('Faved By')+s); };
  };
};
//Photos
function faddphotos(eparent,obj) {
  if (obj!=null && obj.length>0) {
    for (m=0;m<obj.length;m++) {
      var photofig = faddelem('figure',eparent,'photofig',null,null);
      var photo = faddelem('img',photofig,'photo',null,null);
      var url = obj[m].url.replace('square','small');
      photo.src = url;      
      var photocaption = faddelem('figcaption',photofig,'photocaption',null,null);
      faddelem('p',photocaption,'photoattr',null,obj[m].attribution);
      faddelem('p',photocaption,'photourl',null,furl(url));
      faddelem('p',photocaption,'photodim',null,'Original dimensions '+obj[m].original_dimensions.height+'x'+obj[m].original_dimensions.width);
    };
  };
};
//Sounds
function faddsounds(eparent,obj) {
  if (obj!=null && obj.length>0) {
    for (m=0;m<obj.length;m++) {
      var soundfig = faddelem('figure',eparent,'soundfig',null,null);
      faddelem('p',soundfig,'soundurl',null,flabel(obj[m].file_content_type)+furl(obj[m].file_url));
      var soundcaption = faddelem('figcaption',soundfig,'soundcaption',null,null);
      faddelem('p',soundcaption,'soundattr',null,obj[m].attribution);
    };
  };
};
//Outlinks
function foutlinks(eparent,obj) {
  if (obj!=null && obj.length>0) {
    for (m=0;m<obj.length;m++) { faddelem('p',eparent,'outlink',null,flabel(obj[0].source+' URL')+furl(obj[0].url)); };
  };
};
//Community ID + Opt Out indicator
function fcid(eparent,obj) {
  var optout_userlvl = (obj.user.preferences.prefers_community_taxa==null ? false : !obj.user.preferences.prefers_community_taxa);
  var optout_obslvl = (obj.preferences.prefers_community_taxon==null ? false : !obj.preferences.prefers_community_taxon);
  if ( optout_userlvl || optout_obslvl ) {
    var ary = [];
    if (optout_userlvl) { ary.push('User'); };
    if (optout_obslvl) { ary.push('Observation'); };
    faddelem('p',eparent,'cidtaxon',null,flabel('Community Taxon')+ftaxon(obj.community_taxon));
    faddelem('p',eparent,'cidoptout',null,flabel('Community ID Opt Out Level')+ary);     
  };
};
//Map
function fmap(obsid,eparent,obj) {
  if (obj!=null && obj.coordinates[0]!=null && obj.coordinates[1]!=null) {
    // OpenStreetMaps & OpenTopoMap layers
    //var l_osm_fr = L.tileLayer('https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png', {maxZoom:20, attribution:'donn&eacute;es &copy; <a href="https://osm.org/copyright">OpenStreetMap</a>/ODbL - rendu <a href="https://openstreetmap.fr">OSM France</a>'});
    var l_osm_hot = L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {maxZoom:19, attribution:'donn&eacute;es &copy; <a href="https://osm.org/copyright">OpenStreetMap</a>/ODbL - Tiles courtesy of <a href="https://hot.openstreetmap.org/">Humanitarian OpenStreetMap Team</a>'});
    //var l_otm = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',{maxNativeZoom:17, attribution:'Kartendaten: &copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap</a>-Mitwirkende, SRTM | Kartendarstellung: &copy; <a href="http://opentopomap.org/">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)'});
    var mapdiv = faddelem('div',eparent,'mapdiv',null,null);
    var mapid = 'map'+obsid;
    mapdiv.id = mapid;
    var obsmap = L.map(mapid, {
      center: [obj.coordinates[1], obj.coordinates[0]],
      zoom: 10,
      layers: [l_osm_hot],
      doubleClickZoom: false
    });
    L.marker([obj.coordinates[1], obj.coordinates[0]]).addTo(obsmap);
    L.control.scale().addTo(obsmap);
  };
};

function fparams() {
  g_get_photo = document.getElementById('ingetphoto').checked;
  g_get_sound = document.getElementById('ingetsound').checked;
  g_get_proj = document.getElementById('ingetproj').checked;
  g_get_annot = document.getElementById('ingetannot').checked;
  g_get_dqa = document.getElementById('ingetdqa').checked;
  g_get_obsfld = document.getElementById('ingetobsfld').checked;
  g_get_ic = document.getElementById('ingetic').checked;
  g_get_fave = document.getElementById('ingetfave').checked;
  g_get_tag = document.getElementById('ingettag').checked;
  g_get_map = document.getElementById('ingetmap').checked;

  var inobs = document.getElementById('inobs').value;
  var inuser = document.getElementById('inuser').value;
  var inplace = document.getElementById('inplace').value;
  var inproj = document.getElementById('inproj').value;
  var inobsdtfrom = document.getElementById('inobsdtfrom').value;
  var inobsdtto = document.getElementById('inobsdtto').value;
  g_sort_order = document.getElementById('inorderdesc').checked?document.getElementById('inorderdesc').value:document.getElementById('inorderasc').value;

  var onlyid = 'only_id=true';
  var orderby = '&order_by=id&order='+g_sort_order;
  var perpage = '&per_page='+g_perset;

  var qparam = (inobs==''?'':('&id='+inobs))+(inuser==''?'':('&user_id='+inuser))+(inplace==''?'':('&place_id='+inplace))+(inproj==''?'':('&project_id='+inproj))+(inobsdtfrom==''?'':('&d1='+inobsdtfrom))+(inobsdtto==''?'':('&d2='+inobsdtto));  
  if (qparam=='') {
    alert('Please enter at least one query parameter.');
    return false;
    }
  else {
    g_query_string = 'https://api.inaturalist.org/v1/observations?'+onlyid+orderby+qparam+perpage;
    return true;
  };
};
function enablequery(b) {
  g_enable_query = b;
  document.getElementById("submit").disabled = !g_enable_query;
};
function enablequeryaddtl(b) {
   g_enable_query_addtl = b;
};
window.onscroll = function(ev) {
  if (g_enable_query_addtl && (window.innerHeight + window.pageYOffset) >= document.body.offsetHeight) {
    fquery();
  };
};
function fquery() {
  if (g_recs_expected==-1 && fparams()==false) { return; }
  if (g_recs_expected==-1 || g_recs_remaining>0) {
    enablequery(false);
    corslite(g_query_string+'&page='+g_page, function(err, response) {
      if (err) {
//        self.fire('error', {error: err});
        if (g_recs_expected==-1) {
          alert('No observations returned for these parameters. One or more of the parameter values may be invalid.');
          g_recs_remaining=0;
          enablequery(true);
          };
        return;
      };
      var obsset = JSON.parse(response.responseText);
      var ttlresults = obsset.total_results;
      if (ttlresults==0 && g_recs_expected==-1) {
        alert('No observations returned for these parameters.');
        g_recs_remaining=0;
        enablequery(true);
      }
      else if (ttlresults>0) {
        if (g_recs_expected==-1) {
          g_recs_expected = ttlresults;
          g_recs_remaining = ttlresults;
        };
        var aryset = [];
        for (t=0;t<obsset.results.length;t++) { aryset.push(obsset.results[t].id); };
        g_recs_remaining-=aryset.length;
        g_page++;
        enablequeryaddtl(false);
        fqueryset(aryset,g_sort_order);
      };
    }, true);
  }
  else { enablequeryaddtl(false); };
};
function fqueryset(obsset,order) {
  corslite('https://api.inaturalist.org/v1/observations/'+obsset, function(err, response) {
    if (err) {
      self.fire('error', {error: err});
      return;
    };
    var obsdata = JSON.parse(response.responseText);
    if (obsdata.results==null || obsdata.results.length==0) { alert("no observation returned"); };
    var ofactor = (order=='asc' ? 1 : -1);
    obsdata.results.sort(function(a, b) {
      if(a.id < b.id) { return -ofactor; }
      if(a.id > b.id) { return ofactor; }
      return 0;
    });
    for (i=0; i<obsdata.results.length; i++) { 
      var obs = obsdata.results[i];
      var obsid = obs.id;
      var obsdiv = faddelem('div',document.body,'obsdiv',obsid,((obs.photos==null || obs.photos.length==0)?null:'<img class="obsthumb" src="'+obs.photos[0].url+'" />'));
      faddelem('h1',obsdiv,'obsid',null,'Observation #'+obs.id+' <span class="grade">('+flabel('Grade')+obs.quality_grade+')</span>');
      faddelem('p',obsdiv,'url',null,flabel('iNaturalist URL')+furl(obs.uri));
      foutlinks(obsdiv,obs.outlinks);
      faddelem('p',obsdiv,'observer',null,flabel('Observer')+fuser(obs.user));
      faddelem('p',obsdiv,'license',null,flabel('Observation License')+(obs.license_code==null ? 'none (all rights reserved)' : obs.license_code));
      faddelem('p',obsdiv,'obsddt',null,flabel('Observed')+((obs.time_observed_at==null) ? ((obs.observed_on==null) ? '[Unknown]': obs.observed_on) : fdate(obs.time_observed_at)));
      faddelem('p',obsdiv,'createdt',null,flabel('Created')+((obs.created_at==null) ? fdate(obs.created_at_details.date) : fdate(obs.created_at)));
      faddelem('p',obsdiv,'updatedt',null,flabel('Last Updated')+fdate(obs.updated_at));
      faddelem('p',obsdiv,'taxon',null,flabel('Taxon')+ftaxon(obs.taxon,true));
      fcid(obsdiv,obs);
      var placediv = faddelem('div',obsdiv,'placediv',null,null);
      faddelem('p',placediv,'privacy',null,flabel('Observation / Taxon Geoprivacy')+(obs.geoprivacy==null ? '[Open]' : obs.geoprivacy)+' / '+(obs.taxon_geoprivacy==null ? '[Open]' : obs.taxon_geoprivacy));
      if (g_get_map) { fmap(obsid,placediv,obs.geojson); };
      faddelem('p',placediv,'location',null,flabel('Location')+(obs.place_guess==null?'[Unknown]':obs.place_guess));
      faddelem('p',placediv,'lat',null,flabel('Latitude')+((obs.geojson==null || obs.geojson.coordinates[1] == null)?'[Unknown]':obs.geojson.coordinates[1]));
      faddelem('p',placediv,'lng',null,flabel('Longitude')+((obs.geojson==null || obs.geojson.coordinates[0] == null)?'[Unknown]':obs.geojson.coordinates[0]));
      faddelem('p',placediv,'acc',null,flabel('Accuracy')+(obs.positional_accuracy==null?'[Unknown]':(obs.positional_accuracy+'m')));
      faddplaces(placediv,obs.place_ids);
      if (g_get_tag) { faddelem('p',obsdiv,'tags',null,flabel('Tags')+(obs.tags.length==0?'[None]':obs.tags)); };
      faddelem('p',obsdiv,'descr',null,flabel('Description')+(obs.description==null ? '[Not Provided]' : obs.description));
      if (g_get_proj) {
        var projdiv = faddelem('div',obsdiv,'projdiv',null,null);
        faddelem('p',projdiv,'projcount',null,flabel('Projects')+(obs.project_ids==null?'[None]':(obs.project_ids.length)));
        faddprojs(projdiv,obs.project_ids);
      };
      if (g_get_ic) {
        faddelem('p',obsdiv,'iccount',null,flabel('Identifications + Comments')+(obs.identifications==null?'[None]':(obs.identifications.length))+' + '+(obs.comments==null?'[None]':(obs.comments.length)));
        faddic(obsdiv,obs);
      };
      if (g_get_annot) {
        var annotdiv = faddelem('div',obsdiv,'annotdiv',null,null);
        faddelem('p',annotdiv,'annotcount',null,flabel('Annotations')+(obs.annotations==null?'[None]':(obs.annotations.length)));
        faddannot(annotdiv,obs.annotations);
      };
      if (g_get_obsfld) {
        var obsflddiv = faddelem('div',obsdiv,'obsflddiv',null,null);
        faddelem('p',obsflddiv,'obsfldcount',null,flabel('Observation Fields')+(obs.ofvs==null?'[None]':(obs.ofvs.length)));
        faddobsflds(obsflddiv,obs.ofvs);
      };
      if (g_get_dqa) {
        var dqadiv = faddelem('div',obsdiv,'dqadiv',null,null);
        faddelem('p',dqadiv,'dqacount',null,flabel('Data Quality Assessments')+(obs.quality_metrics==null?'[None]':(obs.quality_metrics.length)));
        fadddqa(dqadiv,obs.quality_metrics);
      };
      if (g_get_photo) {
        var photodiv = faddelem('div',obsdiv,'photodiv',null,null);
        faddelem('p',photodiv,'photos',null,flabel('Photos')+(obs.photos==null?'[None]':(obs.photos.length)));
        faddphotos(photodiv,obs.photos);
      };
      if (g_get_sound) {
        var sounddiv = faddelem('div',obsdiv,'sounddiv',null,null);
        faddelem('p',sounddiv,'sounds',null,flabel('Sounds')+(obs.sounds==null?'[None]':(obs.sounds.length)));
        faddsounds(sounddiv,obs.sounds);
      };
      if (g_get_fave) { faddfaves(obsdiv,obs.faves); }; 

      faddelem('p',obsdiv,'no-print',null,'---page break---');
    };
  }, true);
  enablequeryaddtl(true);
};
</script>
</body>
</html>