Tool for making observations printer-friendly

Splitting this from a feature request. @pisum crafted a nice little tool for making iNat observations a bit more printer-friendly.

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

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: beige; }
  }
  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 is *intended* to pull observations from iNaturalist into an easy-to-print format, given parameters input below.
Sections in beige will not be printed.
</p><br />
<p>
To work within iNaturalist API limits, this will retrieve data in sets of up to 15 records at a time.
To get each 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. Excluding projects also helps.)
To clear results so that you can query with new parameters, refresh the page using your browser controls.
</p><br />
<p>
To do in future versions of this page:
<br />1. add additional parameter fields
<br />2. improve formatting
<br />3. *try* to add page numbering and other footer/header items
</p>
<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_recs_retrieved = 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=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 };
  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');
      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');    
    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');
    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');
    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');
    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');
      var photo = faddelem('img',photofig,'photo');
      var url = obj[m].url.replace('square','small');
      photo.src = url;      
      var photocaption = faddelem('figcaption',photofig,'photocaption');
      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');
      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');
    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);
  };
};
//Tags
function faddtags(eparent,obj) {
  var s = '';
  if (obj.length==0) { s='[None]'; }
  else {
    for (m=0;m<obj.length;m++) { s+=(m>0?', ':'')+obj[m]; };
  };
  faddelem('p',eparent,'tags',null,flabel('Tags')+s);
};

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+5) >= 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);
    enablequeryaddtl(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++;
        fqueryset(aryset,g_sort_order);
      };
    }, true);
  }
  else { enablequeryaddtl(false); };
};
function fqueryset(obsset,order) {
  corslite('https://api.inaturalist.org/v1/observations/'+obsset, function(err,response) { // observation detail
    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');
      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) { faddtags(obsdiv,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');
        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');
        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');
        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');
        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');
        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');
        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); }; 

      g_recs_retrieved++;
      faddelem('p',obsdiv,'no-print',null,'page break -- '+g_recs_retrieved+' of '+g_recs_expected+' recs retrieved.');
    };
  }, true);
  enablequeryaddtl(true);
};
</script>
</body>
</html>
5 Likes

Thank you so much for your work on this @pisum! This will be hugely helpful in my work and all kinds of other contexts.

2 Likes

Very cool.

When I run a query it maxes out at the 15 and nothing I do will load more results. Is there something I’m missing (very likely)?

Note: If I run the query using a place ID I can get more than 15 to load, but if I run the query using a user ID, or if I include more than one place ID it freezes at 15.

2 Likes

let me know which user ID and which place IDs are freezing for you. i’m thinking there might be an observation in such a set that’s not properly handled by this code, but i can’t tell without knowing what you’re trying to pull back.

also… let me know what kind of device + browser you’re using. it’s possible that if you find the part of the code that references window.pageYOffset and replace that with Math.ceil(window.pageYOffset) or (if you’re usin a Mac) Math.ceil(window.pageYOffset)+2, that might help.

1 Like

I was testing it on my user ID and the place IDs are for the project I’m running: https://www.inaturalist.org/projects/cat-ba-island-and-surrounding-area

I tried that with the project and as the 3 place IDs separated by commas, as suggested, both with and without spaces.

1 Like

ok. i think i fixed the problem. i think when using the mouse wheel to scroll to the bottom of the page to pull in additional records, the previous code wouldn’t always trigger correctly. the new code (replaced above) should handle things better regardless of how you’re scrolling.

while i was at it, i also:

  1. added record counts at each page break indicator
  2. changed the bright yellow background for non-printed sections to a more muted beige
  3. changed some wording (minor)
  4. parsed out tags a little more elegantly

if you see any other problems or have any other questions or comments, please let me know.

2 Likes

Cool. That seems to have fixed the issue. Thanks!

2 Likes

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.

i revisited this today, fixing some minor bugs in the code, making it more efficient under the hood, adding some minor functionality (an optional map thumbnail, a taxon input box), and changing the formatting a bit. i also put this up in Github so that you don’t have to run your own local version of the code (unless you want to).

page: https://jumear.github.io/stirfry/iNat_print_friendly_obs.html
code: https://github.com/jumear/stirfry/blob/gh-pages/iNat_print_friendly_obs.html

3 Likes

This is amazing, thanks a lot. I was looking for an option to print grid of my observations (photo and name). This is very close, pitty it is not in a grid so when you print it to pdf it occupies less space. Only first photo and name would be enough. I want to share fr4om time to time to my friends what we saw together. Thanks a lot

here’s a simple wrapper page that displays selected fields from the get observations endpoint in a human-friendly tabular format:

page: https://jumear.github.io/stirfry/iNatAPIv1_observations.html
code: https://github.com/jumear/stirfry/blob/gh-pages/iNatAPIv1_observations.html

example usage: https://jumear.github.io/stirfry/iNatAPIv1_observations.html?user_id=fero&per_page=200.

this doesn’t include as much information as the other thing, and you’ll have to specify parameters directly in the URL, but i think it should suffice for general use. i didn’t think to try to create this kind of page before, since it’s very close to the existing List View in the Explore screen, but i suppose this has a few extra fields, and anyone who wants to add additional stuff to it is welcome to modify the code as they wish (or suggest fields to add).

2 Likes

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.