diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/404.html b/404.html new file mode 100644 index 00000000..ab40d891 --- /dev/null +++ b/404.html @@ -0,0 +1,2007 @@ + + + + + + + + + + + + + + + + + + + GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ +

404 - Not found

+ +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..619d44d6 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +geoblacklight.org diff --git a/about/index.html b/about/index.html new file mode 100644 index 00000000..a8b765c8 --- /dev/null +++ b/about/index.html @@ -0,0 +1,2266 @@ + + + + + + + + + + + + + + + + + + + + + + + + + About - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

About

+ +

GeoBlacklight is an open-source software application for discovering geospatial content, including GIS datasets, web services, and digitized paper maps.

+

The GeoBlacklight Ecosystem

+

Technical Core Stack

+
    +
  • GeoBlacklight: A Ruby on Rails engine
  • +
  • Blacklight: A widely-used open-source discovery framework
  • +
  • Apache Solr: Search index to make geospatial metadata searchable.
  • +
  • SQL database: For production uses, GeoBlacklight installations require an SQL database such as MySQL, MariaDB, or PostgreSQL. 
  • +
+

External Services

+

One of GeoBlacklight's strengths is its ability to serve as a bridge to geospatial content hosted on various platforms, simplifying the way users find and interact with data. Instead of storing data directly, GeoBlacklight focuses on integrating with existing data repositories and web services.GeoBlacklight does this through providing useful preview, download, and exports of open standards-based services, including Web Mapping Services (WMS), Web Feature Services (WFS), ArcGIS Rest API, and International Image Interoperability Framework (IIIF). There is also support for externally referenced metadata viewing and file download support.

+

This includes offering previews, downloads, and access to data through web standards such as Web Mapping Services (WMS), Web Feature Services (WFS), the ArcGIS Rest API, and the International Image Interoperability Framework (IIIF). Additionally, GeoBlacklight enables supplemental metadata views and downloads.

+

Examples of technology providing external services:

+
    +
  • GeoServer: previews and generated downloads via OGC Web Services
  • +
  • ArcGIS Platforms: previews via ArcGIS REST Services
  • +
  • Various IIIF Servers: previews and generated downloads of scanned images
  • +
  • Digital Repositories: direct downloads of datasets and related files
  • +
+

Metadata

+

GeoBlacklight uses the OpenGeoMetadata Aardvark Metadata Schema by default, which has been designed to privilege discovery use cases. It supports text searching, faceted searching and refinement, and spatial searching to improve relevance and findability of data. Visit OpenGeoMetadata for more information and full documentation.

+

Key Features

+
    +
  • Text and spatial search with ranking
  • +
  • Facet by institution, year, publisher, data type, access, format
  • +
  • Facet by place, subject
  • +
  • Results list view with icons, snippets, and map view of bounding boxes
  • +
  • Spatial search on map in result list
  • +
  • Detail map view for WMS features with feature inspection
  • +
  • IIIF scanned map viewer
  • +
  • Download the original file (Shapefile, GeoTIFF, GeoJSON, Esri Geodatabase, GeoPackage, or other SQLite database)
  • +
  • Download generated Shapefile/GeoTIFF/KML/GeoJSON
  • +
  • Built-in sample Solr 8.3+ index
  • +
  • Built on top of Blacklight platform
  • +
  • Search history
  • +
  • Bookmark layers
  • +
  • Share link via email
  • +
  • Sort by relevance, year, title
  • +
  • Customizable skin and facets
  • +
+

Technical Values

+
    +
  • +

    Our core focus is geospatial discovery. This focus initially was limited to discretely catalogued data objects, but has expanded over time to include a wider range of information sources.

    +
  • +
  • +

    We emphasize end-user experience, including inclusivity and accessibility in design features.

    +
  • +
  • +

    We prioritize stability by semantically versioning our application releases and metadata schemas.

    +
  • +
  • +

    We aim for GeoBlacklight to be simple to adopt and easy to maintain. We recognize that many adopters are in the cultural heritage space where metadata and software development resources can be limited.

    +
  • +
  • +

    We make GeoBlacklight customizable for common use cases, and extensible to a plugin for a less-common use case.

    +
  • +
  • +

    We leverage existing communities. Building on established standards gives us more bandwidth to focus on discovery and developing plugins.

    +
  • +
  • +

    Excellent geospatial analysis and mapping tools already exist. Rather than build new ones in GeoBlacklight, we focus on integration with these existing tools.

    +
  • +
+

Our Development Practices

+
    +
  • +

    Open source model: GeoBlacklight is an open source software project licensed using the Apache License, version 2.0. Our development practices have been codified in a contribution guide since December 2015 and we use semantic versioning to release the Ruby on Rails engine to RubyGems. Changes are made to the codebase using pull requests to the GitHub source code repository.

    +
  • +
  • +

    Connected frameworks: Many of the development practices for the GeoBlacklight project have foundations in other open source software communities. A strategic design decision was made to build on existing pools of expertise in organizations with Blacklight and Samvera rather than build a completely custom system. The project also relies heavily on configuration and extensibility as useful patterns for adopters making customizations.

    +
  • +
  • +

    Decision-making: Much of the technical decision-making is driven from the original GeoBlacklight Concept Design document and has been further distilled into our GeoBlacklight Technical Values. Major and minor decisions are made using informal consensus.

    +
  • +
  • +

    Testing: GeoBlacklight has Continuous Integration Testing, and tests are expected to be written with code contributions to the project. The project also implements both Ruby and JavaScript style guides to ensure a stylistically similar codebase.

    +
  • +
  • +

    Funding: There is no funding model for GeoBlacklight, and most development comes through volunteered or assigned time from contributing organizations. Some projects have received grants or dedicated funds to build their GeoBlacklight applications. Our community also includes private vendors and independent freelancers that have contributed to the project through contracted work.

    +
  • +
  • +

    GeoBlacklight Software Versioning: GeoBlacklight follows the practice of Semantic Versioning for software releases. The declared semantically versioned public API includes:

    + +
  • +
+

Connected Projects

+

The GeoBlacklight software stack consists of several open source software projects which work together to enable a better discovery experience.

+
+
    +
  • +

    GeoBlacklight

    +

    GeoBlacklight is the main discovery interface for geospatial data. It is developed as a Ruby on Rails engine and built on top of the popular open-source discovery interface Blacklight.

    +
  • +
  • +

    Dockerized GeoBlacklight

    +

    Developers from Harvard University have created a built instance of GeoBlacklight in a Docker context. This will allow new and existing users to test and develop an instance of GeoBlacklight within the Docker environment.

    +

    https://github.com/harvard-lts/GeoBlacklightDockerized

    +
  • +
  • +

    OpenGeoMetadata

    +

    GeoBlacklight is built to use the OpenGeoMetadata schema, which is designed for GIS resource discovery and focuses mainly on discovery use cases. Text search, faceted search and refinement, and spatial search and relevancy are among the primary features that the schema enables.

    +

    https://opengeometadata.org

    +
  • +
  • +

    OpenIndexMaps

    +

    A community format for sharing index maps in GeoBlacklight and a repository that hosts community-produced GeoJSON index maps that facilitate discovery within GeoBlacklight portals.

    +

    https://openindexmaps.org/

    +
  • +
  • +

    GeoBlacklight Sidecar images

    +

    This GeoBlacklight plugin captures remote images from geographic web services and saves them locally.

    +

    https://github.com/geoblacklight/geoblacklight_sidecar_images

    +
  • +
  • +

    Geomonitor

    +

    GeoMonitor is a Ruby on Rails application used to monitor geowebservices. It was built out of the premise that users who are looking for and find data should actually be able to access and use it. The application is setup to periodically monitor WMS web services and log data on a layers availability.

    +

    https://github.com/geoblacklight/geo_monitor

    +
  • +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 00000000..1cf13b9f Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/javascripts/bundle.fe8b6f2b.min.js b/assets/javascripts/bundle.fe8b6f2b.min.js new file mode 100644 index 00000000..cf778d42 --- /dev/null +++ b/assets/javascripts/bundle.fe8b6f2b.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Fi=Object.create;var gr=Object.defineProperty;var ji=Object.getOwnPropertyDescriptor;var Wi=Object.getOwnPropertyNames,Dt=Object.getOwnPropertySymbols,Ui=Object.getPrototypeOf,xr=Object.prototype.hasOwnProperty,no=Object.prototype.propertyIsEnumerable;var oo=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,R=(e,t)=>{for(var r in t||(t={}))xr.call(t,r)&&oo(e,r,t[r]);if(Dt)for(var r of Dt(t))no.call(t,r)&&oo(e,r,t[r]);return e};var io=(e,t)=>{var r={};for(var o in e)xr.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Dt)for(var o of Dt(e))t.indexOf(o)<0&&no.call(e,o)&&(r[o]=e[o]);return r};var yr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Di=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Wi(t))!xr.call(e,n)&&n!==r&&gr(e,n,{get:()=>t[n],enumerable:!(o=ji(t,n))||o.enumerable});return e};var Vt=(e,t,r)=>(r=e!=null?Fi(Ui(e)):{},Di(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var ao=(e,t,r)=>new Promise((o,n)=>{var i=p=>{try{s(r.next(p))}catch(c){n(c)}},a=p=>{try{s(r.throw(p))}catch(c){n(c)}},s=p=>p.done?o(p.value):Promise.resolve(p.value).then(i,a);s((r=r.apply(e,t)).next())});var co=yr((Er,so)=>{(function(e,t){typeof Er=="object"&&typeof so!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Er,function(){"use strict";function e(r){var o=!0,n=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(H){return!!(H&&H!==document&&H.nodeName!=="HTML"&&H.nodeName!=="BODY"&&"classList"in H&&"contains"in H.classList)}function p(H){var mt=H.type,ze=H.tagName;return!!(ze==="INPUT"&&a[mt]&&!H.readOnly||ze==="TEXTAREA"&&!H.readOnly||H.isContentEditable)}function c(H){H.classList.contains("focus-visible")||(H.classList.add("focus-visible"),H.setAttribute("data-focus-visible-added",""))}function l(H){H.hasAttribute("data-focus-visible-added")&&(H.classList.remove("focus-visible"),H.removeAttribute("data-focus-visible-added"))}function f(H){H.metaKey||H.altKey||H.ctrlKey||(s(r.activeElement)&&c(r.activeElement),o=!0)}function u(H){o=!1}function h(H){s(H.target)&&(o||p(H.target))&&c(H.target)}function w(H){s(H.target)&&(H.target.classList.contains("focus-visible")||H.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(H.target))}function A(H){document.visibilityState==="hidden"&&(n&&(o=!0),te())}function te(){document.addEventListener("mousemove",J),document.addEventListener("mousedown",J),document.addEventListener("mouseup",J),document.addEventListener("pointermove",J),document.addEventListener("pointerdown",J),document.addEventListener("pointerup",J),document.addEventListener("touchmove",J),document.addEventListener("touchstart",J),document.addEventListener("touchend",J)}function ie(){document.removeEventListener("mousemove",J),document.removeEventListener("mousedown",J),document.removeEventListener("mouseup",J),document.removeEventListener("pointermove",J),document.removeEventListener("pointerdown",J),document.removeEventListener("pointerup",J),document.removeEventListener("touchmove",J),document.removeEventListener("touchstart",J),document.removeEventListener("touchend",J)}function J(H){H.target.nodeName&&H.target.nodeName.toLowerCase()==="html"||(o=!1,ie())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",A,!0),te(),r.addEventListener("focus",h,!0),r.addEventListener("blur",w,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var Yr=yr((Rt,Kr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Rt=="object"&&typeof Kr=="object"?Kr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Rt=="object"?Rt.ClipboardJS=r():t.ClipboardJS=r()})(Rt,function(){return function(){var e={686:function(o,n,i){"use strict";i.d(n,{default:function(){return Ii}});var a=i(279),s=i.n(a),p=i(370),c=i.n(p),l=i(817),f=i.n(l);function u(V){try{return document.execCommand(V)}catch(_){return!1}}var h=function(_){var M=f()(_);return u("cut"),M},w=h;function A(V){var _=document.documentElement.getAttribute("dir")==="rtl",M=document.createElement("textarea");M.style.fontSize="12pt",M.style.border="0",M.style.padding="0",M.style.margin="0",M.style.position="absolute",M.style[_?"right":"left"]="-9999px";var j=window.pageYOffset||document.documentElement.scrollTop;return M.style.top="".concat(j,"px"),M.setAttribute("readonly",""),M.value=V,M}var te=function(_,M){var j=A(_);M.container.appendChild(j);var D=f()(j);return u("copy"),j.remove(),D},ie=function(_){var M=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},j="";return typeof _=="string"?j=te(_,M):_ instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(_==null?void 0:_.type)?j=te(_.value,M):(j=f()(_),u("copy")),j},J=ie;function H(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?H=function(M){return typeof M}:H=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},H(V)}var mt=function(){var _=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},M=_.action,j=M===void 0?"copy":M,D=_.container,Y=_.target,ke=_.text;if(j!=="copy"&&j!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Y!==void 0)if(Y&&H(Y)==="object"&&Y.nodeType===1){if(j==="copy"&&Y.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(j==="cut"&&(Y.hasAttribute("readonly")||Y.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(ke)return J(ke,{container:D});if(Y)return j==="cut"?w(Y):J(Y,{container:D})},ze=mt;function Ie(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Ie=function(M){return typeof M}:Ie=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},Ie(V)}function _i(V,_){if(!(V instanceof _))throw new TypeError("Cannot call a class as a function")}function ro(V,_){for(var M=0;M<_.length;M++){var j=_[M];j.enumerable=j.enumerable||!1,j.configurable=!0,"value"in j&&(j.writable=!0),Object.defineProperty(V,j.key,j)}}function Ai(V,_,M){return _&&ro(V.prototype,_),M&&ro(V,M),V}function Ci(V,_){if(typeof _!="function"&&_!==null)throw new TypeError("Super expression must either be null or a function");V.prototype=Object.create(_&&_.prototype,{constructor:{value:V,writable:!0,configurable:!0}}),_&&br(V,_)}function br(V,_){return br=Object.setPrototypeOf||function(j,D){return j.__proto__=D,j},br(V,_)}function Hi(V){var _=Pi();return function(){var j=Wt(V),D;if(_){var Y=Wt(this).constructor;D=Reflect.construct(j,arguments,Y)}else D=j.apply(this,arguments);return ki(this,D)}}function ki(V,_){return _&&(Ie(_)==="object"||typeof _=="function")?_:$i(V)}function $i(V){if(V===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return V}function Pi(){if(typeof Reflect=="undefined"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy=="function")return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(V){return!1}}function Wt(V){return Wt=Object.setPrototypeOf?Object.getPrototypeOf:function(M){return M.__proto__||Object.getPrototypeOf(M)},Wt(V)}function vr(V,_){var M="data-clipboard-".concat(V);if(_.hasAttribute(M))return _.getAttribute(M)}var Ri=function(V){Ci(M,V);var _=Hi(M);function M(j,D){var Y;return _i(this,M),Y=_.call(this),Y.resolveOptions(D),Y.listenClick(j),Y}return Ai(M,[{key:"resolveOptions",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof D.action=="function"?D.action:this.defaultAction,this.target=typeof D.target=="function"?D.target:this.defaultTarget,this.text=typeof D.text=="function"?D.text:this.defaultText,this.container=Ie(D.container)==="object"?D.container:document.body}},{key:"listenClick",value:function(D){var Y=this;this.listener=c()(D,"click",function(ke){return Y.onClick(ke)})}},{key:"onClick",value:function(D){var Y=D.delegateTarget||D.currentTarget,ke=this.action(Y)||"copy",Ut=ze({action:ke,container:this.container,target:this.target(Y),text:this.text(Y)});this.emit(Ut?"success":"error",{action:ke,text:Ut,trigger:Y,clearSelection:function(){Y&&Y.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(D){return vr("action",D)}},{key:"defaultTarget",value:function(D){var Y=vr("target",D);if(Y)return document.querySelector(Y)}},{key:"defaultText",value:function(D){return vr("text",D)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(D){var Y=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return J(D,Y)}},{key:"cut",value:function(D){return w(D)}},{key:"isSupported",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Y=typeof D=="string"?[D]:D,ke=!!document.queryCommandSupported;return Y.forEach(function(Ut){ke=ke&&!!document.queryCommandSupported(Ut)}),ke}}]),M}(s()),Ii=Ri},828:function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,p){for(;s&&s.nodeType!==n;){if(typeof s.matches=="function"&&s.matches(p))return s;s=s.parentNode}}o.exports=a},438:function(o,n,i){var a=i(828);function s(l,f,u,h,w){var A=c.apply(this,arguments);return l.addEventListener(u,A,w),{destroy:function(){l.removeEventListener(u,A,w)}}}function p(l,f,u,h,w){return typeof l.addEventListener=="function"?s.apply(null,arguments):typeof u=="function"?s.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(A){return s(A,f,u,h,w)}))}function c(l,f,u,h){return function(w){w.delegateTarget=a(w.target,f),w.delegateTarget&&h.call(l,w)}}o.exports=p},879:function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(o,n,i){var a=i(879),s=i(438);function p(u,h,w){if(!u&&!h&&!w)throw new Error("Missing required arguments");if(!a.string(h))throw new TypeError("Second argument must be a String");if(!a.fn(w))throw new TypeError("Third argument must be a Function");if(a.node(u))return c(u,h,w);if(a.nodeList(u))return l(u,h,w);if(a.string(u))return f(u,h,w);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(u,h,w){return u.addEventListener(h,w),{destroy:function(){u.removeEventListener(h,w)}}}function l(u,h,w){return Array.prototype.forEach.call(u,function(A){A.addEventListener(h,w)}),{destroy:function(){Array.prototype.forEach.call(u,function(A){A.removeEventListener(h,w)})}}}function f(u,h,w){return s(document.body,u,h,w)}o.exports=p},817:function(o){function n(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var p=window.getSelection(),c=document.createRange();c.selectNodeContents(i),p.removeAllRanges(),p.addRange(c),a=p.toString()}return a}o.exports=n},279:function(o){function n(){}n.prototype={on:function(i,a,s){var p=this.e||(this.e={});return(p[i]||(p[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var p=this;function c(){p.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),p=0,c=s.length;for(p;p{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var ts=/["'&<>]/;ei.exports=rs;function rs(e){var t=""+e,r=ts.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function N(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],a;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(s){a={error:s}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(a)throw a.error}}return i}function q(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||s(u,h)})})}function s(u,h){try{p(o[u](h))}catch(w){f(i[0][3],w)}}function p(u){u.value instanceof nt?Promise.resolve(u.value.v).then(c,l):f(i[0][2],u)}function c(u){s("next",u)}function l(u){s("throw",u)}function f(u,h){u(h),i.shift(),i.length&&s(i[0][0],i[0][1])}}function mo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof de=="function"?de(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(a){return new Promise(function(s,p){a=e[i](a),n(s,p,a.done,a.value)})}}function n(i,a,s,p){Promise.resolve(p).then(function(c){i({value:c,done:s})},a)}}function k(e){return typeof e=="function"}function ft(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var zt=ft(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function qe(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Fe=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=de(a),p=s.next();!p.done;p=s.next()){var c=p.value;c.remove(this)}}catch(A){t={error:A}}finally{try{p&&!p.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var l=this.initialTeardown;if(k(l))try{l()}catch(A){i=A instanceof zt?A.errors:[A]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=de(f),h=u.next();!h.done;h=u.next()){var w=h.value;try{fo(w)}catch(A){i=i!=null?i:[],A instanceof zt?i=q(q([],N(i)),N(A.errors)):i.push(A)}}}catch(A){o={error:A}}finally{try{h&&!h.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new zt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)fo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&qe(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&qe(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Tr=Fe.EMPTY;function qt(e){return e instanceof Fe||e&&"closed"in e&&k(e.remove)&&k(e.add)&&k(e.unsubscribe)}function fo(e){k(e)?e():e.unsubscribe()}var $e={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var ut={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,a=n.isStopped,s=n.observers;return i||a?Tr:(this.currentObservers=null,s.push(r),new Fe(function(){o.currentObservers=null,qe(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,a=o.isStopped;n?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,o){return new Eo(r,o)},t}(F);var Eo=function(e){re(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:Tr},t}(g);var _r=function(e){re(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t}(g);var Lt={now:function(){return(Lt.delegate||Date).now()},delegate:void 0};var _t=function(e){re(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=Lt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,a=o._infiniteTimeWindow,s=o._timestampProvider,p=o._windowTime;n||(i.push(r),!a&&i.push(s.now()+p)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,a=n._buffer,s=a.slice(),p=0;p0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t}(vt);var So=function(e){re(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t}(gt);var Hr=new So(To);var Oo=function(e){re(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=bt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var a=r.actions;o!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==o&&(bt.cancelAnimationFrame(o),r._scheduled=void 0)},t}(vt);var Mo=function(e){re(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o=this._scheduled;this._scheduled=void 0;var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t}(gt);var me=new Mo(Oo);var O=new F(function(e){return e.complete()});function Yt(e){return e&&k(e.schedule)}function kr(e){return e[e.length-1]}function Xe(e){return k(kr(e))?e.pop():void 0}function He(e){return Yt(kr(e))?e.pop():void 0}function Bt(e,t){return typeof kr(e)=="number"?e.pop():t}var xt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Gt(e){return k(e==null?void 0:e.then)}function Jt(e){return k(e[ht])}function Xt(e){return Symbol.asyncIterator&&k(e==null?void 0:e[Symbol.asyncIterator])}function Zt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Gi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var er=Gi();function tr(e){return k(e==null?void 0:e[er])}function rr(e){return lo(this,arguments,function(){var r,o,n,i;return Nt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,nt(r.read())];case 3:return o=a.sent(),n=o.value,i=o.done,i?[4,nt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,nt(n)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function or(e){return k(e==null?void 0:e.getReader)}function W(e){if(e instanceof F)return e;if(e!=null){if(Jt(e))return Ji(e);if(xt(e))return Xi(e);if(Gt(e))return Zi(e);if(Xt(e))return Lo(e);if(tr(e))return ea(e);if(or(e))return ta(e)}throw Zt(e)}function Ji(e){return new F(function(t){var r=e[ht]();if(k(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Xi(e){return new F(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?b(function(n,i){return e(n,i,o)}):le,Te(1),r?Be(t):zo(function(){return new ir}))}}function Fr(e){return e<=0?function(){return O}:y(function(t,r){var o=[];t.subscribe(T(r,function(n){o.push(n),e=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new g}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,p=s===void 0?!0:s;return function(c){var l,f,u,h=0,w=!1,A=!1,te=function(){f==null||f.unsubscribe(),f=void 0},ie=function(){te(),l=u=void 0,w=A=!1},J=function(){var H=l;ie(),H==null||H.unsubscribe()};return y(function(H,mt){h++,!A&&!w&&te();var ze=u=u!=null?u:r();mt.add(function(){h--,h===0&&!A&&!w&&(f=Wr(J,p))}),ze.subscribe(mt),!l&&h>0&&(l=new at({next:function(Ie){return ze.next(Ie)},error:function(Ie){A=!0,te(),f=Wr(ie,n,Ie),ze.error(Ie)},complete:function(){w=!0,te(),f=Wr(ie,a),ze.complete()}}),W(H).subscribe(l))})(c)}}function Wr(e,t){for(var r=[],o=2;oe.next(document)),e}function $(e,t=document){return Array.from(t.querySelectorAll(e))}function P(e,t=document){let r=fe(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function fe(e,t=document){return t.querySelector(e)||void 0}function Re(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var xa=S(d(document.body,"focusin"),d(document.body,"focusout")).pipe(_e(1),Q(void 0),m(()=>Re()||document.body),G(1));function et(e){return xa.pipe(m(t=>e.contains(t)),K())}function kt(e,t){return C(()=>S(d(e,"mouseenter").pipe(m(()=>!0)),d(e,"mouseleave").pipe(m(()=>!1))).pipe(t?Ht(r=>Me(+!r*t)):le,Q(e.matches(":hover"))))}function Bo(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Bo(e,r)}function x(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)Bo(o,n);return o}function sr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function wt(e){let t=x("script",{src:e});return C(()=>(document.head.appendChild(t),S(d(t,"load"),d(t,"error").pipe(v(()=>$r(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),L(()=>document.head.removeChild(t)),Te(1))))}var Go=new g,ya=C(()=>typeof ResizeObserver=="undefined"?wt("https://unpkg.com/resize-observer-polyfill"):I(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>Go.next(t)))),v(e=>S(Ke,I(e)).pipe(L(()=>e.disconnect()))),G(1));function ce(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return ya.pipe(E(r=>r.observe(t)),v(r=>Go.pipe(b(o=>o.target===t),L(()=>r.unobserve(t)))),m(()=>ce(e)),Q(ce(e)))}function Tt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function cr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Jo(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Ue(e){return{x:e.offsetLeft,y:e.offsetTop}}function Xo(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Zo(e){return S(d(window,"load"),d(window,"resize")).pipe(Le(0,me),m(()=>Ue(e)),Q(Ue(e)))}function pr(e){return{x:e.scrollLeft,y:e.scrollTop}}function De(e){return S(d(e,"scroll"),d(window,"scroll"),d(window,"resize")).pipe(Le(0,me),m(()=>pr(e)),Q(pr(e)))}var en=new g,Ea=C(()=>I(new IntersectionObserver(e=>{for(let t of e)en.next(t)},{threshold:0}))).pipe(v(e=>S(Ke,I(e)).pipe(L(()=>e.disconnect()))),G(1));function tt(e){return Ea.pipe(E(t=>t.observe(e)),v(t=>en.pipe(b(({target:r})=>r===e),L(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function tn(e,t=16){return De(e).pipe(m(({y:r})=>{let o=ce(e),n=Tt(e);return r>=n.height-o.height-t}),K())}var lr={drawer:P("[data-md-toggle=drawer]"),search:P("[data-md-toggle=search]")};function rn(e){return lr[e].checked}function Je(e,t){lr[e].checked!==t&&lr[e].click()}function Ve(e){let t=lr[e];return d(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function wa(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ta(){return S(d(window,"compositionstart").pipe(m(()=>!0)),d(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function on(){let e=d(window,"keydown").pipe(b(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:rn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),b(({mode:t,type:r})=>{if(t==="global"){let o=Re();if(typeof o!="undefined")return!wa(o,r)}return!0}),pe());return Ta().pipe(v(t=>t?O:e))}function xe(){return new URL(location.href)}function pt(e,t=!1){if(B("navigation.instant")&&!t){let r=x("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function nn(){return new g}function an(){return location.hash.slice(1)}function sn(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Sa(e){return S(d(window,"hashchange"),e).pipe(m(an),Q(an()),b(t=>t.length>0),G(1))}function cn(e){return Sa(e).pipe(m(t=>fe(`[id="${t}"]`)),b(t=>typeof t!="undefined"))}function $t(e){let t=matchMedia(e);return ar(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function pn(){let e=matchMedia("print");return S(d(window,"beforeprint").pipe(m(()=>!0)),d(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function Nr(e,t){return e.pipe(v(r=>r?t():O))}function zr(e,t){return new F(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let a=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+a*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function Ne(e,t){return zr(e,t).pipe(v(r=>r.text()),m(r=>JSON.parse(r)),G(1))}function ln(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),G(1))}function mn(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),G(1))}function fn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function un(){return S(d(window,"scroll",{passive:!0}),d(window,"resize",{passive:!0})).pipe(m(fn),Q(fn()))}function dn(){return{width:innerWidth,height:innerHeight}}function hn(){return d(window,"resize",{passive:!0}).pipe(m(dn),Q(dn()))}function bn(){return z([un(),hn()]).pipe(m(([e,t])=>({offset:e,size:t})),G(1))}function mr(e,{viewport$:t,header$:r}){let o=t.pipe(Z("size")),n=z([o,r]).pipe(m(()=>Ue(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:a,size:s},{x:p,y:c}])=>({offset:{x:a.x-p,y:a.y-c+i},size:s})))}function Oa(e){return d(e,"message",t=>t.data)}function Ma(e){let t=new g;return t.subscribe(r=>e.postMessage(r)),t}function vn(e,t=new Worker(e)){let r=Oa(t),o=Ma(t),n=new g;n.subscribe(o);let i=o.pipe(X(),ne(!0));return n.pipe(X(),Pe(r.pipe(U(i))),pe())}var La=P("#__config"),St=JSON.parse(La.textContent);St.base=`${new URL(St.base,xe())}`;function ye(){return St}function B(e){return St.features.includes(e)}function Ee(e,t){return typeof t!="undefined"?St.translations[e].replace("#",t.toString()):St.translations[e]}function Se(e,t=document){return P(`[data-md-component=${e}]`,t)}function ae(e,t=document){return $(`[data-md-component=${e}]`,t)}function _a(e){let t=P(".md-typeset > :first-child",e);return d(t,"click",{once:!0}).pipe(m(()=>P(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function gn(e){if(!B("announce.dismiss")||!e.childElementCount)return O;if(!e.hidden){let t=P(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return C(()=>{let t=new g;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),_a(e).pipe(E(r=>t.next(r)),L(()=>t.complete()),m(r=>R({ref:e},r)))})}function Aa(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function xn(e,t){let r=new g;return r.subscribe(({hidden:o})=>{e.hidden=o}),Aa(e,t).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))}function Pt(e,t){return t==="inline"?x("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"})):x("div",{class:"md-tooltip",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"}))}function yn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function En(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return x("aside",{class:"md-annotation",tabIndex:0},Pt(t),x("a",{href:r,class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}else return x("aside",{class:"md-annotation",tabIndex:0},Pt(t),x("span",{class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}function wn(e){return x("button",{class:"md-clipboard md-icon",title:Ee("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}function qr(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(p=>!e.terms[p]).reduce((p,c)=>[...p,x("del",null,c)," "],[]).slice(0,-1),i=ye(),a=new URL(e.location,i.base);B("search.highlight")&&a.searchParams.set("h",Object.entries(e.terms).filter(([,p])=>p).reduce((p,[c])=>`${p} ${c}`.trim(),""));let{tags:s}=ye();return x("a",{href:`${a}`,class:"md-search-result__link",tabIndex:-1},x("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&x("div",{class:"md-search-result__icon md-icon"}),r>0&&x("h1",null,e.title),r<=0&&x("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&e.tags.map(p=>{let c=s?p in s?`md-tag-icon md-tag--${s[p]}`:"md-tag-icon":"";return x("span",{class:`md-tag ${c}`},p)}),o>0&&n.length>0&&x("p",{class:"md-search-result__terms"},Ee("search.result.term.missing"),": ",...n)))}function Tn(e){let t=e[0].score,r=[...e],o=ye(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),a=r.findIndex(l=>l.scoreqr(l,1)),...p.length?[x("details",{class:"md-search-result__more"},x("summary",{tabIndex:-1},x("div",null,p.length>0&&p.length===1?Ee("search.result.more.one"):Ee("search.result.more.other",p.length))),...p.map(l=>qr(l,1)))]:[]];return x("li",{class:"md-search-result__item"},c)}function Sn(e){return x("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>x("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?sr(r):r)))}function Qr(e){let t=`tabbed-control tabbed-control--${e}`;return x("div",{class:t,hidden:!0},x("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function On(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Ca(e){var o;let t=ye(),r=new URL(`../${e.version}/`,t.base);return x("li",{class:"md-version__item"},x("a",{href:`${r}`,class:"md-version__link"},e.title,((o=t.version)==null?void 0:o.alias)&&e.aliases.length>0&&x("span",{class:"md-version__alias"},e.aliases[0])))}function Mn(e,t){var o;let r=ye();return e=e.filter(n=>{var i;return!((i=n.properties)!=null&&i.hidden)}),x("div",{class:"md-version"},x("button",{class:"md-version__current","aria-label":Ee("select.version")},t.title,((o=r.version)==null?void 0:o.alias)&&t.aliases.length>0&&x("span",{class:"md-version__alias"},t.aliases[0])),x("ul",{class:"md-version__list"},e.map(Ca)))}var Ha=0;function ka(e){let t=z([et(e),kt(e)]).pipe(m(([o,n])=>o||n),K()),r=C(()=>Jo(e)).pipe(oe(De),ct(1),m(()=>Xo(e)));return t.pipe(Ae(o=>o),v(()=>z([t,r])),m(([o,n])=>({active:o,offset:n})),pe())}function $a(e,t){let{content$:r,viewport$:o}=t,n=`__tooltip2_${Ha++}`;return C(()=>{let i=new g,a=new _r(!1);i.pipe(X(),ne(!1)).subscribe(a);let s=a.pipe(Ht(c=>Me(+!c*250,Hr)),K(),v(c=>c?r:O),E(c=>c.id=n),pe());z([i.pipe(m(({active:c})=>c)),s.pipe(v(c=>kt(c,250)),Q(!1))]).pipe(m(c=>c.some(l=>l))).subscribe(a);let p=a.pipe(b(c=>c),ee(s,o),m(([c,l,{size:f}])=>{let u=e.getBoundingClientRect(),h=u.width/2;if(l.role==="tooltip")return{x:h,y:8+u.height};if(u.y>=f.height/2){let{height:w}=ce(l);return{x:h,y:-16-w}}else return{x:h,y:16+u.height}}));return z([s,i,p]).subscribe(([c,{offset:l},f])=>{c.style.setProperty("--md-tooltip-host-x",`${l.x}px`),c.style.setProperty("--md-tooltip-host-y",`${l.y}px`),c.style.setProperty("--md-tooltip-x",`${f.x}px`),c.style.setProperty("--md-tooltip-y",`${f.y}px`),c.classList.toggle("md-tooltip2--top",f.y<0),c.classList.toggle("md-tooltip2--bottom",f.y>=0)}),a.pipe(b(c=>c),ee(s,(c,l)=>l),b(c=>c.role==="tooltip")).subscribe(c=>{let l=ce(P(":scope > *",c));c.style.setProperty("--md-tooltip-width",`${l.width}px`),c.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(K(),be(me),ee(s)).subscribe(([c,l])=>{l.classList.toggle("md-tooltip2--active",c)}),z([a.pipe(b(c=>c)),s]).subscribe(([c,l])=>{l.role==="dialog"?(e.setAttribute("aria-controls",n),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",n)}),a.pipe(b(c=>!c)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ka(e).pipe(E(c=>i.next(c)),L(()=>i.complete()),m(c=>R({ref:e},c)))})}function lt(e,{viewport$:t},r=document.body){return $a(e,{content$:new F(o=>{let n=e.title,i=yn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t})}function Pa(e,t){let r=C(()=>z([Zo(e),De(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:a,height:s}=ce(e);return{x:o-i.x+a/2,y:n-i.y+s/2}}));return et(e).pipe(v(o=>r.pipe(m(n=>({active:o,offset:n})),Te(+!o||1/0))))}function Ln(e,t,{target$:r}){let[o,n]=Array.from(e.children);return C(()=>{let i=new g,a=i.pipe(X(),ne(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),tt(e).pipe(U(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),S(i.pipe(b(({active:s})=>s)),i.pipe(_e(250),b(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe(Le(16,me)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(ct(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),d(n,"click").pipe(U(a),b(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),d(n,"mousedown").pipe(U(a),ee(i)).subscribe(([s,{active:p}])=>{var c;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(p){s.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(c=Re())==null||c.blur()}}),r.pipe(U(a),b(s=>s===o),Ge(125)).subscribe(()=>e.focus()),Pa(e,t).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))})}function Ra(e){return e.tagName==="CODE"?$(".c, .c1, .cm",e):[e]}function Ia(e){let t=[];for(let r of Ra(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,p]=a;if(typeof p=="undefined"){let c=i.splitText(a.index);i=c.splitText(s.length),t.push(c)}else{i.textContent=s,t.push(i);break}}}}return t}function _n(e,t){t.append(...Array.from(e.childNodes))}function fr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,a=new Map;for(let s of Ia(t)){let[,p]=s.textContent.match(/\((\d+)\)/);fe(`:scope > li:nth-child(${p})`,e)&&(a.set(p,En(p,i)),s.replaceWith(a.get(p)))}return a.size===0?O:C(()=>{let s=new g,p=s.pipe(X(),ne(!0)),c=[];for(let[l,f]of a)c.push([P(".md-typeset",f),P(`:scope > li:nth-child(${l})`,e)]);return o.pipe(U(p)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of c)l?_n(f,u):_n(u,f)}),S(...[...a].map(([,l])=>Ln(l,t,{target$:r}))).pipe(L(()=>s.complete()),pe())})}function An(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return An(t)}}function Cn(e,t){return C(()=>{let r=An(e);return typeof r!="undefined"?fr(r,e,t):O})}var Hn=Vt(Yr());var Fa=0;function kn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return kn(t)}}function ja(e){return ge(e).pipe(m(({width:t})=>({scrollable:Tt(e).width>t})),Z("scrollable"))}function $n(e,t){let{matches:r}=matchMedia("(hover)"),o=C(()=>{let n=new g,i=n.pipe(Fr(1));n.subscribe(({scrollable:c})=>{c&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[];if(Hn.default.isSupported()&&(e.closest(".copy")||B("content.code.copy")&&!e.closest(".no-copy"))){let c=e.closest("pre");c.id=`__code_${Fa++}`;let l=wn(c.id);c.insertBefore(l,e),B("content.tooltips")&&a.push(lt(l,{viewport$}))}let s=e.closest(".highlight");if(s instanceof HTMLElement){let c=kn(s);if(typeof c!="undefined"&&(s.classList.contains("annotate")||B("content.code.annotate"))){let l=fr(c,e,t);a.push(ge(s).pipe(U(i),m(({width:f,height:u})=>f&&u),K(),v(f=>f?l:O)))}}return $(":scope > span[id]",e).length&&e.classList.add("md-code__content"),ja(e).pipe(E(c=>n.next(c)),L(()=>n.complete()),m(c=>R({ref:e},c)),Pe(...a))});return B("content.lazy")?tt(e).pipe(b(n=>n),Te(1),v(()=>o)):o}function Wa(e,{target$:t,print$:r}){let o=!0;return S(t.pipe(m(n=>n.closest("details:not([open])")),b(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(b(n=>n||!o),E(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Pn(e,t){return C(()=>{let r=new g;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),Wa(e,t).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}var Rn=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel rect,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel rect{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color);stroke-width:.05rem}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs #classDiagram-compositionEnd,defs #classDiagram-compositionStart,defs #classDiagram-dependencyEnd,defs #classDiagram-dependencyStart,defs #classDiagram-extensionEnd,defs #classDiagram-extensionStart{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs #classDiagram-aggregationEnd,defs #classDiagram-aggregationStart{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}.attributeBoxEven,.attributeBoxOdd{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--md-mermaid-label-bg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var Br,Da=0;function Va(){return typeof mermaid=="undefined"||mermaid instanceof Element?wt("https://unpkg.com/mermaid@10/dist/mermaid.min.js"):I(void 0)}function In(e){return e.classList.remove("mermaid"),Br||(Br=Va().pipe(E(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Rn,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),G(1))),Br.subscribe(()=>ao(this,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${Da++}`,r=x("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),a=r.attachShadow({mode:"closed"});a.innerHTML=n,e.replaceWith(r),i==null||i(a)})),Br.pipe(m(()=>({ref:e})))}var Fn=x("table");function jn(e){return e.replaceWith(Fn),Fn.replaceWith(On(e)),I({ref:e})}function Na(e){let t=e.find(r=>r.checked)||e[0];return S(...e.map(r=>d(r,"change").pipe(m(()=>P(`label[for="${r.id}"]`))))).pipe(Q(P(`label[for="${t.id}"]`)),m(r=>({active:r})))}function Wn(e,{viewport$:t,target$:r}){let o=P(".tabbed-labels",e),n=$(":scope > input",e),i=Qr("prev");e.append(i);let a=Qr("next");return e.append(a),C(()=>{let s=new g,p=s.pipe(X(),ne(!0));z([s,ge(e),tt(e)]).pipe(U(p),Le(1,me)).subscribe({next([{active:c},l]){let f=Ue(c),{width:u}=ce(c);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let h=pr(o);(f.xh.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([De(o),ge(o)]).pipe(U(p)).subscribe(([c,l])=>{let f=Tt(o);i.hidden=c.x<16,a.hidden=c.x>f.width-l.width-16}),S(d(i,"click").pipe(m(()=>-1)),d(a,"click").pipe(m(()=>1))).pipe(U(p)).subscribe(c=>{let{width:l}=ce(o);o.scrollBy({left:l*c,behavior:"smooth"})}),r.pipe(U(p),b(c=>n.includes(c))).subscribe(c=>c.click()),o.classList.add("tabbed-labels--linked");for(let c of n){let l=P(`label[for="${c.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),d(l.firstElementChild,"click").pipe(U(p),b(f=>!(f.metaKey||f.ctrlKey)),E(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return B("content.tabs.link")&&s.pipe(Ce(1),ee(t)).subscribe(([{active:c},{offset:l}])=>{let f=c.innerText.trim();if(c.hasAttribute("data-md-switching"))c.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let w of $("[data-tabs]"))for(let A of $(":scope > input",w)){let te=P(`label[for="${A.id}"]`);if(te!==c&&te.innerText.trim()===f){te.setAttribute("data-md-switching",""),A.click();break}}window.scrollTo({top:e.offsetTop-u});let h=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...h])])}}),s.pipe(U(p)).subscribe(()=>{for(let c of $("audio, video",e))c.pause()}),Na(n).pipe(E(c=>s.next(c)),L(()=>s.complete()),m(c=>R({ref:e},c)))}).pipe(Qe(se))}function Un(e,{viewport$:t,target$:r,print$:o}){return S(...$(".annotate:not(.highlight)",e).map(n=>Cn(n,{target$:r,print$:o})),...$("pre:not(.mermaid) > code",e).map(n=>$n(n,{target$:r,print$:o})),...$("pre.mermaid",e).map(n=>In(n)),...$("table:not([class])",e).map(n=>jn(n)),...$("details",e).map(n=>Pn(n,{target$:r,print$:o})),...$("[data-tabs]",e).map(n=>Wn(n,{viewport$:t,target$:r})),...$("[title]",e).filter(()=>B("content.tooltips")).map(n=>lt(n,{viewport$:t})))}function za(e,{alert$:t}){return t.pipe(v(r=>S(I(!0),I(!1).pipe(Ge(2e3))).pipe(m(o=>({message:r,active:o})))))}function Dn(e,t){let r=P(".md-typeset",e);return C(()=>{let o=new g;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),za(e,t).pipe(E(n=>o.next(n)),L(()=>o.complete()),m(n=>R({ref:e},n)))})}var qa=0;function Qa(e,t){document.body.append(e);let{width:r}=ce(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=cr(t),n=typeof o!="undefined"?De(o):I({x:0,y:0}),i=S(et(t),kt(t)).pipe(K());return z([i,n]).pipe(m(([a,s])=>{let{x:p,y:c}=Ue(t),l=ce(t),f=t.closest("table");return f&&t.parentElement&&(p+=f.offsetLeft+t.parentElement.offsetLeft,c+=f.offsetTop+t.parentElement.offsetTop),{active:a,offset:{x:p-s.x+l.width/2-r/2,y:c-s.y+l.height+8}}}))}function Vn(e){let t=e.title;if(!t.length)return O;let r=`__tooltip_${qa++}`,o=Pt(r,"inline"),n=P(".md-typeset",o);return n.innerHTML=t,C(()=>{let i=new g;return i.subscribe({next({offset:a}){o.style.setProperty("--md-tooltip-x",`${a.x}px`),o.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),S(i.pipe(b(({active:a})=>a)),i.pipe(_e(250),b(({active:a})=>!a))).subscribe({next({active:a}){a?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe(Le(16,me)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(ct(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?o.style.setProperty("--md-tooltip-0",`${-a}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),Qa(o,e).pipe(E(a=>i.next(a)),L(()=>i.complete()),m(a=>R({ref:e},a)))}).pipe(Qe(se))}function Ka({viewport$:e}){if(!B("header.autohide"))return I(!1);let t=e.pipe(m(({offset:{y:n}})=>n),Ye(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),K()),o=Ve("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),K(),v(n=>n?r:I(!1)),Q(!1))}function Nn(e,t){return C(()=>z([ge(e),Ka(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),K((r,o)=>r.height===o.height&&r.hidden===o.hidden),G(1))}function zn(e,{header$:t,main$:r}){return C(()=>{let o=new g,n=o.pipe(X(),ne(!0));o.pipe(Z("active"),We(t)).subscribe(([{active:a},{hidden:s}])=>{e.classList.toggle("md-header--shadow",a&&!s),e.hidden=s});let i=ue($("[title]",e)).pipe(b(()=>B("content.tooltips")),oe(a=>Vn(a)));return r.subscribe(o),t.pipe(U(n),m(a=>R({ref:e},a)),Pe(i.pipe(U(n))))})}function Ya(e,{viewport$:t,header$:r}){return mr(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=ce(e);return{active:o>=n}}),Z("active"))}function qn(e,t){return C(()=>{let r=new g;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=fe(".md-content h1");return typeof o=="undefined"?O:Ya(o,t).pipe(E(n=>r.next(n)),L(()=>r.complete()),m(n=>R({ref:e},n)))})}function Qn(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),K()),n=o.pipe(v(()=>ge(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),Z("bottom"))));return z([o,n,t]).pipe(m(([i,{top:a,bottom:s},{offset:{y:p},size:{height:c}}])=>(c=Math.max(0,c-Math.max(0,a-p,i)-Math.max(0,c+p-s)),{offset:a-i,height:c,active:a-i<=p})),K((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function Ba(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return I(...e).pipe(oe(o=>d(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),G(1))}function Kn(e){let t=$("input",e),r=x("meta",{name:"theme-color"});document.head.appendChild(r);let o=x("meta",{name:"color-scheme"});document.head.appendChild(o);let n=$t("(prefers-color-scheme: light)");return C(()=>{let i=new g;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),p=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=p.getAttribute("data-md-color-scheme"),a.color.primary=p.getAttribute("data-md-color-primary"),a.color.accent=p.getAttribute("data-md-color-accent")}for(let[s,p]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,p);for(let s=0;sa.key==="Enter"),ee(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(m(()=>{let a=Se("header"),s=window.getComputedStyle(a);return o.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(p=>(+p).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(be(se)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),Ba(t).pipe(U(n.pipe(Ce(1))),st(),E(a=>i.next(a)),L(()=>i.complete()),m(a=>R({ref:e},a)))})}function Yn(e,{progress$:t}){return C(()=>{let r=new g;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(E(o=>r.next({value:o})),L(()=>r.complete()),m(o=>({ref:e,value:o})))})}var Gr=Vt(Yr());function Ga(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function Bn({alert$:e}){Gr.default.isSupported()&&new F(t=>{new Gr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Ga(P(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(E(t=>{t.trigger.focus()}),m(()=>Ee("clipboard.copied"))).subscribe(e)}function Gn(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function Ja(e,t){let r=new Map;for(let o of $("url",e)){let n=P("loc",o),i=[Gn(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let a of $("[rel=alternate]",o)){let s=a.getAttribute("href");s!=null&&i.push(Gn(new URL(s),t))}}return r}function ur(e){return mn(new URL("sitemap.xml",e)).pipe(m(t=>Ja(t,new URL(e))),ve(()=>I(new Map)))}function Xa(e,t){if(!(e.target instanceof Element))return O;let r=e.target.closest("a");if(r===null)return O;if(r.target||e.metaKey||e.ctrlKey)return O;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),I(new URL(r.href))):O}function Jn(e){let t=new Map;for(let r of $(":scope > *",e.head))t.set(r.outerHTML,r);return t}function Xn(e){for(let t of $("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return I(e)}function Za(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...B("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=fe(o),i=fe(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=Jn(document);for(let[o,n]of Jn(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Se("container");return je($("script",r)).pipe(v(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new F(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),O}),X(),ne(document))}function Zn({location$:e,viewport$:t,progress$:r}){let o=ye();if(location.protocol==="file:")return O;let n=ur(o.base);I(document).subscribe(Xn);let i=d(document.body,"click").pipe(We(n),v(([p,c])=>Xa(p,c)),pe()),a=d(window,"popstate").pipe(m(xe),pe());i.pipe(ee(t)).subscribe(([p,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",p)}),S(i,a).subscribe(e);let s=e.pipe(Z("pathname"),v(p=>ln(p,{progress$:r}).pipe(ve(()=>(pt(p,!0),O)))),v(Xn),v(Za),pe());return S(s.pipe(ee(e,(p,c)=>c)),s.pipe(v(()=>e),Z("pathname"),v(()=>e),Z("hash")),e.pipe(K((p,c)=>p.pathname===c.pathname&&p.hash===c.hash),v(()=>i),E(()=>history.back()))).subscribe(p=>{var c,l;history.state!==null||!p.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",sn(p.hash),history.scrollRestoration="manual")}),e.subscribe(()=>{history.scrollRestoration="manual"}),d(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),t.pipe(Z("offset"),_e(100)).subscribe(({offset:p})=>{history.replaceState(p,"")}),s}var ri=Vt(ti());function oi(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,a)=>`${i}${a}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return a=>(0,ri.default)(a).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function It(e){return e.type===1}function dr(e){return e.type===3}function ni(e,t){let r=vn(e);return S(I(location.protocol!=="file:"),Ve("search")).pipe(Ae(o=>o),v(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:B("search.suggest")}}})),r}function ii({document$:e}){let t=ye(),r=Ne(new URL("../versions.json",t.base)).pipe(ve(()=>O)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:a,aliases:s})=>a===i||s.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),v(n=>d(document.body,"click").pipe(b(i=>!i.metaKey&&!i.ctrlKey),ee(o),v(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&n.has(s.href)){let p=s.href;return!i.target.closest(".md-version")&&n.get(p)===a?O:(i.preventDefault(),I(p))}}return O}),v(i=>ur(new URL(i)).pipe(m(a=>{let p=xe().href.replace(t.base,i);return a.has(p.split("#")[0])?new URL(p):new URL(i)})))))).subscribe(n=>pt(n,!0)),z([r,o]).subscribe(([n,i])=>{P(".md-header__topic").appendChild(Mn(n,i))}),e.pipe(v(()=>o)).subscribe(n=>{var a;let i=__md_get("__outdated",sessionStorage);if(i===null){i=!0;let s=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(s)||(s=[s]);e:for(let p of s)for(let c of n.aliases.concat(n.version))if(new RegExp(p,"i").test(c)){i=!1;break e}__md_set("__outdated",i,sessionStorage)}if(i)for(let s of ae("outdated"))s.hidden=!1})}function ns(e,{worker$:t}){let{searchParams:r}=xe();r.has("q")&&(Je("search",!0),e.value=r.get("q"),e.focus(),Ve("search").pipe(Ae(i=>!i)).subscribe(()=>{let i=xe();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=et(e),n=S(t.pipe(Ae(It)),d(e,"keyup"),o).pipe(m(()=>e.value),K());return z([n,o]).pipe(m(([i,a])=>({value:i,focus:a})),G(1))}function ai(e,{worker$:t}){let r=new g,o=r.pipe(X(),ne(!0));z([t.pipe(Ae(It)),r],(i,a)=>a).pipe(Z("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(Z("focus")).subscribe(({focus:i})=>{i&&Je("search",i)}),d(e.form,"reset").pipe(U(o)).subscribe(()=>e.focus());let n=P("header [for=__search]");return d(n,"click").subscribe(()=>e.focus()),ns(e,{worker$:t}).pipe(E(i=>r.next(i)),L(()=>r.complete()),m(i=>R({ref:e},i)),G(1))}function si(e,{worker$:t,query$:r}){let o=new g,n=tn(e.parentElement).pipe(b(Boolean)),i=e.parentElement,a=P(":scope > :first-child",e),s=P(":scope > :last-child",e);Ve("search").subscribe(l=>s.setAttribute("role",l?"list":"presentation")),o.pipe(ee(r),Ur(t.pipe(Ae(It)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:a.textContent=f.length?Ee("search.result.none"):Ee("search.result.placeholder");break;case 1:a.textContent=Ee("search.result.one");break;default:let u=sr(l.length);a.textContent=Ee("search.result.other",u)}});let p=o.pipe(E(()=>s.innerHTML=""),v(({items:l})=>S(I(...l.slice(0,10)),I(...l.slice(10)).pipe(Ye(4),Vr(n),v(([f])=>f)))),m(Tn),pe());return p.subscribe(l=>s.appendChild(l)),p.pipe(oe(l=>{let f=fe("details",l);return typeof f=="undefined"?O:d(f,"toggle").pipe(U(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(b(dr),m(({data:l})=>l)).pipe(E(l=>o.next(l)),L(()=>o.complete()),m(l=>R({ref:e},l)))}function is(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=xe();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function ci(e,t){let r=new g,o=r.pipe(X(),ne(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),d(e,"click").pipe(U(o)).subscribe(n=>n.preventDefault()),is(e,t).pipe(E(n=>r.next(n)),L(()=>r.complete()),m(n=>R({ref:e},n)))}function pi(e,{worker$:t,keyboard$:r}){let o=new g,n=Se("search-query"),i=S(d(n,"keydown"),d(n,"focus")).pipe(be(se),m(()=>n.value),K());return o.pipe(We(i),m(([{suggest:s},p])=>{let c=p.split(/([\s-]+)/);if(s!=null&&s.length&&c[c.length-1]){let l=s[s.length-1];l.startsWith(c[c.length-1])&&(c[c.length-1]=l)}else c.length=0;return c})).subscribe(s=>e.innerHTML=s.join("").replace(/\s/g," ")),r.pipe(b(({mode:s})=>s==="search")).subscribe(s=>{switch(s.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(b(dr),m(({data:s})=>s)).pipe(E(s=>o.next(s)),L(()=>o.complete()),m(()=>({ref:e})))}function li(e,{index$:t,keyboard$:r}){let o=ye();try{let n=ni(o.search,t),i=Se("search-query",e),a=Se("search-result",e);d(e,"click").pipe(b(({target:p})=>p instanceof Element&&!!p.closest("a"))).subscribe(()=>Je("search",!1)),r.pipe(b(({mode:p})=>p==="search")).subscribe(p=>{let c=Re();switch(p.type){case"Enter":if(c===i){let l=new Map;for(let f of $(":first-child [href]",a)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,h])=>h-u);f.click()}p.claim()}break;case"Escape":case"Tab":Je("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof c=="undefined")i.focus();else{let l=[i,...$(":not(details) > [href], summary, details[open] [href]",a)],f=Math.max(0,(Math.max(0,l.indexOf(c))+l.length+(p.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}p.claim();break;default:i!==Re()&&i.focus()}}),r.pipe(b(({mode:p})=>p==="global")).subscribe(p=>{switch(p.type){case"f":case"s":case"/":i.focus(),i.select(),p.claim();break}});let s=ai(i,{worker$:n});return S(s,si(a,{worker$:n,query$:s})).pipe(Pe(...ae("search-share",e).map(p=>ci(p,{query$:s})),...ae("search-suggest",e).map(p=>pi(p,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,Ke}}function mi(e,{index$:t,location$:r}){return z([t,r.pipe(Q(xe()),b(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>oi(o.config)(n.searchParams.get("h"))),m(o=>{var a;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let p=s.textContent,c=o(p);c.length>p.length&&n.set(s,c)}for(let[s,p]of n){let{childNodes:c}=x("span",null,p);s.replaceWith(...Array.from(c))}return{ref:e,nodes:n}}))}function as(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(n,Math.max(0,s-i))-n,{height:a,locked:s>=i+n})),K((i,a)=>i.height===a.height&&i.locked===a.locked))}function Jr(e,o){var n=o,{header$:t}=n,r=io(n,["header$"]);let i=P(".md-sidebar__scrollwrap",e),{y:a}=Ue(i);return C(()=>{let s=new g,p=s.pipe(X(),ne(!0)),c=s.pipe(Le(0,me));return c.pipe(ee(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*a}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),c.pipe(Ae()).subscribe(()=>{for(let l of $(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:h}=ce(f);f.scrollTo({top:u-h/2})}}}),ue($("label[tabindex]",e)).pipe(oe(l=>d(l,"click").pipe(be(se),m(()=>l),U(p)))).subscribe(l=>{let f=P(`[id="${l.htmlFor}"]`);P(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),as(e,r).pipe(E(l=>s.next(l)),L(()=>s.complete()),m(l=>R({ref:e},l)))})}function fi(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return Ct(Ne(`${r}/releases/latest`).pipe(ve(()=>O),m(o=>({version:o.tag_name})),Be({})),Ne(r).pipe(ve(()=>O),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),Be({}))).pipe(m(([o,n])=>R(R({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return Ne(r).pipe(m(o=>({repositories:o.public_repos})),Be({}))}}function ui(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return Ne(r).pipe(ve(()=>O),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),Be({}))}function di(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return fi(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return ui(r,o)}return O}var ss;function cs(e){return ss||(ss=C(()=>{let t=__md_get("__source",sessionStorage);if(t)return I(t);if(ae("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return O}return di(e.href).pipe(E(o=>__md_set("__source",o,sessionStorage)))}).pipe(ve(()=>O),b(t=>Object.keys(t).length>0),m(t=>({facts:t})),G(1)))}function hi(e){let t=P(":scope > :last-child",e);return C(()=>{let r=new g;return r.subscribe(({facts:o})=>{t.appendChild(Sn(o)),t.classList.add("md-source__repository--active")}),cs(e).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}function ps(e,{viewport$:t,header$:r}){return ge(document.body).pipe(v(()=>mr(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),Z("hidden"))}function bi(e,t){return C(()=>{let r=new g;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(B("navigation.tabs.sticky")?I({hidden:!1}):ps(e,t)).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}function ls(e,{viewport$:t,header$:r}){let o=new Map,n=$(".md-nav__link",e);for(let s of n){let p=decodeURIComponent(s.hash.substring(1)),c=fe(`[id="${p}"]`);typeof c!="undefined"&&o.set(s,c)}let i=r.pipe(Z("height"),m(({height:s})=>{let p=Se("main"),c=P(":scope > :first-child",p);return s+.8*(c.offsetTop-p.offsetTop)}),pe());return ge(document.body).pipe(Z("height"),v(s=>C(()=>{let p=[];return I([...o].reduce((c,[l,f])=>{for(;p.length&&o.get(p[p.length-1]).tagName>=f.tagName;)p.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let h=f.offsetParent;for(;h;h=h.offsetParent)u+=h.offsetTop;return c.set([...p=[...p,l]].reverse(),u)},new Map))}).pipe(m(p=>new Map([...p].sort(([,c],[,l])=>c-l))),We(i),v(([p,c])=>t.pipe(jr(([l,f],{offset:{y:u},size:h})=>{let w=u+h.height>=Math.floor(s.height);for(;f.length;){let[,A]=f[0];if(A-c=u&&!w)f=[l.pop(),...f];else break}return[l,f]},[[],[...p]]),K((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([s,p])=>({prev:s.map(([c])=>c),next:p.map(([c])=>c)})),Q({prev:[],next:[]}),Ye(2,1),m(([s,p])=>s.prev.length{let i=new g,a=i.pipe(X(),ne(!0));if(i.subscribe(({prev:s,next:p})=>{for(let[c]of p)c.classList.remove("md-nav__link--passed"),c.classList.remove("md-nav__link--active");for(let[c,[l]]of s.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",c===s.length-1)}),B("toc.follow")){let s=S(t.pipe(_e(1),m(()=>{})),t.pipe(_e(250),m(()=>"smooth")));i.pipe(b(({prev:p})=>p.length>0),We(o.pipe(be(se))),ee(s)).subscribe(([[{prev:p}],c])=>{let[l]=p[p.length-1];if(l.offsetHeight){let f=cr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:h}=ce(f);f.scrollTo({top:u-h/2,behavior:c})}}})}return B("navigation.tracking")&&t.pipe(U(a),Z("offset"),_e(250),Ce(1),U(n.pipe(Ce(1))),st({delay:250}),ee(i)).subscribe(([,{prev:s}])=>{let p=xe(),c=s[s.length-1];if(c&&c.length){let[l]=c,{hash:f}=new URL(l.href);p.hash!==f&&(p.hash=f,history.replaceState({},"",`${p}`))}else p.hash="",history.replaceState({},"",`${p}`)}),ls(e,{viewport$:t,header$:r}).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))})}function ms(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:a}})=>a),Ye(2,1),m(([a,s])=>a>s&&s>0),K()),i=r.pipe(m(({active:a})=>a));return z([i,n]).pipe(m(([a,s])=>!(a&&s)),K(),U(o.pipe(Ce(1))),ne(!0),st({delay:250}),m(a=>({hidden:a})))}function gi(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new g,a=i.pipe(X(),ne(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(U(a),Z("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),d(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),ms(e,{viewport$:t,main$:o,target$:n}).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))}function xi({document$:e,viewport$:t}){e.pipe(v(()=>$(".md-ellipsis")),oe(r=>tt(r).pipe(U(e.pipe(Ce(1))),b(o=>o),m(()=>r),Te(1))),b(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,B("content.tooltips")?lt(n,{viewport$:t}).pipe(U(e.pipe(Ce(1))),L(()=>n.removeAttribute("title"))):O})).subscribe(),B("content.tooltips")&&e.pipe(v(()=>$(".md-status")),oe(r=>lt(r,{viewport$:t}))).subscribe()}function yi({document$:e,tablet$:t}){e.pipe(v(()=>$(".md-toggle--indeterminate")),E(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>d(r,"change").pipe(Dr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),ee(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function fs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Ei({document$:e}){e.pipe(v(()=>$("[data-md-scrollfix]")),E(t=>t.removeAttribute("data-md-scrollfix")),b(fs),oe(t=>d(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function wi({viewport$:e,tablet$:t}){z([Ve("search"),t]).pipe(m(([r,o])=>r&&!o),v(r=>I(r).pipe(Ge(r?400:100))),ee(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function us(){return location.protocol==="file:"?wt(`${new URL("search/search_index.js",Xr.base)}`).pipe(m(()=>__index),G(1)):Ne(new URL("search/search_index.json",Xr.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ot=Yo(),jt=nn(),Ot=cn(jt),Zr=on(),Oe=bn(),hr=$t("(min-width: 960px)"),Si=$t("(min-width: 1220px)"),Oi=pn(),Xr=ye(),Mi=document.forms.namedItem("search")?us():Ke,eo=new g;Bn({alert$:eo});var to=new g;B("navigation.instant")&&Zn({location$:jt,viewport$:Oe,progress$:to}).subscribe(ot);var Ti;((Ti=Xr.version)==null?void 0:Ti.provider)==="mike"&&ii({document$:ot});S(jt,Ot).pipe(Ge(125)).subscribe(()=>{Je("drawer",!1),Je("search",!1)});Zr.pipe(b(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=fe("link[rel=prev]");typeof t!="undefined"&&pt(t);break;case"n":case".":let r=fe("link[rel=next]");typeof r!="undefined"&&pt(r);break;case"Enter":let o=Re();o instanceof HTMLLabelElement&&o.click()}});xi({viewport$:Oe,document$:ot});yi({document$:ot,tablet$:hr});Ei({document$:ot});wi({viewport$:Oe,tablet$:hr});var rt=Nn(Se("header"),{viewport$:Oe}),Ft=ot.pipe(m(()=>Se("main")),v(e=>Qn(e,{viewport$:Oe,header$:rt})),G(1)),ds=S(...ae("consent").map(e=>xn(e,{target$:Ot})),...ae("dialog").map(e=>Dn(e,{alert$:eo})),...ae("header").map(e=>zn(e,{viewport$:Oe,header$:rt,main$:Ft})),...ae("palette").map(e=>Kn(e)),...ae("progress").map(e=>Yn(e,{progress$:to})),...ae("search").map(e=>li(e,{index$:Mi,keyboard$:Zr})),...ae("source").map(e=>hi(e))),hs=C(()=>S(...ae("announce").map(e=>gn(e)),...ae("content").map(e=>Un(e,{viewport$:Oe,target$:Ot,print$:Oi})),...ae("content").map(e=>B("search.highlight")?mi(e,{index$:Mi,location$:jt}):O),...ae("header-title").map(e=>qn(e,{viewport$:Oe,header$:rt})),...ae("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?Nr(Si,()=>Jr(e,{viewport$:Oe,header$:rt,main$:Ft})):Nr(hr,()=>Jr(e,{viewport$:Oe,header$:rt,main$:Ft}))),...ae("tabs").map(e=>bi(e,{viewport$:Oe,header$:rt})),...ae("toc").map(e=>vi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Ot})),...ae("top").map(e=>gi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Ot})))),Li=ot.pipe(v(()=>hs),Pe(ds),G(1));Li.subscribe();window.document$=ot;window.location$=jt;window.target$=Ot;window.keyboard$=Zr;window.viewport$=Oe;window.tablet$=hr;window.screen$=Si;window.print$=Oi;window.alert$=eo;window.progress$=to;window.component$=Li;})(); +//# sourceMappingURL=bundle.fe8b6f2b.min.js.map + diff --git a/assets/javascripts/bundle.fe8b6f2b.min.js.map b/assets/javascripts/bundle.fe8b6f2b.min.js.map new file mode 100644 index 00000000..82635852 --- /dev/null +++ b/assets/javascripts/bundle.fe8b6f2b.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/clipboard/dist/clipboard.js", "node_modules/escape-html/index.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/rxjs/node_modules/tslib/tslib.es6.js", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 960px)\")\nconst screen$ = watchMedia(\"(min-width: 1220px)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n if (typeof b !== \"function\" && b !== null)\r\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n}\r\n\r\nexport function __spreadArray(to, from, pack) {\r\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\r\n if (ar || !(i in from)) {\r\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\r\n ar[i] = from[i];\r\n }\r\n }\r\n return to.concat(ar || Array.prototype.slice.call(from));\r\n}\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\r\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\r\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\r\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\r\n}\r\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n *\n * @class Subscription\n */\nexport class Subscription implements SubscriptionLike {\n /** @nocollapse */\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n * @return {void}\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n *\n * @class Subscriber\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @nocollapse\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param {T} [value] The `next` value.\n * @return {void}\n */\n next(value?: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param {any} [err] The `error` exception.\n * @return {void}\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n * @return {void}\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as (((value: T) => void) | undefined),\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent\n * @param subscriber The stopped subscriber\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n *\n * @class Observable\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @constructor\n * @param {Function} subscribe the function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @owner Observable\n * @method create\n * @param {Function} subscribe? the subscriber function to be passed to the Observable constructor\n * @return {Observable} a new observable\n * @nocollapse\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @method lift\n * @param operator the operator defining the operation to take on the observable\n * @return a new observable with the Operator applied\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param {Observer|Function} observerOrNext (optional) Either an observer with methods to be called,\n * or the first of three possible handlers, which is the handler for each value emitted from the subscribed\n * Observable.\n * @param {Function} error (optional) A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param {Function} complete (optional) A handler for a terminal event resulting from successful completion.\n * @return {Subscription} a subscription reference to the registered handlers\n * @method subscribe\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next a handler for each value emitted by the observable\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @method Symbol.observable\n * @return {Observable} this instance of the observable\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n * @method pipe\n * @return {Observable} the Observable result of all of the operators having\n * been called in the order they were passed in.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @method toPromise\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @nocollapse\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return {Observable} Observable that the Subject casts to\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\n/**\n * @class AnonymousSubject\n */\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n *\n * @class BehaviorSubject\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param bufferSize The size of the buffer to replay on subscription\n * @param windowTime The amount of time the buffered items will stay buffered\n * @param timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n *\n * @class Action\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler.\n * @return {void}\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n * @return {any}\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @class Scheduler\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return {number} A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param {function(state: ?T): ?Subscription} work A function representing a\n * task, or some unit of work to be executed by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler itself.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @return {Subscription} A subscription in order to be able to unsubscribe\n * the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @type {boolean}\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @type {any}\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n const flushId = this._scheduled;\n this._scheduled = undefined;\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an + +

Community Roles

+

Administrative Roles

+

Meeting Facilitators: Karen Majewicz + 1 vacancy

+
    +
  • Manage Google calendar
  • +
  • Set up and host monthly meetings
  • +
  • Prepare agendas
  • +
+

Platform Management Team: 2 vacancies

+
    +
  • Manage GitHub users and roles
  • +
  • Manage Google group permissions, roles, etc.
  • +
  • Manage Google Drive members
  • +
  • Maintain Slack
  • +
+

Documentation Roles

+

Public Content Manager: 1 vacancy

+
    +
  • Keep the public website(s) up to date
  • +
  • Write periodic blog posts, including sprint wrap-up posts
  • +
+

Technical Writing Lead: 1 vacancy

+
    +
  • Keep track of what code, metadata, and tutorials need updating
  • +
  • Manage GitHub issues related to documentation
  • +
  • Coordinate volunteers to update documentation as necessary
  • +
+

Technical Roles

+
+
    +
  • Members of the GeoBlacklight Developers team in GitHub
  • +
  • Review pull requests on behalf of the team, or assign individual code reviewers based on expertise
  • +
+

Release Coordinators: Eric Larson + 1 vacancy

+
    +
  • Cut tags and releases in the GitHub repository, and write release notes
  • +
  • Publish the GeoBlacklight Ruby gem
  • +
  • Publish Javascript to npm
  • +
+

Workgroups and Interest Groups

+
+
    +
  • +

    Issue Triage Workgroup

    +

    Meets quarterly to review and prioritize GitHub issues related to code.

    +
      +
    • Eliot Jordan
    • +
    • Eric Larson
    • +
    • Karen Majewicz
    • +
    • Keith Jenkins
    • +
    • Maura Carbone
    • +
    +
  • +
  • +

    Governance Workgroup

    +

    Improves our communication around change management, connected projects, and community roles.

    +
      +
    • Karen Majewicz
    • +
    • Becky Seifried
    • +
    • Eliot Jordan
    • +
    +
  • +
  • +

    Georeferencing Interest Group

    +

    Discusses georeferencing projects and strategies to enhance GeoBlacklight’s georeferenced map capabilities.

    +
      +
    • Adam Cox
    • +
    • Alex Marden
    • +
    • Eliot Jordan
    • +
    • Kim Leaman
    • +
    • Marc McGee
    • +
    • Michael Shensky
    • +
    • Stephen Appel
    • +
    +
  • +
+
+

GeoBlacklight Institutional Contributors

+

GeoBlacklight has been collaboratively developed by affiliates of the following institutions:

+
    +
  • Auraria Library
  • +
  • Big Ten Academic Alliance
  • +
  • Cornell University
  • +
  • MIT Libraries
  • +
  • Harvard University Library
  • +
  • Johns Hopkins University
  • +
  • New York University Libraries
  • +
  • Princeton University Library
  • +
  • Stanford University Libraries
  • +
  • University of California San Diego
  • +
  • University of California Berkeley
  • +
  • University of California Santa Barbara
  • +
  • University of Illinois at Urbana-Champaign
  • +
  • University of Massachusetts Amherst
  • +
  • University of Minnesota Libraries
  • +
  • University of Pennsylvania Libraries
  • +
  • The University of Texas at Austin
  • +
  • University of Wisconsin-Milwaukee
  • +
+
+

Feedback

+

Did we miss your institution? Have a suggestion for this website? Create an issue on the GeoBlacklight Website Github page here.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/adding_mirador_viewer/index.html b/docs/adding_mirador_viewer/index.html new file mode 100644 index 00000000..282d6d40 --- /dev/null +++ b/docs/adding_mirador_viewer/index.html @@ -0,0 +1,2295 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Add Mirador IIIF Viewer - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Add Mirador IIIF Manifest Viewer

+

What is Mirador?

+

Mirador is an open source IIIF image and IIIF manifest viewer. Core GeoBlacklight contributors have contributed significantly to Mirador. Learn more on the Project Mirador website.

+

Install

+

Add the Mirador 3 javascript and stylesheet assets to your project. If you are using Yarn, you can just add mirador via:

+
yarn add mirador
+
+

Or you can edit your package.json file like so, and run:

+
  ...
+  "dependencies": {
+    "@babel/cli": "^7.5.5",
+    "@babel/core": "^7.5.5",
+    "@rails/webpacker": "^4.0.7",
+    "babel-loader": "^8.0.6",
+    "bloodhound-js": "^1.2.3",
+    "bootstrap": "^4.3.1",
+    "jquery": "^3.4.0",
+    "mirador": "^3.3.0",
+    "readmore-js": "^3.0.0-beta-1",
+    "typeahead.js": "^0.11.1",
+    "lodash": "^4.17.13",
+    "lodash.template": "^4.5.0"
+  }
+
+
yarn install
+
+

Configure

+

With Mirador installed, you need to add the javascript library to your application.

+

application.js +

// Mirador
+//= require mirador/dist/mirador.min.js
+

+

Add a GeoBlacklight Viewer

+

Within app/assets/javascripts/geoblacklight/viewers add a new iiif_manifest.js viewer, and specify your Mirador configuration values.

+
//= require geoblacklight/viewers/viewer
+
+GeoBlacklight.Viewer.IiifManifest = GeoBlacklight.Viewer.extend({
+  load: function() {
+    var manifest_uri = document.getElementById('map').getAttribute('data-url');
+
+    var miradorInstance = Mirador.viewer({
+       id: 'map',
+       themes: {
+         light: {
+           palette: {
+             type: 'light',
+             primary: {
+               main: '#0088ce',
+             },
+           },
+         },
+       },
+       windows: [{
+         manifestId: manifest_uri,
+         thumbnailNavigationPosition: 'far-bottom',
+       }],
+       window: {
+         hideSearchPanel: false,
+         hideWindowTitle: true,
+         hideAnnotationsPanel: true,
+         allowClose: false,
+         allowMaximize: false,
+         allowFullscreen: true,
+       },
+       workspace: {
+         showZoomControls: true,
+       },
+       workspaceControlPanel: {
+         enabled: false,
+       }
+     });
+  }
+});
+
+

Override the GeoBlacklight ItemViewer to add support for your iiif_manifest viewer. In the B1G Geoportal we keep our local ItemViewer customizations in config/initializers/item_viewer.rb

+
module Geoblacklight
+  class ItemViewer
+    def initialize(references)
+      @references = references
+    end
+
+    def viewer_protocol
+      return 'map' if viewer_preference.nil?
+      viewer_preference.keys.first.to_s
+    end
+
+    def viewer_endpoint
+      return '' if viewer_preference.nil?
+      viewer_preference.values.first.to_s
+    end
+
+    def wms
+      @references.wms
+    end
+
+    def iiif
+      @references.iiif
+    end
+
+    # HERE - Added viewer
+    def iiif_manifest
+      @references.iiif_manifest
+    end
+
+    def tiled_map_layer
+      @references.tiled_map_layer
+    end
+
+    def dynamic_map_layer
+      @references.dynamic_map_layer
+    end
+
+    def feature_layer
+      @references.feature_layer
+    end
+
+    def image_map_layer
+      @references.image_map_layer
+    end
+
+    def index_map
+      @references.index_map
+    end
+
+    # HERE - also need to specify viewer preference
+    def viewer_preference
+      [index_map, wms, iiif, iiif_manifest, tiled_map_layer, dynamic_map_layer,
+       image_map_layer, feature_layer].compact.map(&:to_hash).first
+    end
+  end
+end
+
+

Enjoy!

+

Add a IIIF Manifest-based fixture to your spec fixtures and reload the application (rake geoblacklight:server).

+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/adding_svg_icons/index.html b/docs/adding_svg_icons/index.html new file mode 100644 index 00000000..193bb134 --- /dev/null +++ b/docs/adding_svg_icons/index.html @@ -0,0 +1,2160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Add SVG Icons - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

SVG Icons

+

Add new SVGs to GeoBlacklight or your local GBL application:

+
    +
  1. Add your new or replacement SVG icon into the /app/assets/images/blacklight directory
  2. +
  3. Add an :en I18n translation entry for the SVG icon in /config/locales/geoblacklight.en.yml, following this pattern:
  4. +
+
    ...
+      blacklight:
+        icon:
+          arrow-circle-down: Arrow within a circle, pointing down
+          baruch-cuny: Baruch College
+          berkeley: University of California, Berkeley
+          your-new-icon-filename-without-the-extension: Your new icon description
+
+

Render your new SVG icon using the blacklight_icon helper like so:

+
<%= blacklight_icon('icon-filename') %>
+
+

SVG Icon Maintenance

+

The stock GeoBlacklight SVG icons come from institutional partners and Font Awesome. The IcoMoon App was used to generate a working project board of icons.

+

To view the IcoMoon project board:

+
    +
  1. Visit https://icomoon.io/app/#/select
  2. +
  3. Drag and drop the geoblacklight-icons.json file onto the page
  4. +
+

Now you'll see all icons in the project. You can add additional icons, change the default color value, or export to other formats here, ex. PNGs.

+

Screen Shot 2019-08-09 at 11 02 51 AM

+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/catalog_controller/index.html b/docs/catalog_controller/index.html new file mode 100644 index 00000000..c51be989 --- /dev/null +++ b/docs/catalog_controller/index.html @@ -0,0 +1,2111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Update the Catalog Controller - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Update the Catalog Controller

+

A number of other configurations you may need to update are handled in your app/controllers/catalog_controller.rb file. This file has plenty of code comments to help you out with the details, so the following is just a brief summary of the main configurations you can make within it.

+

Configurations you can alter include:

+
    +
  • Search Facet Management
      +
    • Determine which fields are exposed to users in the search facet panel, and how they are labeled.
    • +
    +
  • +
  • Search Results Fields
      +
    • Determine which fields will be displayed for each item in the search results list.
    • +
    +
  • +
  • Item View Fields
      +
    • Show/hide fields from the catalog view for each item. Note that a number of default fields are not displayed directly in the catalog view because they are used to power different aspects of the presentation.
    • +
    +
  • +
  • Default Search Behavior
      +
    • Alter search ranking, sorting, and filtering.
    • +
    +
  • +
  • Basemap Style +
  • +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/data_relations_widget/index.html b/docs/data_relations_widget/index.html new file mode 100644 index 00000000..9e991612 --- /dev/null +++ b/docs/data_relations_widget/index.html @@ -0,0 +1,2130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Add Data Relations Widget - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Data Relations Widget

+
+

Warning

+

This documentation is for GeoBlacklight versions 1.3 to versions 3.x only. Beginning with version 4.0, GeoBlacklight performs this function by default.

+
+

Beginning with v1.3.0, GeoBlacklight supports simple visualization of parent/children relations between records in a catalog. When records that were derived from a parent record point back to that parent, it enables a toolbar widget that displays the relation.

+

To make use of this, we have introduced support for a "source" field in GeoBlacklight metadata records. The actual key of this field is arbitrary –– just make sure that Settings.FIELDS.SOURCE properly reflects what you want to use –– but the GeoBlacklight schema allocates the Dublin Core field dc_source_sm for this purpose.

+

The responsibility of this field is to point to a parent document (a document from which the current one is derived). The value should be the layer_slug_s of that parent (or parents), which also resides in your catalog. No modifications need to be made to the parent record in order to point back towards the derived records.

+

Here is an example use of the dc_source_sm field, from a GeoBlacklight schema record: +

  "dc_source_sm": [
+    "nyu_2451_34635",
+    "nyu_2451_34636"
+  ],
+
+In the above case, the record being described is derived from two different records in the catalog (namely, nyu_2451_34635 and nyu_2451_34636).

+

Now, when navigating to the show page for either the current record, or one of the two parent records, a user will see something like this:

+

Child's view +Parent's view

+

This functionality also provides a HTML and JSON API for viewing all parent/child datasets for a record. The route used is: +localhost:3000/catalog/fake-record-001/relations

+

A sample JSON response for a record with two parents and no children might look like this: +

{
+  "ancestors": {
+    "numFound": 2,
+    "start": 0,
+    "docs": [
+      {
+        "dc_title_s": "2016 NYC Geodatabase, ArcGIS Version (jan2016)",
+        "layer_slug_s": "nyu_2451_34636"
+      },
+      {
+        "dc_title_s": "2016 NYC Geodatabase, Open Source Version (jan2016)",
+        "layer_slug_s": "nyu_2451_34635"
+      }
+    ]
+  },
+  "descendants": {
+    "numFound": 0,
+    "start": 0,
+    "docs": []
+  },
+  "current_doc": "nyu_2451_34513"
+}
+

+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/developers/index.html b/docs/developers/index.html new file mode 100644 index 00000000..3d6b9089 --- /dev/null +++ b/docs/developers/index.html @@ -0,0 +1,2260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + For Developers - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

For Developers

+

This page is for software developers looking to build GeoBlacklight from source, especially to contribute code to the core application.

+
+

Creating a custom application

+

If you are looking to start a new, branded and customized GeoBlacklight application for your institution, follow the Quick Start instructions. The application you create will inherit from the latest stable GeoBlacklight release. See the architecture diagram below for more context.

+
+
+

Dependencies

+

You should have the following installed before beginning:

+
    +
  • Ruby > 3.0.0
  • +
  • Java > JRE version 11 or higher
  • +
+
+

Build the Application

+

Clone the code base from the official GeoBlacklight repository or from your own fork if you plan to make upstream pull requests.

+
$ git clone git@github.com:geoblacklight/geoblacklight.git
+
+

Once cloned, enter the repository and install dependencies:

+
$ cd geoblacklight
+$ bundle install
+
+

Now initialize and start the application:

+
$ bundle exec rake geoblacklight:server
+
+

This command will executes all of the following steps and leave you with a running a local instance of GeoBlacklight:

+
    +
  • Download, configure, and start a local Solr instance
      +
    • Located in tmp/solr
    • +
    +
  • +
  • Seed this Solr instance with test fixtures
      +
    • JSON file fixtures located in spec/fixtures/solr_documents/
    • +
    +
  • +
  • Create a test application
      +
    • Located in .internal_test_app/
    • +
    +
  • +
  • Create a development database within the test application
      +
    • Located at .internal_test_app/storage/development.sqlite3
    • +
    • Database connection defined in .internal_test_app/config/database.yml
    • +
    • ActiveRecord supports PostgreSQL, SQLite, and MySQL (learn more)
    • +
    +
  • +
  • Run the Rails server
  • +
+
+

Troubleshooting

+

If you run into issues running this rake task, try removing your Gemfile.lock file and removing the test app with rm -R .internal_test_app. Then run bundle install before running the above command again.

+
+

You should now be able to visit http://localhost:3000/ in a web browser and see the test application. If you modify content in the test application, these changes will be reflected on browser refresh. This may be a good time to learn more about the GeoBlacklights's structure:

+

./img/geoblacklight-structure.png

+

In the diagram above, "My Application" is actually the local test application, .internal_test_app, and it is analogous to the standalone Rails application that you would create through the Quick Start instructions.

+

Running Solr and Rails server separately

+

You may decide to run Solr and the Rails server separately. Solr can be run separately using either Docker or solr_wrapper.

+

To use solr_wrapper use the following rake task, which starts Solr and seeds the index with data: +

$ rake geoblacklight:solr
+

+

To use docker instead, start the server: +

$ docker compose up
+
+If using docker to run Solr, you need to manually seed the index with data. This seed task must be run after the rails server has been started (see below). +
$ rake geoblacklight:internal:seed
+

+

Lastly start the rails server. Open another Terminal window, navigate to the place where your app is located, and run: +

$ rake geoblacklight:server_only
+

+

Once the server is running, you can open a web browser and visit the URL it prompts, usually http://localhost:8983/solr/#/blacklight-core to see the admin interface of your test instance of Solr. As before, remember that ^C (ctrl + c) stops the server.

+

You may also want to use an external Solr instance, especially in production. You can read more about that here.

+
+

Unit Testing

+

Running all the tests

+

As you develop and make changes, you may want to run tests on parts of the app to see if any warning occur. You can run the following to test the app +

$ rake ci
+
+Note that a test like this could take up to 5-6 minutes to complete, or longer. Warnings, deprecations, and other messages will be printed on your Terminal screen.

+

Running the tests separately

+

$ rake geoblacklight:solr
+
+Then, in another terminal window: +
$ rspec spec/
+
+Note: It is not necessary to run tests after every change you make. You can, for instance, change the name of a facet field, save your file, and then refresh your browser to see the change. However, if you add a new fixture metadata record, you will have to stop the servers and then restart them so the new file will be indexed.

+
+

Browser Testing

+

Cross-browser testing provided by:

+

+

Helpful Development Tools

+

Version Managers

+

Using version management tools for compatible versions of Ruby (rvm, rbenv, asdf) and Node (nvm, asdf) can make development easier.

+

asdf

+

Many developers like asdf because you can manage versions for Ruby and Node in a single utility. For developers who use asdf, it is helpful to add a .tool-versions file for each app.

+

Example: +

ruby 2.7.5
+nodejs 17.4.0
+java openjdk-11.0.2
+

+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/external_solr/index.html b/docs/external_solr/index.html new file mode 100644 index 00000000..c09df136 --- /dev/null +++ b/docs/external_solr/index.html @@ -0,0 +1,2150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Using an External Solr Instance - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Using an External Solr Instance

+

In some cases you may need to install Solr through a different method than described above, or link your GeoBlacklight installation to an existing Solr installation. You can learn more about installing Solr in the Apache documentation.

+

Configure the Solr Core

+

Once you have Solr installed, you will need to create a new core and configure it for GeoBlacklight. How you create the core may depend on your installation method, but will likely be something like

+
$ bin/solr -c blacklight-core
+
+

Now rename/remove the core's conf directory and replace it with the solr/conf directory from GeoBlacklight: github.com/geoblacklight/geoblacklight/tree/main/solr/conf.

+

You can alter the core's configuration here as well, generally in the schema.xml file.

+

You can find the installation location of your Solr instance through the web admin interface: http://yourdomain.com:8983/solr/

+

Set SOLR_URL Environment Variable

+

GeoBlacklight will use the SOLR_URL environment variable (if present) to look for Solr. For example, assuming your core is named blacklight-core:

+
$ export SOLR_URL=http://yourdomain.com:8983/solr/blacklight-core
+
+

Now run the rails server and your external Solr will be used

+
$ rake engine_cart:server
+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/extra.css b/docs/extra.css new file mode 100644 index 00000000..82934f54 --- /dev/null +++ b/docs/extra.css @@ -0,0 +1,114 @@ + + .md-grid { + /* Set width of page content */ + max-width: 90%; +} + + +/* TOP MENU */ + .md-tabs__link { + font-size: 1.7em; + } + +/* SIDE MENU */ +.md-nav--primary { + border-right: .1em solid #24574F; + } + +.md-nav__item--section > .md-nav__link[for] { + color: inherit; +} + +.md-nav__item{ + font-size: 1em; + } + +/* FONT SIZES */ +* { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + } + +.md-typeset h1{ + color: #000; + font-weight: 600; + font-size: 2.1em; +} + +.md-typeset h2{ + color: #818589; + font-size: 1.75em; +} + +.md-typeset h3{ + color: inherit; + font-size: 1.3em; +} + + +/* BLOG TITLES */ +a.toclink{ + font-size: .85em; + font-weight: 500; + } + +/* CENTER IMAGES */ +.center { + display: block; + margin-left: auto; + margin-right: auto; + width: 25%; +} + +/* OTHER GRIDS */ +/* RESPONSIVE - 1 COLUMN ON SMALL SCREENS */ +@media screen and (max-width: 640px) { + #grid-col { grid-template-columns: 100%; } +} + +/* GRID CONTAINER */ +#grid-line { + display: grid; + grid-template-columns: auto auto ; + grid-gap: 20px; +} + +/* GRID CELL */ +div.cell { +/* border: .5px solid #e0e0e0; */ + padding: 5px; +} + +/* SPAN MULTIPLE COLUMNS */ +.spancol { + grid-column-start: 1; + grid-column-end: 2; +} + +th, td { + border: 1px solid var(--md-typeset-table-color); + border-spacing: 0; + border-bottom: none; + border-left: none; + border-top: none; +} + +.md-typeset__table { + line-height: 1.5; +} + +.md-typeset__table table:not([class]) { + font-size: .74rem; + border-right: none; + font-family: monospace; +} + +.md-typeset__table table:not([class]) td, +.md-typeset__table table:not([class]) th { + padding: 9px; +} + +/* light mode alternating table bg colors */ +.md-typeset__table tr:nth-child(2n) { + background-color: #f8f8f8; +} + diff --git a/docs/framework-recommendations/index.html b/docs/framework-recommendations/index.html new file mode 100644 index 00000000..5551c5aa --- /dev/null +++ b/docs/framework-recommendations/index.html @@ -0,0 +1,2149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Framework Recommendations - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Framework Recommendations

+ +

This page lists the recommended languages and frameworks to use with your installation of GeoBlacklight

+

Metadata

+
    +
  • Aardvark (Recommended)
  • +
  • GBL 1.0 (Deprecated)
  • +
+

Ruby

+
    +
  • 3.2 (Recommended)
  • +
  • 2.7 / Support Ends 31 Mar 2023
  • +
+

Ruby on Rails

+
    +
  • 7.0+ (Recommended)
  • +
  • 6.1
  • +
  • 6.0 / Support Ends June 1, 2023
  • +
+

Blacklight

+
    +
  • v7+ (Recommended)
  • +
  • v8 / Will be supported in GBL v5+
  • +
+

Bootstrap

+
    +
  • v4 (Recommended)
  • +
  • v5 / Will be supported in GBL v5+
  • +
+

ViewComponents

+
    +
  • GBL v5 (Required)
  • +
  • GBL v4 (Recommended)
  • +
+

Javascript

+
    +
  • GBL v5 - ES6 / Modern Javascript (Required)
  • +
  • GBL v4 / jQuery + ES5 (Recommended)
  • +
+

Map Library

+
    +
  • Leaflet (Recommended)
  • +
  • TBD / GBL v5+
  • +
+

Apache Solr

+
    +
  • v9+ (Recommended)
  • +
  • <8.11 versions are End Of Life (EOL)
  • +
+

Production RDBMS

+
    +
  • PostgreSQL (Recommended)
      +
    • For potential adopters of GEOMG
    • +
    +
  • +
+

Background Queue

+
    +
  • Not Required
  • +
  • Sidekiq + Redis (Recommended)
      +
    • For potential adopters of GEOMG
    • +
    • Potential future GBL enhancement: Background Downloads
    • +
    +
  • +
+

GeoServer

+
    +
  • Not Required
  • +
  • Used by many GeoBlacklight adopters: Harvard, Princeton, Stanford
  • +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/geoblacklight_quick_start/index.html b/docs/geoblacklight_quick_start/index.html new file mode 100644 index 00000000..0e910417 --- /dev/null +++ b/docs/geoblacklight_quick_start/index.html @@ -0,0 +1,2157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Quick Start - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

GeoBlacklight Quick Start

+

This guide covers the quickest way to get up and running with GeoBlacklight, including:

+
    +
  • How to install GeoBlacklight on your local computer.
  • +
  • How to create a new application.
  • +
  • How to add and index geospatial content.
  • +
+

Installation

+

Bootstrap a new GeoBlacklight Ruby on Rails application using the template script:

+

DISABLE_SPRING=1 rails new app-name -m https://raw.githubusercontent.com/geoblacklight/geoblacklight/v4.4.0/template.rb
+
+ Then run the geoblacklight:server rake task to run the application:

+
$ cd app-name
+$ bundle exec rake geoblacklight:server
+
+ +

Index Example Data

+

With your Solr server and Rails server already running (via the geoblacklight:server rake task above), open a new terminal window and index the GeoBlacklight project's test fixtures via:

+
$ bundle exec rake "geoblacklight:index:seed[:remote]"
+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/geopackages/index.html b/docs/geopackages/index.html new file mode 100644 index 00000000..b64e6008 --- /dev/null +++ b/docs/geopackages/index.html @@ -0,0 +1,2135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + GeoPackages - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

GeoPackages

+ +

Accessing Raster and Vector Layers in GeoPackages

+

OGC GeoPackage

+

GeoPackage is an encoding standard specified and maintained by the Open Geospatial Consortium, primarily directed towards the structure of SQLite geodatabases. As the GeoPackage standard provides standardization for vector features, tile matrix sets, and raster maps, it may be used as a container for either vector or raster spatial data sets.

+

To indicate the download is a “GeoPackage”, add this term to the dct_format_s OpenGeoMetadata schema field.

+

GIS Web Services

+

GeoPackages may be rendered using the layer viewer by providing the URL of a standard Web Map Service (WMS) or Web Feature Service (WFS) within the dct_references_s field of the schema.

+

GeoServer Support

+

For those who are currently using GeoServer in order to provide access to these data sets, the documentation outlines the process for uploading data sets and extracting vector or raster layers. Further, there also exists a plugin which permits one to export vector or raster data layers into GeoPackages (please see further documentation outlining the extended WMS/WFS output formats). +Unfortunately (as stated above), exporting into GeoPackage in GeoServer requires that one install a plugin. Only reading is supported by GeoServer core.

+

ArcMap and ArcGIS Pro Support

+

For those using Esri's ArcMap, the process of connecting to a GeoPackage data source is identical to that of connecting to any SQLite database: https://desktop.arcgis.com/en/arcmap/latest/manage-data/databases/connect-sqlite.htm. This is the case also for users of ArcGIS Pro: https://pro.arcgis.com/en/pro-app/help/data/databases/work-with-sqlite-databases-in-arcgis-pro.htm +

+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/glossary/index.html b/docs/glossary/index.html new file mode 100644 index 00000000..6b048003 --- /dev/null +++ b/docs/glossary/index.html @@ -0,0 +1,2113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Glossary - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Glossary

+ +
+

Tip

+

See the Geo4LibCamp Glossary for more definitions at https://geo4libcamp.org/glossary

+
+
+
Aardvark
+
Metadata schema for GeoBlacklight 4.x and beyond
+
GBL 1.0
+
Metadata schema for GeoBlacklight 1.0-3.x
+
GeoJSON
+
Specific type of JSON for geographic features, such as OpenIndexMaps. GeoBlacklight metadata files are NOT GeoJSONs.
+
Faraday
+
A type of middleware for Ruby on Rails that provides a common interface, enabling different applications to send and receive data
+
IIIF
+
International Image Interoperability Framework. Used for displaying static images, like scanned maps.
+
JSON
+
JavaScript Object Notation - a flexible key:value pair file format used for GeoBlacklight metadata documents
+
Mirador
+
Open-Source IIIF viewer
+
OGM
+
OpenGeoMetadata
+
OpenIndexMaps
+
GeoJSON-based file specification for standardizing spatial index maps
+
Solr
+
The search index for GeoBlacklight
+
Sprockets
+
Ruby library for compiling and serving web assets
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/hardware_recommendations/index.html b/docs/hardware_recommendations/index.html new file mode 100644 index 00000000..f274509a --- /dev/null +++ b/docs/hardware_recommendations/index.html @@ -0,0 +1,2146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Hardware Recommendations - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Hardware Recommendations

+

Running GeoBlacklight in production has modest hardware requirements. Your local IT system administrators and DevOps personnel will be critical in shaping your deployment environment.

+

Example Production Environments

+

B1G/BTAA Geoportal

+

Currently (9/2022) the B1G/BTAA Geoportal is running on AWS web services in production with these hardware specs:

+

Solr server

+
    +
  • 4GB RAM
  • +
  • 2 CPU cores
  • +
  • Java Xmx configured for 3GB memory
  • +
  • OS base disk: small, at least 8GB for Linux but not much more needed
  • +
  • Solr data partition: 40GB (in practice, <10GB in use for BTAA)
  • +
+

Web/Rails server

+
    +
  • 8GB RAM
  • +
  • 2 CPU cores
  • +
  • OS base disk: at least 20GB for Linux
  • +
  • Data partition: 60GB for ample thumbnail caching space
  • +
  • Puma Rails server: Recommend 2 workers, 8 threads per worker in this configuration. More threads will necessitate more system memory
  • +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/img/advanced-architecture.png b/docs/img/advanced-architecture.png new file mode 100644 index 00000000..78919648 Binary files /dev/null and b/docs/img/advanced-architecture.png differ diff --git a/docs/img/allRelationships.jpg b/docs/img/allRelationships.jpg new file mode 100644 index 00000000..937da25a Binary files /dev/null and b/docs/img/allRelationships.jpg differ diff --git a/docs/img/cartodb-github.jpg b/docs/img/cartodb-github.jpg new file mode 100644 index 00000000..95744a43 Binary files /dev/null and b/docs/img/cartodb-github.jpg differ diff --git a/docs/img/downloadPanel.png b/docs/img/downloadPanel.png new file mode 100644 index 00000000..d170ea0f Binary files /dev/null and b/docs/img/downloadPanel.png differ diff --git a/docs/img/gbl-favicon.png b/docs/img/gbl-favicon.png new file mode 100644 index 00000000..56074b19 Binary files /dev/null and b/docs/img/gbl-favicon.png differ diff --git a/docs/img/geoblacklight-demo.gif b/docs/img/geoblacklight-demo.gif new file mode 100644 index 00000000..a47d206e Binary files /dev/null and b/docs/img/geoblacklight-demo.gif differ diff --git a/docs/img/geoblacklight-homepage.jpg b/docs/img/geoblacklight-homepage.jpg new file mode 100644 index 00000000..30d74f40 Binary files /dev/null and b/docs/img/geoblacklight-homepage.jpg differ diff --git a/docs/img/geoblacklight-itempage.jpg b/docs/img/geoblacklight-itempage.jpg new file mode 100644 index 00000000..2f3a9ff8 Binary files /dev/null and b/docs/img/geoblacklight-itempage.jpg differ diff --git a/docs/img/geoblacklight-logo-small.png b/docs/img/geoblacklight-logo-small.png new file mode 100644 index 00000000..00b9d191 Binary files /dev/null and b/docs/img/geoblacklight-logo-small.png differ diff --git a/docs/img/geoblacklight-logo-small@2x.png b/docs/img/geoblacklight-logo-small@2x.png new file mode 100644 index 00000000..3adb4cb1 Binary files /dev/null and b/docs/img/geoblacklight-logo-small@2x.png differ diff --git a/docs/img/geoblacklight-logo.png b/docs/img/geoblacklight-logo.png new file mode 100644 index 00000000..cebf77f3 Binary files /dev/null and b/docs/img/geoblacklight-logo.png differ diff --git a/docs/img/geoblacklight-logo@2x.png b/docs/img/geoblacklight-logo@2x.png new file mode 100644 index 00000000..10196187 Binary files /dev/null and b/docs/img/geoblacklight-logo@2x.png differ diff --git a/docs/img/geoblacklight-resultspage.jpg b/docs/img/geoblacklight-resultspage.jpg new file mode 100644 index 00000000..42dbd8f8 Binary files /dev/null and b/docs/img/geoblacklight-resultspage.jpg differ diff --git a/docs/img/geoblacklight-structure.d2 b/docs/img/geoblacklight-structure.d2 new file mode 100644 index 00000000..70bfd0a5 --- /dev/null +++ b/docs/img/geoblacklight-structure.d2 @@ -0,0 +1,24 @@ +direction: down + +bl: Blacklight + +gbl: GeoBlacklight\n(inherits from Blacklight) +solr: Solr\nSearch index and records database +db: Database\nHolds user accounts, bookmarks, and history + +app: "" +app.text: |md +## My Application + +- Rails app inherits from Blacklight → GeoBlacklight +- Connects to Solr via environment variable `SOLR_URL` +- Connects to database via config in `database.yml` +- Customize catalog, settings, locale strings, etc. +| + +bl -- gbl +gbl -- app + +app -> solr: Rake tasks index\nJSON records into Solr +solr -> app: The web client queries\n Solr for records +app <> db \ No newline at end of file diff --git a/docs/img/geoblacklight-structure.png b/docs/img/geoblacklight-structure.png new file mode 100644 index 00000000..2ff305f8 Binary files /dev/null and b/docs/img/geoblacklight-structure.png differ diff --git a/docs/img/geoblacklight@small.png b/docs/img/geoblacklight@small.png new file mode 100644 index 00000000..916037dc Binary files /dev/null and b/docs/img/geoblacklight@small.png differ diff --git a/docs/img/geoblacklight_metadata_view_ISO19139.png b/docs/img/geoblacklight_metadata_view_ISO19139.png new file mode 100644 index 00000000..a7b4b0c8 Binary files /dev/null and b/docs/img/geoblacklight_metadata_view_ISO19139.png differ diff --git a/docs/img/index-map-color.png b/docs/img/index-map-color.png new file mode 100644 index 00000000..f77d8bc9 Binary files /dev/null and b/docs/img/index-map-color.png differ diff --git a/docs/img/indexMap.png b/docs/img/indexMap.png new file mode 100644 index 00000000..cb7768a6 Binary files /dev/null and b/docs/img/indexMap.png differ diff --git a/docs/img/multiple-bbox.png b/docs/img/multiple-bbox.png new file mode 100644 index 00000000..972b8669 Binary files /dev/null and b/docs/img/multiple-bbox.png differ diff --git a/docs/img/open-in-cartodb-button.jpg b/docs/img/open-in-cartodb-button.jpg new file mode 100644 index 00000000..c1e3dc10 Binary files /dev/null and b/docs/img/open-in-cartodb-button.jpg differ diff --git a/docs/img/schema-example.png b/docs/img/schema-example.png new file mode 100644 index 00000000..20e22ca3 Binary files /dev/null and b/docs/img/schema-example.png differ diff --git a/docs/img/simple-architecture.png b/docs/img/simple-architecture.png new file mode 100644 index 00000000..99b31183 Binary files /dev/null and b/docs/img/simple-architecture.png differ diff --git a/docs/img/sketches/facets@small.png b/docs/img/sketches/facets@small.png new file mode 100644 index 00000000..7abb0108 Binary files /dev/null and b/docs/img/sketches/facets@small.png differ diff --git a/docs/img/sketches/map-notch@small.png b/docs/img/sketches/map-notch@small.png new file mode 100644 index 00000000..a3e42529 Binary files /dev/null and b/docs/img/sketches/map-notch@small.png differ diff --git a/docs/img/sketches/ui@small.png b/docs/img/sketches/ui@small.png new file mode 100644 index 00000000..4cfe3ca4 Binary files /dev/null and b/docs/img/sketches/ui@small.png differ diff --git a/docs/img/sortOptions.jpg b/docs/img/sortOptions.jpg new file mode 100644 index 00000000..583f260e Binary files /dev/null and b/docs/img/sortOptions.jpg differ diff --git a/docs/img/vt.png b/docs/img/vt.png new file mode 100644 index 00000000..016a5735 Binary files /dev/null and b/docs/img/vt.png differ diff --git a/docs/img/web-services-button.png b/docs/img/web-services-button.png new file mode 100644 index 00000000..8e32ff58 Binary files /dev/null and b/docs/img/web-services-button.png differ diff --git a/docs/implementation_recommendations/index.html b/docs/implementation_recommendations/index.html new file mode 100644 index 00000000..e4372f20 --- /dev/null +++ b/docs/implementation_recommendations/index.html @@ -0,0 +1,2190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Implementation Recommendations - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Implementation Recommendations

+

Adhering to local IT best practices will help your GeoBlacklight install get up and running with optimal support from your IT staff. Below are some discussion points worth discussing locally as you move your GBL application to production:

+

Analytics

+

You will want to collect web analytics for your application. Some institutions have policies in place to protect the anonymity of web users. Be sure to discuss how analytics will be implemented and monitored.

+

Sitemap and robots.txt

+

A sitemap and a robots.txt file will help you keep bots from crawling your application in ways that would cause significant performance issues.

+

For example, in the B1G Geoportal, we use the sitemap_generator rubygem and a cronjob to keep a sitemap up to date:

+
# config/sitemap.rb
+solr = RSolr.connect url: Blacklight.connection_config[:url]
+
+# Select all the docs from Solr
+response = sol.get('select', params: {q: '*:*', fl: 'id', rows: 9999999})
+
+# Build a flat sorted array of all document slugs
+slugs = response['response']['docs'].map { |doc| doc['id'] }.sort
+
+# Set the host name for URL creation
+SitemapGenerator::Sitemap.default_host = 'https://geo.btaa.org'
+SitemapGenerator::Sitemap.create do
+  # Put links creation logic here.
+  #
+  # The root path '/' and sitemap index file are added automatically for you.
+  # Links are added to the Sitemap in the order they are specified.
+  #
+  # Usage: add(path, options={})
+  #        (default options are used if you don't specify)
+  #
+  # Defaults: :priority => 0.5, :changefreq => 'weekly',
+  #           :lastmod => Time.now, :host => default_host
+  #
+
+  slugs.each { |slug| add "/catalog/#{slug}" }
+end
+
+
# whenever gem
+every :day, at: '12:30am', roles: [:app] do
+  rake 'sitemap:refresh'
+end
+
+

You'll likely want to disallow any code paths that hit Solr with a search query:

+
User-agent: *
+Disallow: /?q=
+Disallow: /*?q=*
+Disallow: /?f
+Disallow: /*?f*
+Disallow: /?_
+Disallow: /?bbox
+Disallow: /?page=
+Disallow: /bookmarks
+Disallow: /catalog.html?f
+Disallow: /catalog.html?_
+Disallow: /catalog.atom
+Disallow: /catalog.rss
+Disallow: /catalog/*/relations
+Disallow: /catalog/facet/*
+Disallow: /catalog/*/web_services
+Disallow: /catalog/email
+Disallow: /catalog/opensearch
+Disallow: /catalog/range_limit
+Disallow: /catalog/sms
+Disallow: /saved_searches
+Disallow: /search_history
+Disallow: /suggest
+Disallow: /users
+Disallow: /404
+Disallow: /422
+Disallow: /500
+
+

You might also want to disallow bots that aggressively index content as well:

+
User-agent: AhrefsBot
+Disallow: /
+User-agent: SemrushBot
+Disallow: /
+User-agent: PetalBot
+Disallow: /
+User-agent: BLEXBot
+Disallow: /
+User-agent: DotBot
+Disallow: /
+User-agent: DataForSeoBot
+Disallow: /
+
+

Uptime and Performance Monitoring

+

Your local IT staff should implement uptime and performance monitoring for your production GeoBlacklight application.

+

Systemd for process / uptime management and Nagios/Zabbix/CloudWatch for alerting are common tools. Third party options like AppSignal and UptimeRobot can help, too.

+

Data Backups

+

Discuss options for automatically backing up your application data from Solr and your application's relational database. Having a backup of your data will help you restore service after an unplanned interruption or corrupted index.

+

See the Apache Solr Reference Guide's Backup and Restore chapter for more details.

+

Log Rolling

+

You will need to schedule your application logs to periodically rotate to maintain the size of these files. The logrotate utility can be very helpful here.

+

Useful Cron Tasks

+

A few cronjobs will help keep your database lean. These examples use the popular whenever rubygem.

+
Delete Old Searches
+
# Clean up recent anonymous search records
+every :day, at: '2:30am', roles: [:app] do
+  rake 'blacklight:delete_old_searches[7]'
+end
+
+
Delete Old Guest Users
+
# Cleans up anonymous user accounts created by search sessions
+every :day, at: '1:30am', roles: [:app] do
+  rake 'devise_guests:delete_old_guest_users[2]'
+end
+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..acbb3182 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,2078 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Welcome to the GeoBlacklight Documentation - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Welcome to the GeoBlacklight Documentation

+

The GeoBlacklight documentation aims to help anyone interested in implementing the software, or developers working to customize or upgrade an existing implementation.

+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/index_maps/index.html b/docs/index_maps/index.html new file mode 100644 index 00000000..3be26050 --- /dev/null +++ b/docs/index_maps/index.html @@ -0,0 +1,2195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Index Maps - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Index Maps

+ +

Index Maps: Introduction

+

The 2020 Geo4LibCamp featured a workshop on index maps that provides useful information if you are new to index maps and want a basic primer. Many of the links below will lead you to relevant parts of this workshop.

+

For a conceptual introduction to index maps (i.e. what are index maps anyway?), see this explanation.

+

Here are examples of "live" index maps hosted within the GeoBlacklight instances of Cornell and Stanford.

+

Making Index Maps

+

Before making index maps for use in GeoBlacklight, it is important to be familiar with OpenIndexMaps, a specific index map standard that is used by the GeoBlacklight community. For an introduction to this standard, see here. For more detailed information on making index maps according to the OpenIndexMaps standard, see here.

+

Index maps that are made according to the OpenIndexMaps standard are encoded in the GeoJSON format. For more information about GeoJSON, see here. For a longer guide to GeoJSON with additional useful information, please see the Data Curation Network (DCN) primer on GeoJSON.

+

When working with GeoJSON, it is recommended to use QGIS. For a quick overview of QGIS features that are relevant to working with GeoJSON, see here.

+

The following tutorials cover how to make a polygon index map from an existing shapefile, how to make a point index map from a spreadsheet containing coordinates, how to create a grid index map from scratch, and how to create a polygon index map using virtual layer magic.

+

Adding, Customizing, and Displaying Index Maps

+

#588 added index map discovery +and preview to GeoBlacklight. Index map preview can be added to a layer by adding an accessible url to a GeoJSON file in a layer's dct_references_s section:

+
"dct_references_s": "{\"https://openindexmaps.org\": \"https://gist.githubusercontent.com/mejackreed/4a44f1f7cc4fbb926068738e903a9e96/raw/fedfb0e599d647920f084627b7dca8f88a358757/stanford-fb897vt9938.geojson\"}",
+
+

As noted above, index maps should be created using the OpenIndexMaps specification. In GeoBlacklight, the label property will be used for the tooltip that appears when the user hovers over a feature on the index map.

+

The index map preview can be customized by overriding the Handlebars template index_map_info.hbs and/or overriding the GeoBlacklight.Util.indexMapTemplate method.

+

#759 added selection styling for GeoJSON index map features and adjusted where style customizations are set. Styling for index map features can be customized in settings.yml. Any style that is set in the DEFAULT section will be applied to all feature states unless overwritten within each specific state. Style options follow the Leaflet Path Options, so any new style added should be from those available.

+

Metadata for Index Maps

+

Here are some recommendations to keep in mind when generating metadata for index maps:

+
    +
  • +

    The Geometry Type/Resource Type field (layer_geom_type_s in Metadata 1.0 or gbl_resourceType_sm in OpenGeoMetadata Aardvark) in the index map's metadata record should reflect the geometry type of the scanned map, aerial photo, LiDAR dataset etc. (i.e. the underlying data for which the index map serves as a contextual guide). It should not indicate the geom type of the index map itself.

    +
  • +
  • +

    The Subject field (dc_subject_sm or dct_subject_sm)) in the index map's metadata should include "index map" (in addition to other keywords relevant to the underlying data collection).

    +
  • +
  • +

    The Source field (dc_source_sm or dct_source_sm) in the metadata records of the underlying data should should reference the index map, since the index map can be seen as a "source dataset" that offers a guide to the broader collection.

    +
  • +
+

Committing GeoBlacklight Index Maps to the OpenIndexMaps Github repository

+

Once you have generated your index map and its associated metadata, the index map must go "live" on the web. There are different ways to pursue the task of making an index map go "live", but the recommended approach is to commit the map/GeoJSON to OpenIndexMaps' Github Repository, which facilitates the discovery and sharing of index maps across institutions.

+

Once your map has been committed to your OpenIndexMaps repository, you will want to take the url for the map's blob ("blob" stands for binary large object, which is an object that contains the contents of your file), and add this information back to your GeoBlacklight metadata record (in particular, you'll want to add this information to the dct_references_s section).

+

To get the blob url, click the "Raw" link on your map's Github page, and copy the url of the page to which you are taken upon clicking this link.

+

The dct_references_s section of the index map's GeoBlacklight metadata contains relevant external links, and are organized as a serialized JSON array of key/value pairs (for more information on this section in the GeoBlacklight metadata schema, see here). In this case, the blob url which you copied (above) will be the value associated with the OpenIndexMaps url (which is the key).

+

The following site, from the GeoBlacklight team at NYU, provides a script that adds references to existing GeoBlacklight metadata records. This script can be adapted to add the OpenIndexMaps/Blob-url key-value pair into the metadata's dct_references_s section.

+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/item_images/index.html b/docs/item_images/index.html new file mode 100644 index 00000000..0a5f8ca6 --- /dev/null +++ b/docs/item_images/index.html @@ -0,0 +1,2099 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Add Thumbnail Images - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Add thumbnail images

+

The GeoBlacklight Sidecar Images plugin adds support for harvesting remote images from geographic web services.

+

Requirements

+

GBL Sidecar Images requires:

+ +

A background job processor like Sidekiq is optional, but highly recommended.

+

Example Screenshot

+

Screenshot

+

Installation and Use

+

See the plugin project repo for full installation and use documentation.

+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/json-geojson/index.html b/docs/json-geojson/index.html new file mode 100644 index 00000000..7b0f7fa7 --- /dev/null +++ b/docs/json-geojson/index.html @@ -0,0 +1,2196 @@ + + + + + + + + + + + + + + + + + + + + + + + JSONs and GeoJSONs - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

JSONs and GeoJSONs

+
+

Summary

+
    +
  • GeoBlacklight metadata files are JSONs
  • +
  • OpenIndexMaps are GeoJSONs
  • +
+
+

JSON

+

JSON is a general-purpose data format.

+

JSON (JavaScript Object Notation) is a lightweight data-interchange format that is easy for machines to parse and generate. JSON is built on two structures:

+
    +
  • A collection of name/value pairs (often realized as an object, record, structure, dictionary, hash table, keyed list, or associative arrays)
  • +
  • An ordered list of values (often realized as an array, vector, list, or sequence)
  • +
+

JSON is used to represent a wide variety of data structures, including GeoBlacklight metadata files. These files contain a mix of text, numbers, booleans, and arrays, organizing the metadata in a structured way for the Solr index. Although the metadata files contain geospatial coordinates, they are not in the GeoJSONs format.

+

Example

+
[
+  {
+    "gbl_mdVersion_s": "Aardvark",
+    "dct_title_s": "Sample Record",
+    "gbl_resourceClass_sm": [
+      "Other"
+    ],
+    "gbl_resourceType_sm": [
+      "Aerial photographs"
+    ],
+    "gbl_indexYear_im": [
+      "1900"
+    ],
+    "gbl_dateRange_drsim": [
+      "[1900 TO 1910]"
+    ],
+    "dct_accessRights_s": "Public",
+    "dct_format_s": "JPEG",
+    "id": "2b22c800-a9fe-4fe1-aee6-f8784f4e987f",
+  }
+]
+
+

GeoJSON

+

GeoJSON is a specialized format for representing geographic information.

+

GeoJSON is a specific JSON format for encoding geographic data. It extends JSON by adding geographical features, geometries, and properties. GeoJSON supports the following geometry types:

+
    +
  • Point
  • +
  • LineString
  • +
  • Polygon
  • +
  • MultiPoint
  • +
  • MultiLineString
  • +
  • MultiPolygon
  • +
  • GeometryCollection
  • +
+

Example

+
{
+  "type": "Feature",
+  "geometry": {
+    "type": "Point",
+    "coordinates": [-123.365556, 48.428611]
+  },
+  "properties": {
+    "name": "Victoria, BC",
+    "population": 85792
+  }
+}
+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/leaflet/index.html b/docs/leaflet/index.html new file mode 100644 index 00000000..170d90d9 --- /dev/null +++ b/docs/leaflet/index.html @@ -0,0 +1,2717 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Customize Leaflet - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Customizing Leaflet

+

Adding Leaflet controls

+

GeoBlacklight supports adding customized Leaflet plugin controls to the maps. This can useful for adding a geocoding or fullscreen. This guide will walkthrough adding the Leaflet.fullscreen control plugin.

+

Add required Javascript and CSS

+

To add a custom control, first make sure that you require the needed JavaScript and/or CSS styles in your GeoBlacklight application.

+
// In your applications's app/assets/javascripts/geoblacklight.js
+
+//= require geoblacklight/geoblacklight
+//= require geoblacklight/basemaps
+//= require geoblacklight/controls
+//= require geoblacklight/viewers
+//= require geoblacklight/modules
+//= require geoblacklight/downloaders
+//= require leaflet-iiif
+//= require esri-leaflet
+//= require readmore.min
+
+//= require Leaflet.fullscreen.js
+
+

You should do something similar for vendor css files and images. GeoBlacklight uses the Rails asset pipeline for asset management. Vendor maintained files should usually be added under ./vendor/assets.

+
// In your applications's app/assets/stylesheets/geoblacklight.css.scss
+/*
+*= require geoblacklight/application
+*= require leaflet.fullscreen
+*/
+
+

Configure your settings

+

Next, you need to configure your settings to tell the viewers to load your control. Your application's lib/generators/geoblacklight/templates/settings.yml should look something like this:

+
...
+OPACITY_CONTROL: &opacity_control
+  CONTROLS:
+    - 'Opacity'
+
+LEAFLET:
+  MAP:
+  LAYERS:
+  VIEWERS:
+    WMS:
+      <<: *opacity_control
+    TILEDMAPLAYER:
+      <<: *opacity_control
+    FEATURELAYER:
+      <<: *opacity_control
+    DYNAMICMAPLAYER:
+      <<: *opacity_control
+    IMAGEMAPLAYER:
+      <<: *opacity_control
+...
+
+

Let's say you want to add the fullscreen control for just your WMS viewer. You will need to update your WMS viewer controls to add it like so:

+
...
+  VIEWERS:
+      WMS:
+        CONTROLS:
+          - 'Opacity'
+          - 'Fullscreen'
+...
+
+

Initialize your plugin

+

Finally you need to initialize your controls like this. You can initialize the plugin with additional options.

+
// In your applications's app/assets/javascripts/geoblacklight/geoblacklight.js
+...
+//= require Leaflet.fullscreen.js
+
+GeoBlacklight.Controls.Fullscreen = function() {
+  this.map.addControl(new L.Control.Fullscreen({
+    position: 'topright'
+  }));
+};
+
+

You should now have a working fullscreen button in your application!

+

GeoBlacklight WMS viewer with Fullscreen plugin

+

Adding a Search Control

+

Customizing Leaflet has certain limitations which can fortunately be overcome through the usage of plugins developed by third parties. Leaflet provides the following listing of plugins for the library: https://leafletjs.com/plugins.html#search--popups

+

Downloading Leaflet Plugins

+

Firstly, in order to integrate a plugin, the JavaScript source file(s) are downloaded into the vendor/assets/javascripts directory, where names are all in the lower case, with whitespace being replaced by dash characters (e. g. vendor/assets/javascripts/esri-leaflet.js)

+

Downloading JavaScript Source Files

+

Using wget +

$ wget -O vendor/assets/javascripts/leaflet-search.js https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.src.js
+
+Using curl +
$ curl -o vendor/assets/javascripts/leaflet-search.js https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.src.js
+
+For Production Builds +
$ wget -O vendor/assets/javascripts/leaflet-search.js https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.min.js
+
+or +
$ curl -o vendor/assets/javascripts/leaflet-search.js https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.min.js
+

+

Downloading CSS Files

+

wget +

$ wget -O vendor/assets/stylesheets/leaflet-search.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.src.css
+
+$ wget -O vendor/assets/stylesheets/leaflet-search.mobile.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.mobile.src.css
+
+curl +
$ curl -o vendor/assets/stylesheets/leaflet-search.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.src.css
+
+$ curl -o vendor/assets/stylesheets/leaflet-search.mobile.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.mobile.src.css
+
+Production Builds +
$ wget -O vendor/assets/stylesheets/leaflet-search.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.min.css
+
+$ wget -O vendor/assets/stylesheets/leaflet-search.mobile.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.mobile.min.css
+
+or +
$ curl -o vendor/assets/stylesheets/leaflet-search.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.min.css
+
+$ curl -o vendor/assets/stylesheets/leaflet-search.mobile.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.mobile.min.css
+

+

Downloading Image Files

+

$ wget -O app/assets/images/loader.gif https://github.com/stefanocudini/leaflet-search/raw/master/images/loader.gif
+$ wget -O app/assets/images/search-icon-mobile.png https://github.com/stefanocudini/leaflet-search/raw/master/images/search-icon-mobile.png
+$ wget -O app/assets/images/search-icon.png https://github.com/stefanocudini/leaflet-search/raw/master/images/search-icon.png
+
+or +
$ curl -o app/assets/images/loader.gif https://github.com/stefanocudini/leaflet-search/raw/master/images/loader.gif
+$ curl -o app/assets/images/search-icon-mobile.png https://github.com/stefanocudini/leaflet-search/raw/master/images/search-icon-mobile.png
+$ curl -o app/assets/images/search-icon.png https://github.com/stefanocudini/leaflet-search/raw/master/images/search-icon.png
+

+

Integrating Plugins into Leaflet

+

Integrating a plugin varies depending upon precisely what is being used, but the general approach seems to often follow a pattern such as the following: +

var searchLayer = L.layerGroup().addTo(map);
+//... adding data in searchLayer ...
+map.addControl( new L.Control.Search({layer: searchLayer}) );
+
+...where the map Object invokes addControl using the search L.Control Object as an argument.

+

Configuring GeoBlacklight

+

When integrating this GeoBlacklight, the approach above could modified by extending the previous example:

+
...
+  VIEWERS:
+      WMS:
+        CONTROLS:
+          - 'Opacity'
+          - 'Fullscreen'
+          - 'Search'
+...
+
+
// In the application app/assets/javascripts/geoblacklight.js
+...
+//= require
+//= require leaflet-search
+
+GeoBlacklight.Controls.Search = function() {
+  this.map.addControl(new L.control.search({
+    url: 'http://nominatim.openstreetmap.org/search?format=json&q={s}',
+        jsonpParam: 'json_callback',
+        propertyName: 'display_name',
+        propertyLoc: ['lat','lon'],
+        marker: L.circleMarker([0,0], { radius: 30 }),
+        autoCollapse: true,
+        autoType: false,
+        minLength: 2
+  }));
+};
+
+

// In the application app/assets/stylesheets/geoblacklight.scss
+/*
+*= require geoblacklight/application
+*= require leaflet-label
+*= require leaflet-search
+*= require leaflet-search.mobile
+*/
+
+// SCSS overrides for the default styles properties
+.leaflet-container {
+  .leaflet-control-search {
+    margin-top: 3.2rem;
+
+    .search-button {
+      background-image: image-url('search-icon-mobile');
+
+      &:hover {
+        background-image: image-url('search-icon-mobile');
+      }
+    }
+  }
+}
+
+After refreshing your web browser, the map viewer should now have a search control integrated:

+

geoblacklight_issues_528_screenshot_0

+

Switching the default basemap

+

GeoBlacklight comes with a default open-source basemap, Carto's Positron, but it is possible to switch to one of the seven baselayers supported within the GeoBlacklight application. They are:

+
    +
  • Dark Matter
  • +
  • Positron
  • +
  • Positron Lite
  • +
  • World Antique
  • +
  • World Eco
  • +
  • Flat Blue
  • +
  • Midnight Commander
  • +
+

In order to toggle between them, all you need to do is go to the catalog_controller.rb file in your application and replace the config.basemap_provider value. The valid values are in the comments above this line as a helpful reminder.

+

Dynamic Basemap Switching

+
+

Warning

+

This kind of customization may potentially make your future GeoBlacklight upgrades more difficult. If you choose to implement this feature, you will need to be extra vigilant when GBL JavaScript files change in future releases.

+
+

Need a dynamic basemap switcher? You can customize GeoBlacklight to add support for Leaflet's basemap switching:

+

basemap-switcher

+ +

Use yarn to install js-cookie:

+
$ yarn add js-cookie
+
+

Add the node_modules directory to your asset path:

+

/config/initializers/assets.rb

+
Rails.application.config.assets.paths << Rails.root.join('node_modules')
+
+

Add js-cookie to your geoblacklight.js file:

+

/app/assets/javascript/geoblacklight.js

+
//= require handlebars.runtime
+//= require geoblacklight/geoblacklight
+//= require geoblacklight/basemaps
+//= require geoblacklight/controls
+//= require geoblacklight/viewers
+//= require geoblacklight/modules
+//= require geoblacklight/downloaders
+//= require leaflet-iiif
+//= require esri-leaflet
+
+// Local Customizations
+//= require js-cookie/dist/js.cookie.js
+//= require ./local/viewers/map
+
+

2. Add Basemap options

+

Configure the additional basemap options in your geoblacklight.js file:

+

/app/assets/javascript/geoblacklight.js

+
...
+
+// Local Customizations
+//= require js-cookie/dist/js.cookie.js
+//= require ./local/viewers/map
+
+// LOCAL Namespace
+if (!window.LOCAL){ LOCAL={}; }
+
+// Basemap select - Text: Value
+LOCAL.baseLayerMap = {
+  "Default (Esri)": 'esri',
+  "OpenStreetMaps": 'openstreetmapStandard',
+  "World Imagery (Esri)": 'esri_world_imagery'
+}
+
+// Additional leaflet base layers
+GeoBlacklight.Basemaps.esri =  L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', {
+  attribution: false,
+  maxZoom: 18,
+  worldCopyJump: true,
+  detectRetina: true,
+  noWrap: false
+});
+
+GeoBlacklight.Basemaps.esri_world_imagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
+  attribution: false,
+  maxZoom: 18,
+  worldCopyJump: true,
+  detectRetina: true,
+  noWrap: false
+})
+
+

3. Modify GeoBlacklight's map.js file

+

Copy and move GeoBlacklight's map.js file to your local application.

+

Copy from GeoBlacklight: +app/assets/javascripts/geoblacklight/viewers/map.js

+

Move to your local application here: +app/assets/javascripts/local/viewers/map.js

+

Add a call to this.addBasemapSwitcher(); in the load block.

+
//= require geoblacklight/viewers/viewer
+
+GeoBlacklight.Viewer.Map = GeoBlacklight.Viewer.extend({
+
+  options: {
+    /**
+    * Initial bounds of map
+    * @type {L.LatLngBounds}
+    */
+    bbox: [[-82, -144], [77, 161]],
+    opacity: 0.75
+  },
+
+  overlay: L.layerGroup(),
+
+  load: function() {
+    if (this.data.mapGeom) {
+      this.options.bbox = L.geoJSONToBounds(this.data.mapGeom);
+    }
+    this.map = L.map(this.element).fitBounds(this.options.bbox);
+    this.map.addLayer(this.selectBasemap());
+
+    // Add initial bbox to map element for easier testing
+    if (this.map.getBounds().isValid()) {
+      this.element.setAttribute('data-js-map-render-bbox', this.map.getBounds().toBBoxString());
+    }
+
+    this.map.addLayer(this.overlay);
+    if (this.data.map !== 'index') {
+      this.addBoundsOverlay(this.options.bbox);
+    }
+
+    // Local Customizations
+    this.addBasemapSwitcher();
+  },
+
+  ...
+
+

Now add the functions to switch basemaps and store the current basemap in JS Cookie:

+
...
+
+/**
+* Selects basemap if specified in 1) cookie, 2) data options, 3) if not return mapquest
+*/
+selectBasemap: function() {
+  console.log("Selecting basemap");
+  console.log("Cookie: " + Cookies.get('basemap'));
+
+  var _this = this;
+  if (Cookies.get('basemap')) {
+    return GeoBlacklight.Basemaps[LOCAL.baseLayerMap[Cookies.get('basemap')]];
+  } else if (_this.data.basemap) {
+    return GeoBlacklight.Basemaps[_this.data.basemap];
+  } else {
+    return _this.basemap.mapquest;
+  }
+},
+
+addBasemapSwitcher: function() {
+  // basemaps control
+  console.log('Control: Base Layer');
+  var baseLayers = {
+    "Default (Esri)": GeoBlacklight.Basemaps.esri,
+    "OpenStreetMaps": GeoBlacklight.Basemaps.openstreetmapStandard,
+    "World Imagery (Esri)": GeoBlacklight.Basemaps.esri_world_imagery
+  };
+
+  L.control.layers(baseLayers, null, { position: 'bottomleft' }).addTo(this.map);
+
+  // Event listener for layer switcher
+  this.map.on('baselayerchange', function (e) {
+    Cookies.set('basemap', e.name)
+  });
+}
+
+...
+
+

4. Add Leaflet's CSS file to the asset pipeline

+

Unfortunately, Rails' asset pipeline cannot find Leaflet's Layer Group icon/images without some additional help.

+

Download a copy of Leaflet and copy the leaflet.css file into your local project here:

+

app/assets/stylesheets/leaflet/leaflet.css.erb

+

Add an import statement to application.scss for this new file:

+
...
+
+// Customizations
+@import 'leaflet/leaflet';
+
+

We'll need to modify this CSS file slightly to reference the images we need in the application.

+

At the top of this file add these lines:

+
//= depend_on_asset 'layer.png'
+//= depend_on_asset 'layers-2x.png'
+
+

Farther down the file, we'll need to edit this block too:

+
/* layers control */
+
+.leaflet-control-layers {
+    box-shadow: 0 1px 5px rgba(0,0,0,0.4);
+    background: #fff;
+    border-radius: 5px;
+    }
+.leaflet-control-layers-toggle {
+    background-image: url(<%= asset_url 'layers.png' %>);
+    width: 36px;
+    height: 36px;
+    }
+.leaflet-retina .leaflet-control-layers-toggle {
+    background-image: url(<%= asset_url 'layers-2x.png' %>);
+    background-size: 26px 26px;
+    }
+
+

We need the background-image paths to use Rails' asset_url helper so these images are fingerprinted correctly.

+

Lastly, from your Leaflet download copy the layers.png and layers-2x.png files into your local application here:

+
    +
  • app/assets/images/layers.png
  • +
  • app/assets/images/layers-2x.png
  • +
+

Homepage Map Centroid Clusters

+
+

Warning

+

This kind of customization may potentially make your future GeoBlacklight upgrades more difficult. If you choose to implement this feature, you will need to be extra vigilant when GBL JavaScript files change in future releases.

+
+

Want your homepage map to display centroid clusters? You can customize GeoBlacklight to add support for that:

+

homepage-centroid-visualization

+

1. Add a rake task to generate a centroids.json file

+

Create a new rake file here: +/lib/tasks/generate_centroids_json.rake

+

This rake task will write a centroids.json file to your application's public directory. Add these lines to the file:

+
require 'rsolr'
+
+namespace :geoportal do
+  desc 'Generate homepage centroids for map clustering'
+  task generate_centroids_json: :environment do
+    response = Blacklight.default_index.connection.get 'select', params: { q: "*:*", rows: '1000000' }
+
+    docs = []
+    response["response"]["docs"].each_with_index do |doc, index|
+      begin
+        if doc.key?('dcat_centroid') && !doc['dcat_centroid'].empty?
+          entry = {}
+          entry['l'] = doc['id']
+          entry['t'] = ActionController::Base.helpers.truncate(doc['dct_title_s'], length: 50)
+          lat,lng    = doc['dcat_centroid'].split(",")
+          lat = lat.to_f.round(4) # Truncate long values
+          lng = lng.to_f.round(4) # Truncate long values
+          entry['c'] = "#{lat},#{lng}"
+          docs << entry
+        end
+      rescue Exception => e
+        puts "Caught #{e}"
+        puts "BBox or centroid no good - #{doc['id']}"
+      end
+    end
+
+    centroids_file = "#{Rails.root}/public/centroids.json"
+    File.open(centroids_file, "w"){ |f| f.write(JSON.generate(docs)) }
+  end
+end
+
+

Run this rake task via this command: bundle exec rake geoportal:generate_centroids_json

+

2. Install JavaScript Dependencies

+
    +
  • Oboe - Oboe.js reads json, giving you the objects as they are found without waiting for the stream to finish
  • +
  • PruneCluster - Fast and realtime marker clustering for Leaflet
  • +
+

Use yarn to add these two new dependencies to the project:

+

yarn add oboe +yarn add @sintef/prune-cluster

+

Add the node_modules directory to your asset path:

+

/config/initializers/assets.rb

+
Rails.application.config.assets.paths << Rails.root.join('node_modules')
+
+

3. Add our JavaScript changes for the Homepage Map

+

We need to override the GeoBlacklight app/assets/javascripts/geoblacklight/modules/home.js file to add our customization.

+

To override a Rails Engine's javascript (GeoBlacklight), we need to update our asset pipeline calls to require specific files from the GeoBlacklight modules directory instead of globbing all of the file from /modules/.

+

Change your local geoblacklight.js file to look like this:

+
//= require handlebars.runtime
+//= require geoblacklight/geoblacklight
+//= require geoblacklight/basemaps
+//= require geoblacklight/controls
+//= require geoblacklight/viewers
+
+// Local Customization - Start
+//= require geoblacklight/modules/bookmarks
+//= require geoblacklight/modules/download
+//= require geoblacklight/modules/geosearch
+//= require geoblacklight/modules/help_text
+//= require ./geoportal/modules/home
+//= require geoblacklight/modules/item
+//= require geoblacklight/modules/layer_opacity
+//= require geoblacklight/modules/metadata_download_button
+//= require geoblacklight/modules/metadata
+//= require geoblacklight/modules/relations
+//= require geoblacklight/modules/results
+//= require geoblacklight/modules/svg_tooltips
+//= require geoblacklight/modules/util
+// Local Customization - End
+
+//= require geoblacklight/downloaders
+//= require leaflet-iiif
+//= require esri-leaflet
+
+

As included in the code snippet above, add a file named app/assets/javascripts/geoportal/modules/home.js to your application.

+

Inside that file write these lines:

+
Blacklight.onLoad(function() {
+  $('[data-map="home"]').each(function(i, element) {
+    var geoblacklight = new GeoBlacklight.Viewer.Map(this);
+    var data = $(this).data();
+
+    geoblacklight.map.addControl(L.control.geosearch({
+      baseUrl: data.catalogPath,
+      dynamic: false,
+      searcher: function() {
+        window.location.href = this.getSearchUrl();
+      },
+      staticButton: '<a href="#" class="btn btn-primary">Search here</a>'
+    }));
+
+    // Local Customization - Start
+    var pruneCluster = new PruneClusterForLeaflet();
+
+    oboe('/centroids.json')
+      .node('*', function( doc ){
+          if(typeof doc.c != 'undefined'){
+            var latlng = doc.c.split(",")
+
+            var marker = new PruneCluster.Marker(latlng[0],latlng[1], {popup: "<a href='/catalog/" + doc.l + "'>" + doc.t + "</a>"});
+            pruneCluster.RegisterMarker(marker);
+          }
+        }
+      )
+      .done(function(){
+        geoblacklight.map.addLayer(pruneCluster)
+      });
+    // Local Customization - End
+  });
+});
+
+

4. Add our Stylesheet changes for the Homepage Map

+

All that is missing now are is the CSS changes to style our clusters. Update your application.scss file to include the missing stylesheet:

+
@import 'customizations';
+@import 'bootstrap';
+@import 'blacklight';
+@import 'geoblacklight';
+
+// Local Customization
+@import '@sintef/prune-cluster/dist/LeafletStyleSheet';
+
+

Reload your homepage and you should see something like this:

+

homepage-result-clusters

+

Configure Leaflet for retina displays

+

GeoBlacklight allows implementers to configure the way in which basemaps and tile layers (WMS) are displayed on high pixel density 'retina' screens. When retina detection settings are enabled, Leaflet will request larger tiles to take advantage of the increased resolution.

+

Tile layers

+

In your application's settings.yml, find DETECT_RETINA and set it to true or false.

+
...
+LEAFLET:
+  MAP:
+  LAYERS:
+    DETECT_RETINA: true
+...
+
+

When set to true, Leaflet will load 512 pixel tiles on retina displays.

+

retina-layer

+

Basemaps

+

To configure the stock CartoDB basemaps for higher resolution display you will have to override the GeoBlacklight.Basemaps javascript module. In your application, create a geoblacklight directory in app/assets/javascripts/ and then create a new file called basemaps.js in that directory.

+

basemap

+

Now copy the contents of the Geoblacklight basemaps.js file into your new file. On any basemaps that you want to enable retina, set detectRetina to true. Your file should look something like this:

+
// basemaps
+
+GeoBlacklight.Basemaps = {
+  darkMatter: L.tileLayer(
+    'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}{retina}.png', {
+      attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, &copy; <a href="http://cartodb.com/attributions">CartoDB</a>',
+      maxZoom: 18,
+      worldCopyJump: true,
+      retina: '@2x',
+      detectRetina: true
+    }
+  ),
+  positron: L.tileLayer(
+    'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{retina}.png', {
+      attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, &copy; <a href="http://cartodb.com/attributions">CartoDB</a>',
+      maxZoom: 18,
+      worldCopyJump: true,
+      retina: '@2x',
+      detectRetina: true
+    }
+  )
+};
+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/metadata-functions.csv b/docs/metadata-functions.csv new file mode 100644 index 00000000..011645b7 --- /dev/null +++ b/docs/metadata-functions.csv @@ -0,0 +1,43 @@ +Label
Field URI,Required - will cause app to error if missing,Default facet,Search results display,Item show default display,Relation widget,Notes +**ID**
id,==yes==,no,no,no,no,Functions as the slug (end part) of the item's URL +**Access Rights**
dct_accessRights_s,==yes==,no,icon,no,no,"If set to ""Restricted"", a closed padlock icon appears next to the title. Downloads and web service previews will be hidden unless a user logs in using an authentication process. If set to ""Public"", an open padlock icon appears next to the title. Downloads and web services are accessible." +**Format**
dct_format_s,{++conditional++},==yes==,no,==yes==,no,Required if a Download link is included. This value displays on the item show in the button under the download widget. +**Title**
dct_title_s,no (see notes),no,Clickable text,==yes==,no,"If a title is missing, GeoBlacklight will display the ID as the title." +**Alternative Title**
dct_alternative_sm,no,no,no,==yes==,no,- +**Bounding Box**
dcat_bbox,no,no,no,no,no,Rectangular extent required for overlap ratio boosting as part of the overall relevance ranking algorithm. +**Centroid**
dcat_centroid,no,no,no,no,no,Can be leveraged for homepage centroid map. Not used in default GeoBlacklight. +**Creator**
dct_creator_sm,no,==yes==,text on expanded view,==yes==,no,- +**Date Issued**
dct_issued_s,no,no,no,==yes==,no,- +**Date Range**
gbl_dateRange_drsim,no,no,no,==yes==,no,- +**Description**
dct_description_sm,no,no,text on expanded view,==yes==,no,- +**Display Note**
gbl_displayNote_sm,no,no,no,==yes==,no,Text displays within a callout box +**File Size**
gbl_fileSize_s,no,no,no,==yes==,no,- +**Geometry**
locn_geometry,no,no,no,no,no,Required for the map search +**Georeferenced**
gbl_georeferenced_b,no,==yes==,no,==yes==,no,- +**Identifier**
dct_identifier_sm,no,no,no,no,no,- +**Index Year**
gbl_indexYear_im,no,==yes==,text on expanded view,==yes==,no,Can be used with the Date Range plugin +**Is Part Of**
dct_isPartOf_sm,no,no,no,no,==yes==,- +**Is Replaced By**
dct_isReplacedBy_sm,no,no,no,no,no,Not used in GeoBlacklight +**Is Version Of**
dct_isVersionOf_sm,no,no,no,no,==yes==,- +**Keyword**
dcat_keyword_sm,no,no,no,==yes==,no,- +**Language**
dct_language_sm,no,no,no,==yes==,no,3 digit code +**License**
dct_license_sm,no,no,no,==yes==,no,- +**Member Of**
pcdm_memberOf_sm,no,no,no,no,==yes==,- +**Metadata Version**
gbl_mdVersion_s,no,no,no,no,no,May be required by some harvesters in OpenGeoMetadata +**Provider**
schema_provider_s,no,==yes==,icon,==yes==,no,May control which records an authenticated user has access to. +**Publisher**
dct_publisher_sm,no,==yes==,no,==yes==,no,- +**References**
dct_references_s,no,no,no,no,no,See https://opengeometadata.org/reference-uris/ for how different references function in GeoBlacklight +**Relation**
dct_relation_sm,no,no,no,no,==yes==,- +**Replaces**
dct_replaces_sm,no,no,no,no,==yes==,- +**Resource Class**
gbl_resourceClass_sm,no,==yes==,icon,==yes==,no,"GBL will display an icon associated with the value. If multivalued, the first value will be used. Note: there is currently no icon for ""Web services""." +**Resource Type**
gbl_resourceType_sm,no,==yes==,no,==yes==,no,- +**Rights**
dct_rights_sm,no,no,no,==yes==,no,- +**Rights Holder**
dct_rightsHolder_sm,no,no,no,==yes==,no,- +**Source**
dct_source_sm,no,no,no,no,==yes==,- +**Spatial Coverage**
dct_spatial_sm,no,==yes==,no,==yes==,no,- +**Subject**
dct_subject_sm,no,==yes==,no,==yes==,no,- +**Suppressed**
gbl_suppressed_b,no,no,no,no,no,Hides items from search results. Items are only visible as child or related records in the Relations widgets. +**Temporal Coverage**
dct_temporal_sm,no,no,no,==yes==,no,- +**Theme**
dcat_theme_sm,no,==yes==,no,==yes==,no,- +**WxS Identifier**
gbl_wxsIdentifier_s,no,no,no,no,no,Required if a OCG web service is included +**Modified**
gbl_mdModified_dt,no,no,no,no,no,- \ No newline at end of file diff --git a/docs/metadata/index.html b/docs/metadata/index.html new file mode 100644 index 00000000..2ea3e031 --- /dev/null +++ b/docs/metadata/index.html @@ -0,0 +1,2541 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Metadata Fields - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Metadata Reference

+

GeoBlacklight uses a lightweight metadata schema designed for geospatial resource discovery. The schema enables keyword searches, faceted refinement, and spatial map searching.

+

Metadata schema features:

+
    +
  • based on Dublin Core, with custom elements added for spatial values
  • +
  • designed for discovery - to help users find items
  • +
  • not designed for complete technical documentation, such as a GIS dataset's processing history
  • +
  • includes elements for external links, such as downloads, web services, or supplemental metadata
  • +
  • interoperable for the OpenGeoMetadata federated metadata sharing community
  • +
+
+

Note

+

Metadata for GeoBlacklight is documented on the OpenGeoMetadata website . Key pages include:

+ +
+

Metadata functionality in GeoBlacklight 4.x

+
+

Hover over the column headers for sorting options.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Label
Field URI
Required - will cause app to error if missingDefault facetSearch results displayItem show default displayRelation widgetNotes
ID
id
yesnonononoFunctions as the slug (end part) of the item's URL
Access Rights
dct_accessRights_s
yesnoiconnonoIf set to "Restricted", a closed padlock icon appears next to the title. Downloads and web service previews will be hidden unless a user logs in using an authentication process. If set to "Public", an open padlock icon appears next to the title. Downloads and web services are accessible.
Format
dct_format_s
conditionalyesnoyesnoRequired if a Download link is included. This value displays on the item show in the button under the download widget.
Title
dct_title_s
no (see notes)noClickable textyesnoIf a title is missing, GeoBlacklight will display the ID as the title.
Alternative Title
dct_alternative_sm
nononoyesno-
Bounding Box
dcat_bbox
nononononoRectangular extent required for overlap ratio boosting as part of the overall relevance ranking algorithm.
Centroid
dcat_centroid
nononononoCan be leveraged for homepage centroid map. Not used in default GeoBlacklight.
Creator
dct_creator_sm
noyestext on expanded viewyesno-
Date Issued
dct_issued_s
nononoyesno-
Date Range
gbl_dateRange_drsim
nononoyesno-
Description
dct_description_sm
nonotext on expanded viewyesno-
Display Note
gbl_displayNote_sm
nononoyesnoText displays within a callout box
File Size
gbl_fileSize_s
nononoyesno-
Geometry
locn_geometry
nononononoRequired for the map search
Georeferenced
gbl_georeferenced_b
noyesnoyesno-
Identifier
dct_identifier_sm
nonononono-
Index Year
gbl_indexYear_im
noyestext on expanded viewyesnoCan be used with the Date Range plugin
Is Part Of
dct_isPartOf_sm
nonononoyes-
Is Replaced By
dct_isReplacedBy_sm
nononononoNot used in GeoBlacklight
Is Version Of
dct_isVersionOf_sm
nonononoyes-
Keyword
dcat_keyword_sm
nononoyesno-
Language
dct_language_sm
nononoyesno3 digit code
License
dct_license_sm
nononoyesno-
Member Of
pcdm_memberOf_sm
nonononoyes-
Metadata Version
gbl_mdVersion_s
nononononoMay be required by some harvesters in OpenGeoMetadata
Provider
schema_provider_s
noyesiconyesnoMay control which records an authenticated user has access to.
Publisher
dct_publisher_sm
noyesnoyesno-
References
dct_references_s
nononononoSee https://opengeometadata.org/reference-uris/ for how different references function in GeoBlacklight
Relation
dct_relation_sm
nonononoyes-
Replaces
dct_replaces_sm
nonononoyes-
Resource Class
gbl_resourceClass_sm
noyesiconyesnoGBL will display an icon associated with the value. If multivalued, the first value will be used. Note: there is currently no icon for "Web services".
Resource Type
gbl_resourceType_sm
noyesnoyesno-
Rights
dct_rights_sm
nononoyesno-
Rights Holder
dct_rightsHolder_sm
nononoyesno-
Source
dct_source_sm
nonononoyes-
Spatial Coverage
dct_spatial_sm
noyesnoyesno-
Subject
dct_subject_sm
noyesnoyesno-
Suppressed
gbl_suppressed_b
nononononoHides items from search results. Items are only visible as child or related records in the Relations widgets.
Temporal Coverage
dct_temporal_sm
nononoyesno-
Theme
dcat_theme_sm
noyesnoyesno-
WxS Identifier
gbl_wxsIdentifier_s
nononononoRequired if a OCG web service is included
Modified
gbl_mdModified_dt
nonononono-
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/periodic_maintenance/index.html b/docs/periodic_maintenance/index.html new file mode 100644 index 00000000..113330d5 --- /dev/null +++ b/docs/periodic_maintenance/index.html @@ -0,0 +1,2098 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Periodic Maintenance - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Periodic Maintenance

+

Once your GBL application is running in production, you'll need to schedule some periodic maintenance sprints to keep things up to date and running happily.

+

Test Suite Maintenance

+

The best way to future-proof your GBL install is to write and maintain a local test suite that provides coverage for basic application functionality.

+

As you upgrade versions of GeoBlacklight, Blacklight, Ruby on Rails, or Ruby, this test suite is your insurance that the application is running as expected after these upgrades are implemented. If upon upgrading GeoBlacklight or another core component, you test suite begins to fail, you'll know you have some development work to complete to successfully finish the upgrade.

+

GeoBlacklight Releases

+

GBL usually releases new versions of the software after the Winter and Summer community sprints. Schedule some time for your local development team to review the latest GBL release and upgrade notes on a biannual timeframe.

+

Blacklight Releases

+

Upstream releases of Blacklight can have a significant impact on GeoBlacklight installations. The Blacklight community is very good at leaving deprecation warnings in blacklight releases to help the GeoBlacklight community and local adopters keep their code maintained.

+

Ruby and Ruby on Rails Releases

+

Blacklight and GeoBlacklight will adjust their test matrices for new releases of Ruby and Ruby on Rails to ensure proper support.

+

Commonly, there is no need to rush to upgrade to a new Ruby or Ruby on Rails release, unless there is a significant security issue to resolve.

+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/releases/index.html b/docs/releases/index.html new file mode 100644 index 00000000..69ae1f54 --- /dev/null +++ b/docs/releases/index.html @@ -0,0 +1,2184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Software Releases - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Release Calendar

+

GeoBlacklight release and technology dependency matrix.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
GBL v3 LTSGBL v4 • Current Major ReleaseGBL v5 • Next Major Release
Support StatusReleased 2020; Ends 2025Released 2022; Ends 2026ETA 2024/2025
MetadataGBL 1.0AardvarkAardvark
Rubyv3+v3+v3.3+
Ruby on Railsv6-v7v6-v7v8+
Blacklightv7v7v8+
Bootstrapv4v4v5
ViewComponentsFewFewMany
JavaScript (JS)jQuery / ES5jQuery / ES5ES6 (Modern JavaScript) Published to NPM
JS Map LibraryLeafletLeafletTBD
Asset ManagementSprocketsSprocketsVite Ruby / Import Maps + Bundling
Apache Solr<= v8v8-v9+v9+
Production RDBMSN/AN/ARecommended PostgreSQL
Background QueueN/AN/ASolid Queue
GeoServerOptionalOptionalOptional
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/rendering_html_from_description/index.html b/docs/rendering_html_from_description/index.html new file mode 100644 index 00000000..ff0c7189 --- /dev/null +++ b/docs/rendering_html_from_description/index.html @@ -0,0 +1,2100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Render HTML in Metadata Fields - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Render HTML in Metadata Fields

+ +

Blacklight includes a helper_method argument for catalog_controller.rb field configuration. You can use that helpful technique to output whatever you need from the solr field value.

+

An example for adding line breaks and even HTML to a dc_description_s field would work like this:

+

1) Add a custom helper for presenting the data, using Rails' simple_format helper

+
# ApplicationHelper / application_helper.rb
+
+def render_html_description(args)
+  simple_format(Array(args[:value]).flatten.join(' '))
+end
+
+

2) Point the show field at your new helper_method

+
# CatalogController / catalog_controller.rb
+
+config.add_show_field Settings.FIELDS.DESCRIPTION, label: 'Description', itemprop: 'description', helper_method: :render_html_description
+
+

3) Example description value with line breaks ("\n\n") and some HTML markup, too:

+
  "dc_description_s": "This table shows all 911 police emergency response and officer-initiated calls for service in the City of Detroit since September 20, 2016. Emergency response calls are the result of people calling 911 to request police services. \n\n Officer-initiated calls include traffic stops, street investigations and other policing activities (such as observing crimes in progress) where police officers initiate the response. The table includes all calls taken, dispatch, travel, and total response times for those calls serviced by a police agency. The data also include the responding agency, unit, call type and category of each call. Should you have questions about this dataset, you may contact the Commanding Officer of the Detroit Police Department's Crime Intelligence Unit at 313-596-2250 or <a href= "mailto:CrimeIntelligenceBureau@detroitmi.gov\">CrimeIntelligenceBureau@detroitmi.gov</a>. ",
+
+

4) Now the show page will render like this

+

Screen Shot 2020-08-06 at 3 30 38 PM

+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/settings-yml/index.html b/docs/settings-yml/index.html new file mode 100644 index 00000000..86842d36 --- /dev/null +++ b/docs/settings-yml/index.html @@ -0,0 +1,2206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Settings.yml fields - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

settings.yml fields

+

APPLICATION_LOGO_URL

+

URL for logo image to be used in generated Carto OneClick links.

+ +

Optional integration with Carto OneClick Service.

+

ARCGIS_BASE_URL

+

Used to view layers directly in ArcGIS online. More information in the ArcGIS Online documentation.

+

DOWNLOAD_PATH

+

Local path used for temporary storage of generated download files.

+

BBOX_WITHIN_BOOST

+

The SOLR Boost Query value for spatial search matches within a bounding box.

+

OVERLAP_RATIO_BOOST

+

The SOLR Boost Functions value for overlap ratio.

+

HOMEPAGE_MAP_GEOM

+

Leave null to default to entire world, or add a stringified GeoJSON object to scope initial render of the map on the homepage of the application.

+

GBL_PARAMS

+

Explicit list of whitelisted URL params that can be used within the application, enforced via Rails StrongParameters.

+
+

Note

+

If you are trying to use a new URL param within your app, you will need to register it here. You may see "unpermitted parameters" errors until you update this setting.

+
+

FIELDS

+

All metadata fields are linked with their respective identifiers in the SOLR index in this hash. To learn more about the default GeoBlacklight metadata schema, Aardvark, view the OGM Aardvark specification.

+

INSTITUTION

+

This setting should hold the name of your institution, and can be used to help determine access to restricted records. In some GeoBlacklight implementations, for example, when a restricted record's schema_provider_s field matches Settings.INSTITUTION, authenticated users will be granted full access.

+

METADATA_SHOWN

+

Enables links for various metadata formats in the tool panel for a record if the corresponding URI key is present in that record's dct_references_s field.

+

TIMEOUT_DOWNLOAD

+

(For external Download) timeout and open_timeout parameters for Faraday.

+

TIMEOUT_WMS

+

(For WMS inspection) timeout and open_timeout parameters for Faraday.

+

USE_GEOM_FOR_RELATIONS_ICON

+

Use the Geometry Type value from a record to determine what icon to use for its data relations.

+
+

Warning

+

This setting is only applicable for GBL 1.0 metadata and is not compatible with OGM Aardvark.

+
+

WEBSERVICES_SHOWN

+

A list of web services that will be available for a record, if that record has a corresponding URI key in its dct_references_s field.

+

For example, if a record's references include a wms entry, and WEBSERVICES_SHOWN includes wms (as it does by default), a preview map will appear in the tool panel showing the WMS layer.

+

Supported web services:

+
  - 'wms'
+  - 'tms'
+  - 'wfs'
+  - 'xyz'
+  - 'wmts'
+  - 'tilejson'
+  - 'iiif'
+  - 'feature_layer'
+  - 'tiled_map_layer'
+  - 'dynamic_map_layer'
+  - 'image_map_layer'
+
+

DISPLAY_NOTES_SHOWN

+

Configuration for special rendering of gbl_displayNote_sm field values. Default note types are danger, info, tip, and warning.

+

You can add your own display note configuration as well. Each entry must have the follow properties:

+
+
bootstrap_alert_class
+
Name of Bootstrap alert class to use for the note's container
+
icon
+
Name of GeoBlacklight SVG icon to display with note
+
note_prefix
+
String that will be used at the beginning of a gbl_displayNote_sm entry to trigger this particular rendering.
+
+

For example, the "info" note is configured like this:

+
DISPLAY_NOTES_SHOWN
+  info:
+    bootstrap_alert_class: alert-info
+    icon: circle-info-solid
+    note_prefix: "Info: "
+
+
+

Info

+

Display Notes will appear in GeoBlacklight in a similar manner to this admonition box.

+
+

RELATIONSHIPS_SHOWN

+

GeoBlacklight supports many different types of relations between records. Configuration for how these are displayed is stored here. Each relationship defined must have the following properties:

+
+
field
+
The SOLR field that the query is performed against
+
query_type
+
The type of query sent to SOLR
+
icon
+
GeoBlacklight icon to use for matched records
+
label
+
Label from the locale string translations file
+
inverse
+
The inverse relationship to this one, used to generate bidirectional linkages
+
+

For example, the MEMBER_OF_ANCESTORS relationship would be defined like so (note that the MEMBER_OF_DESCENDANTS relationship would also need to be defined as it is referenced in the inverse property):

+
RELATIONSHIPS_SHOWN:
+  MEMBER_OF_ANCESTORS:
+    field: pcdm_memberOf_sm
+    icon: parent-item
+    inverse: :MEMBER_OF_DESCENDANTS
+    label: geoblacklight.relations.member_of_ancestors
+    query_type: ancestors
+
+

WMS_PARAMS

+

These parameters are appended to all WMS endpoints that your records contain. If you always want to be requesting VERSION=1.3.0 services, for example, you would update that here.

+

Default values:

+
  :SERVICE: 'WMS'
+  :VERSION: '1.1.1'
+  :REQUEST: 'GetFeatureInfo'
+  :STYLES: ''
+  :SRS: 'EPSG:4326'
+  :EXCEPTIONS: 'application/json'
+  :INFO_FORMAT: 'text/html'
+
+

LEAFLET

+

GeoBlacklight uses Leaflet to power its web map interfaces. This setting contains many default configuration values for how these maps appear and behave.

+

A few common customizations of GeoBlacklight involve updates to this setting. See the Customizing Leaflet page.

+

HELP_TEXT

+

Labels shown in the popover for various viewer protocols, to provide more context for users. The values here must reference entries in the locale translation string file.

+ +

Show a sidebar static map for items with the listed viewer protocols.

+

Default values:

+
  - 'iiif'
+  - 'iiif_manifest'
+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/settings/index.html b/docs/settings/index.html new file mode 100644 index 00000000..73f69529 --- /dev/null +++ b/docs/settings/index.html @@ -0,0 +1,2094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Configure the Settings - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Configure the Settings

+

A lot of configuration for your GeoBlacklight instance will be handled in the settings.yml file.

+

Keep in mind, GeoBlacklight has reasonable defaults for all settings, so you do not need to change anything in order to get up and running. That said, you will eventually need to change something. Below is an annotated list of all variables in the settings file.

+

If you are developing a custom application, look for config/settings.yml. If you are working on the core GeoBlacklight codebase, the file is lib/generators/geoblacklight/templates/settings.yml.

+
+

Note

+

Settings are implemented with the config gem, and are available as properties of the Settings object throughout the application.

+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/solr_search_relevancy/index.html b/docs/solr_search_relevancy/index.html new file mode 100644 index 00000000..55e0031b --- /dev/null +++ b/docs/solr_search_relevancy/index.html @@ -0,0 +1,2185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Configure Search Relevancy - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Configuring Search Relevancy in Solr for GeoBlacklight

+

In GeoBlacklight, search relevancy determins how search results are ranked based on the importance of various metadata fields. You can adjust the relevancy by modifying the solrconfig.xml file, which allows you to control which fields have more influence on the search results. The configuration uses the edismax query parser, which provides advanced relevancy tuning options.

+

Basic steps to configure search relevancy

+
    +
  1. Identify important fields: Determine which metadata fields are most relevant for your application's search results.
  2. +
  3. Set boost values: Adjust the boost values in the qf and pf parameters to prioritize those fields.
  4. +
  5. Test and iterate: After making changes, test the search results to ensure they meet expectations. You may need to tweak the boost values or add/remove fields based on the results.
  6. +
+

Key sections of solrconfig.xml

+

Query parameters

+
    +
  • defType: specifies the query parser (edismax)
  • +
  • qf: The query fields with boosting values that control how much weight each field has in the search relevancy.
  • +
  • pf: phrase boosting fields, used to boost exact phrase matches within the results.
  • +
+

Relevancy fields and boosting

+
    +
  • The qf and pf parameters list the fields to be searched and their associated boost values. The higher the boost value, the more weight that field has in determining the relevance of the search result.
  • +
+

Example:

+
    <str name="qf">
+          text^1
+          dct_description_ti^2
+          dct_creator_tmi^3
+          dct_publisher_ti^3
+          dct_isPartOf_tmi^4
+          dct_subject_tmi^5
+          dct_spatial_tmi^5
+          dct_temporal_tmi^5
+          dct_title_ti^6
+          dct_accessRights_ti^7
+          dct_provider_ti^8
+          dct_identifier_ti^10
+    </str>
+
+
    +
  • dct_description_ti^2: The Description field has a boost value of 2
  • +
  • dct_creator_tmi: The Creator field has a boost value of 3, making it more relevant than the Description field
  • +
  • The most relevant field is dct_identifier_ti
  • +
  • Higher boost values indicate greater importance in the search results.
  • +
+

Sorting results

+
    +
  • The sort parameter controls how the search results are orderd.
  • +
+

Example:

+
    <str name="sort">score desc, dct_title_sort asc</str>
+
+
    +
  • This sorts results first by score in descending order, then by dct_title_sort in ascending order.
  • +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/tutorials/index.html b/docs/tutorials/index.html new file mode 100644 index 00000000..69d4f849 --- /dev/null +++ b/docs/tutorials/index.html @@ -0,0 +1,2100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Tutorials - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Tutorials

+

These tutorials were developed by members of the GeoBlacklight community and are designed to help users get started with GeoBlacklight.

+
+

Works in progress

+

We are currently in the process of updating these tutorials. Please contact us using one of the options listed on our Connect page with any questions or challenges you encounter.

+
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/upgrade_version_2_0/index.html b/docs/upgrade_version_2_0/index.html new file mode 100644 index 00000000..42c33aae --- /dev/null +++ b/docs/upgrade_version_2_0/index.html @@ -0,0 +1,2230 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Upgrade to Version 2.0 - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Upgrade to Version 2.0

+ +

Upgrading to GeoBlacklight 2.0

+

While we suggest using the latest version of GeoBlacklight to take advantage of its modern features, sometimes you need to upgrade to an older release. GeoBlacklight 2.0 adds support for Blacklight 7.0, which itself includes several significant component upgrades:

+
    +
  • Bootstrap 4
  • +
  • Rails 5.2 support
  • +
  • Webpacker support (see below)
  • +
  • JSON-API support
  • +
  • Solr 7.2+ support
  • +
+

The Bootstrap 3 to Bootstrap 4 migration will require existing GeoBlacklight installations to update any local view or layout customizations they have created. See the Blacklight guide on updating Bootstrap for additional assistance.

+

Blacklight 7 upgrades

+

Update User Model

+

With the release of Blacklight 7, the Blacklight::Utils Module has been deprecated. User Models must have the following removed:

+
class User < ApplicationRecord
+  ## Please remove or comment this code:
+  ##
+  # if Blacklight::Utils.needs_attr_accessible?
+  #   attr_accessible :email, :password, :password_confirmation
+  # end
+
+  # Connects this user object to Blacklights Bookmarks.
+  include Blacklight::User
+  # Include default devise modules. Others available are:
+  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
+  devise :database_authenticatable, :registerable,
+         :recoverable, :rememberable, :validatable
+
+  # Method added by Blacklight; Blacklight uses #to_s on your
+  # user class to get a user-displayable login/identifier for
+  # the account.
+  def to_s
+    email
+  end
+end
+
+

Update CatalogController

+

Release 2.0 provides the ability to request JSON representations of Solr Documents by using the path /catalog/:id/raw In other words, append /raw to the end of a catalog URL stem. Note that this is different from previous versions of GeoBlacklight and is a result of Blacklight 7 incorporating a JSON:API compliant specification. Appending .json to the end of a catalog URL stem will now return a JSON:API compliant record, which is nested and not Solr compatible. For more information, see the metadata documentation.

+

The JSON record return is enabled within the CatalogController by setting config.raw_endpoint.enabled to true: +

  configure_blacklight do |config|
+
+    # Ensures that JSON representations of Solr Documents can be retrieved using
+    # the path /catalog/:id/raw
+    # Please see https://github.com/projectblacklight/blacklight/pull/2006/
+    config.raw_endpoint.enabled = true
+
+    ## Default parameters to send to solr for all search-like requests. See also SolrHelper#solr_search_params
+    ## @see https://lucene.apache.org/solr/guide/6_6/common-query-parameters.html
+    ## @see https://lucene.apache.org/solr/guide/6_6/the-dismax-query-parser.html#TheDisMaxQueryParser-Theq.altParameter
+    config.default_solr_params = {
+

+

Webpacker

+

Rails currently offers the ability for one to manage JavaScript source files and package dependencies using the Webpacker Gem. By default, this is available for usage in GeoBlacklight, but not enabled.

+

Requirements

+

Webpacker requires that either Yarn or the Node Package Manager be installed in the environment where the GeoBlacklight implementation is deployed.

+

Installing Webpacker

+

From within the root directory path of the GeoBlacklight application, please execute the following: +

bundle exec rails generate geoblacklight:webpacker --force
+

+

This will create a number of directories and files, most notably: + - package.json + - app/javascript/packs/application.js

+

Running yarn install or npm install, followed by yarn upgrade/npm update would be best in order to install and update any JavaScript dependencies.

+
Adding packs
+

In order to add JavaScript packs to a GeoBlacklight application, one should override the view template app/views/layouts/blacklight/base.html.erb (provided in https://github.com/projectblacklight/blacklight/blob/v7.0.1/app/views/layouts/blacklight/base.html.erb) with the following line: +

    <%= javascript_include_tag "application" %>
+    <%= javascript_pack_tag 'application' %>
+    <%= csrf_meta_tags %>
+    <%= content_for(:head) %>
+  </head>
+

+

For any new JS file added to app/javascript/packs, this will need to be added with a different name. For example, app/javascript/packs/my_new_script.js would be added with: +

    <%= javascript_include_tag "application" %>
+    <%= javascript_pack_tag 'application' %>
+    <%= javascript_pack_tag 'my_new_script' %>
+    <%= csrf_meta_tags %>
+    <%= content_for(:head) %>
+  </head>
+

+
Running the Webpack server
+

Release 2.0 uses the Foreman Gem in order to run both the Rails server and Webpack development server in parallel. This is useful for development environments where the Webpack dev. server listens for source file changes, and automatically recompiles packs. A file (named Procfile) within the root path of the application should be created with the following content: +

rails: bin/rails server --port=3000
+webpack: bin/webpack-dev-server
+

+

This can then be executed using bundle exec foreman start.

+

For deployments to testing, staging, or production environments, it is perhaps preferred to simply precompile the Webpack builds. This can be achieved with the task bundle exec rails webpacker:compile

+

GeoBlacklight updates

+

Dropped leaflet-rails; Vendorized a rails-savvy leaflet.js file

+

To fix a Leaflet FeatureLayer asset path issue, we decided to remove leaflet-rails as a gem dependency. Instead of the gem, we're now using a slightly modified leaflet.js file in vendor/javascripts.

+

For existing GBL installations, you will need to remove the require leaflet-rails statement from lib/geoblacklight/engine.rb to avoid an error upon application restart.

+

Added Spatial Search BBox overlapRatio Relevancy Option

+

A new Settings constant was added to provide optional support for Solr's BBoxField overlapRatio relevancy boosting within spatial searches.

+

For existing GBL installations, you will need to add the Settings.OVERLAP_RATIO_BOOST setting to your settings.yml file.

+
    # The bf boost value for overlap ratio
+    OVERLAP_RATIO_BOOST: '2'
+
+

If this option has a value, the boost will be appended to the spatial search like so:

+
    if Settings.OVERLAP_RATIO_BOOST
+      solr_params[:overlap] =
+        "{!field uf=* defType=lucene f=solr_bboxtype score=overlapRatio}Intersects(#{envelope_bounds})"
+      solr_params[:bf] = "$overlap^#{Settings.OVERLAP_RATIO_BOOST}"
+    end
+
+

Relevancy is Best Tuned Locally

+

Everyone's idea of relevancy is different. The default boost value here ("2") might not be the best for your collection or user needs. Please adjust this relevancy boost as necessary to ensure best results for your GBL install.

+

Homepage

+

The _homepage_text.html.erb view partial has been updated to use a view component for rendering the featured facets feature. You should update any local customizations to this file to use the components.

+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/upgrade_version_4_0/index.html b/docs/upgrade_version_4_0/index.html new file mode 100644 index 00000000..73aa31d8 --- /dev/null +++ b/docs/upgrade_version_4_0/index.html @@ -0,0 +1,2528 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Upgrade to Version 4.0 - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Upgrading to GeoBlacklight 4.0

+

There are several steps to complete this major release upgrade. Detailed notes follow for each of the following upgrade steps:

+
    +
  1. Gemfile
  2. +
  3. Apache Solr
  4. +
  5. Data Migration
  6. +
  7. Application Configuration
  8. +
  9. Application Changes
  10. +
+

1. Gemfile

+

Update your Gemfile to GBL v4:

+
  gem 'geoblacklight', '~> 4.0'
+
+

2. Apache Solr

+

GeoBlacklight now requires Solr 8.3 or higher.

+

GBL's Solr configuration files are updated to reflect the Aardvark metadata element list and support new complex geometries features. See the default versions of schema.xml and solrconfig.xml and update your local files as necessary.

+
    +
  • solr/config/schema.xml
  • +
  • solr/config/solrconfig.xml
  • +
+

3. Data Migration

+

Migrate your Solr documents from the GBL v1.0 metadata standard to OGM Aardvark. GBL community documentation and migration tools are listed below:

+ +

4. Application Configuration

+

Review the configuration files for your GBL instance. You will need to update your settings.yml file and catalog_controller.rb file to use the new Aardvark field mappings. See the default versions of these files in GeoBlacklight v4 and alter your files as necessary:

+

You will also need to search your local application code for any old Settings.FIELDS.(X) mappings and update them as necessary.

+

Settings

+

config/settings.yml

+

Many GBLv4 configuration changes take place in the settings.yml file.

+

List of GBLv4 settings.yml changes:

+
    +
  • Solr field mappings: Settings.FIELDS
  • +
  • GeoBlacklight Params: Settings.GBL_PARAMS
  • +
  • Relationships to display: Settings.RELATIONSHIPS_SHOWN
  • +
  • Parent/Child SVG Icon titles
  • +
+

Solr field mappings: Settings.FIELDS

+

With the adoption of the OGM Aardvark metadata schema, we need to update all the Settings.FIELDS values for Aardvark. Here are the default GBLv4 values. If you have additional local customizations here, you'll need carry those over, too.

+
# Solr field mappings
+FIELDS:
+  :ACCESS_RIGHTS: 'dct_accessRights_s'
+  :ALTERNATIVE_TITLE: 'dct_alternative_sm'
+  :CENTROID: 'dcat_centroid'
+  :CREATOR: 'dct_creator_sm'
+  :DATE_ISSUED: 'dct_issued_s'
+  :DATE_RANGE: 'gbl_dateRange_drsim'
+  :DESCRIPTION: 'dct_description_sm'
+  :FORMAT: 'dct_format_s'
+  :FILE_SIZE: 'gbl_fileSize_s'
+  :GEOREFERENCED: 'gbl_georeferenced_b'
+  :ID: 'id'
+  :IDENTIFIER: 'dct_identifier_sm'
+  :INDEX_YEAR: 'gbl_indexYear_im'
+  :IS_PART_OF: 'dct_isPartOf_sm'
+  :IS_REPLACED_BY: 'dct_isReplacedBy_sm'
+  :THEME: 'dcat_theme_sm'
+  :KEYWORD: 'dcat_keyword_sm'
+  :LANGUAGE: 'dct_language_sm'
+  :LAYER_MODIFIED: 'gbl_mdModified_dt'
+  :LICENSE: 'dct_license_sm'
+  :MEMBER_OF: 'pcdm_memberOf_sm'
+  :METADATA_VERSION: 'gbl_mdVersion_s'
+  :MODIFIED: 'gbl_mdModified_dt'
+  :OVERLAP_FIELD: 'solr_bboxtype'
+  :PUBLISHER: 'dct_publisher_sm'
+  :PROVIDER: 'schema_provider_s'
+  :REFERENCES: 'dct_references_s'
+  :RELATION: 'dct_relation_sm'
+  :REPLACES: 'dct_replaces_sm'
+  :RESOURCE_CLASS: 'gbl_resourceClass_sm'
+  :RESOURCE_TYPE: 'gbl_resourceType_sm'
+  :RIGHTS: 'dct_rights_sm'
+  :RIGHTS_HOLDER: 'dct_rightsHolder_sm'
+  :SOURCE: 'dct_source_sm'
+  :SPATIAL_COVERAGE: 'dct_spatial_sm'
+  :GEOMETRY: 'locn_geometry'
+  :SUBJECT: 'dct_subject_sm'
+  :SUPPRESSED: 'gbl_suppressed_b'
+  :TEMPORAL_COVERAGE: 'dct_temporal_sm'
+  :TITLE: 'dct_title_s'
+  :VERSION: 'dct_isVersionOf_sm'
+  :WXS_IDENTIFIER: 'gbl_wxsIdentifier_s'
+
+

GeoBlacklight Params

+

Settings.GBL_PARAMS

+

Add the GBL_PARAMS array to settings.yml to whitelist the GBL application params so they are appended to controller methods and search builder queries.

+
# Non-search-field GeoBlacklight application permitted params
+GBL_PARAMS:
+  - :bbox
+  - :email
+  - :file
+  - :format
+  - :id
+  - :logo
+  - :provider
+  - :type
+  - :BBOX
+  - :HEIGHT
+  - :LAYERS
+  - :QUERY_LAYERS
+  - :URL
+  - :WIDTH
+  - :X
+  - :Y
+
+

Relationships to display

+

Settings.RELATIONSHIPS_SHOWN

+

The number of item/parent/collection relationships supported within GBLv4 has grown considerably. Add these default values to support the new relationships. You can also add additional relationship keys, fields, and query_types to support local customizations.

+
# Relationships to display
+RELATIONSHIPS_SHOWN:
+  MEMBER_OF:
+    field: pcdm_memberOf_sm
+    query_type: ancestors
+    icon: nil
+    label: geoblacklight.relations.member_of
+  PART_OF_ANCESTORS:
+    field: dct_isPartOf_sm
+    query_type: ancestors
+    icon: nil
+    label: geoblacklight.relations.part_of_ancestors
+  PART_OF_DESCENDANTS:
+    field: dct_isPartOf_sm
+    query_type: descendants
+    icon: child-item
+    label: geoblacklight.relations.part_of_descendants
+  RELATION:
+    field: dct_relation_sm
+    query_type: ancestors
+    icon: nil
+    label: geoblacklight.relations.relation
+  REPLACES:
+    field: dct_replaces_sm
+    query_type: ancestors
+    icon: nil
+    label: geoblacklight.relations.replaces
+  REPLACED_BY:
+    field: dct_isReplacedBy_sm
+    query_type: descendants
+    icon: nil
+    label: geoblacklight.relations.replaced_by
+  SOURCE_ANCESTORS:
+    field: dct_source_sm
+    query_type: ancestors
+    icon: parent-item
+    label: geoblacklight.relations.ancestor
+  SOURCE_DESCENDANTS:
+    field: dct_source_sm
+    query_type: descendants
+    icon: child-item
+    label: geoblacklight.relations.descendant
+  VERSION_OF:
+    field: dct_isVersionOf_sm
+    query_type: descendants
+    icon: nil
+    label: geoblacklight.relations.version_of
+
+

Parent/Child SVG Icon titles

+

Replace these relationship icon file names.

+
SOURCE_ANCESTORS:
+  field: dct_source_sm
+  query_type: ancestors
+- icon: pagelines-brands
++ icon: parent-item
+  label: geoblacklight.relations.ancestor
+SOURCE_DESCENDANTS:
+  field: dct_source_sm
+  query_type: descendants
+- icon: leaf
++ icon: child-item
+  label: geoblacklight.relations.descendant
+VERSION_OF:
+  field: dct_isVersionOf_sm
+
+

Viewer Controls

+

Settings.LEAFLET.VIEWERS.*.CONTROLS

+

GBLv4 includes native support for the Leaflet.fullscreen plugin. Update your Leaflet configuration to include the Fullscreen viewer option.

+
# Settings for leaflet
+LEAFLET:
+  ...
+  VIEWERS:
+    DYNAMICMAPLAYER:
+      CONTROLS:
+        - 'Opacity'
+        - 'Fullscreen'
+    FEATURELAYER:
+      CONTROLS:
+        - 'Opacity'
+        - 'Fullscreen'
+    IIIF:
+      CONTROLS:
+        - 'Fullscreen'
+    IMAGEMAPLAYER:
+      CONTROLS:
+        - 'Opacity'
+        - 'Fullscreen'
+    INDEXMAP:
+      CONTROLS:
+        - 'Fullscreen'
+    TILEDMAPLAYER:
+      CONTROLS:
+        - 'Opacity'
+        - 'Fullscreen'
+    WMS:
+      CONTROLS:
+        - 'Opacity'
+        - 'Fullscreen'
+
+

CatalogController

+

app/controllers/catalog_controller.rb

+

Besides the settings.yml configuration changes above, the catalog_controller.rb file holds a great deal of application configuration and it needs to be updated for the new Settings.FIELD values.

+

It may be helpful to review the diff of changes to catalog_controller.rb from v3.8.0 to v4.0.0

+

Here is a list of GBL v4 catalog_controller.rb changes:

+

Default Solr Params

+

config.default_document_solr_params

+

This uses the Settings.FIELDS.ID field now.

+
  ## Default parameters to send on single-document requests to Solr...
+  config.default_document_solr_params = {
+    :qt => 'document',
+    :q => "{!raw f=#{Settings.FIELDS.ID} v=$id}"
+  }
+
+

View Defaults

+

config.view defaults

+

Adds the "map" split view for catalog#index

+
    # GeoBlacklight Defaults
+    # * Adds the "map" split view for catalog#index
+    config.view.split(partials: ['index'])
+    config.view.delete_field('list')
+
+

Facet Fields

+

config.add_facet_field(s)

+

These are all now mapped to Aardvark fields. Note: 'icon_facet' partials are now replaced by the item_component: Geoblacklight::IconFacetItemComponent

+
    # FACETS
+
+    # DEFAULT FACETS
+    # to add additional facets, use the keys defined in the settings.yml file
+    config.add_facet_field Settings.FIELDS.INDEX_YEAR, :label => 'Year', :limit => 10
+    config.add_facet_field Settings.FIELDS.SPATIAL_COVERAGE, :label => 'Place', :limit => 8
+    config.add_facet_field Settings.FIELDS.ACCESS_RIGHTS, label: 'Access', limit: 8, item_component: Geoblacklight::IconFacetItemComponent
+    config.add_facet_field Settings.FIELDS.RESOURCE_CLASS, label: 'Resource Class', :limit => 8
+    config.add_facet_field Settings.FIELDS.RESOURCE_TYPE, label: 'Resource Type', :limit => 8
+    config.add_facet_field Settings.FIELDS.FORMAT, :label => 'Format', :limit => 8
+    config.add_facet_field Settings.FIELDS.SUBJECT, :label => 'Subject', :limit => 8
+    config.add_facet_field Settings.FIELDS.THEME, :label => 'Theme', :limit => 8
+    config.add_facet_field Settings.FIELDS.CREATOR, :label => 'Creator', :limit => 8
+    config.add_facet_field Settings.FIELDS.PUBLISHER, :label => 'Publisher', :limit => 8
+    config.add_facet_field Settings.FIELDS.PROVIDER, label: 'Provider', limit: 8, item_component: Geoblacklight::IconFacetItemComponent
+    config.add_facet_field Settings.FIELDS.GEOREFERENCED, :label => 'Georeferenced', :limit => 3
+
+

GBL Application Facets

+

Our map-based search feature is now run via a series of (Geo)Blacklight class extensions which require this configuration:

+
    # GEOBLACKLIGHT APPLICATION FACETS
+
+    # Map-Based "Search Here" Feature
+    # item_presenter       - Defines how the facet appears in the GBL UI
+    # filter_query_builder - Defines the query generated for Solr
+    # filter_class         - Defines how to add/remove facet from query
+    # label                - Defines the label used in contstraints container
+    config.add_facet_field Settings.FIELDS.GEOMETRY, item_presenter: Geoblacklight::BboxItemPresenter, filter_class: Geoblacklight::BboxFilterField, filter_query_builder: Geoblacklight::BboxFilterQuery, within_boost: Settings.BBOX_WITHIN_BOOST, overlap_boost: Settings.OVERLAP_RATIO_BOOST, overlap_field: Settings.FIELDS.OVERLAP_FIELD, label: 'Bounding Box'
+
+

Item Relationship Facets

+

To display item-to-item relationships, add this block below:

+
    # Item Relationship Facets
+    # * Not displayed to end user (show: false)
+    # * Must be present for relationship "Browse all 4 records" links to work
+    # * Label value becomes the search contraint filter name
+    config.add_facet_field Settings.FIELDS.MEMBER_OF, label: "Member Of", show: false
+    config.add_facet_field Settings.FIELDS.IS_PART_OF, label: "Is Part Of", show: false
+    config.add_facet_field Settings.FIELDS.RELATION, label: "Related", show: false
+    config.add_facet_field Settings.FIELDS.REPLACES, label: "Replaces", show: false
+    config.add_facet_field Settings.FIELDS.IS_REPLACED_BY, label: "Is Replaced By", show: false
+    config.add_facet_field Settings.FIELDS.SOURCE, label: "Source", show: false
+    config.add_facet_field Settings.FIELDS.VERSION, label: "Is Version Of", show: false
+
+

Index Fields

+

config.add_index_field(s)

+

The "Index Fields" are the values that appear on search results lists. These have been mapped to Aardvark fields.

+
    config.add_index_field Settings.FIELDS.INDEX_YEAR
+    config.add_index_field Settings.FIELDS.CREATOR
+    config.add_index_field Settings.FIELDS.DESCRIPTION, helper_method: :snippit
+    config.add_index_field Settings.FIELDS.PUBLISHER
+
+

Show Fields

+

config.add_show_field(s)

+

The "Show Fields" are the values that appear on an item detail page. These have been mapped to Aardvark fields, and many non-activated optional fields have been added to the default catalog_controller.rb file, too.

+

View "Show Field" configuration online

+

Sort Fields

+

config.add_sort_field(s)

+

The GBLv4 default sort fields options have been expanded. Here is the new default value for sorting:

+
    config.add_sort_field 'score desc, dct_title_sort asc', :label => 'Relevance'
+    config.add_sort_field "#{Settings.FIELDS.INDEX_YEAR} desc, dct_title_sort asc", :label => 'Year (Newest first)'
+    config.add_sort_field "#{Settings.FIELDS.INDEX_YEAR} asc, dct_title_sort asc", :label => 'Year (Oldest first)'
+    config.add_sort_field 'dct_title_sort asc', :label => 'Title (A-Z)'
+    config.add_sort_field 'dct_title_sort desc', :label => 'Title (Z-A)'
+
+

Web Services Changes

+

Our web_services method is no longer a show tool partial. Migrating from GBLv3 to GBLv4, you will need to remove your config.add_show_tools_partial :web_services... line and add the new def web_services method:

+

View new method online

+
  # Custom tools for GeoBlacklight
+- config.add_show_tools_partial :web_services, if: proc { |_context, _config, options| options[:document] && (Settings.WEBSERVICES_SHOWN & options[:document].references.refs.map(&:type).map(&:to_s)).any? }
+
+
  def web_services
+    @response, @documents = action_documents
+
+    respond_to do |format|
+      format.html do
+        return render layout: false if request.xhr?
+        # Otherwise draw the full page
+      end
+    end
+  end
+
+

Locales

+

config/locales/geoblacklight.en.yml

+

We have added additional relations entries for GBLv4 config/locales/geoblacklight.en.yml.

+

If you have local overrides or customizations to this file, please include the new relations entries locally.

+

5. Application Changes

+

ApplicationController

+

app/controllers/application_controller.rb

+

GBL installer now includes a before_action method to permit GBL application params. You'll need to add this code to your application_controller.rb file:

+
  before_action :allow_geoblacklight_params
+
+  def allow_geoblacklight_params
+    # Blacklight::Parameters will pass these to params.permit
+    blacklight_config.search_state_fields.append(Settings.GBL_PARAMS)
+  end
+
+

SearchBuilder

+

app/models/search_builder.rb

+

GBL's default search builder concerns have changed. We've added Geoblacklight::SuppressedRecordsSearchBehavior. You'll need this code in your application's search_builder.rb class:

+
# frozen_string_literal: true
+class SearchBuilder < Blacklight::SearchBuilder
+  include Blacklight::Solr::SearchBuilderBehavior
+  include Geoblacklight::SuppressedRecordsSearchBehavior
+
+

Stylesheets

+

app/assets/stylesheets/application.scss

+

GBL v4 no longer vendorizes the leaflet-label stylesheet. Check your local stylesheet files and remove any *= require leaflet-label or @import 'leaflet-label'; lines.

+
- /*
+- *= require leaflet-label
+- */
+
+

JavaScripts

+

app/assets/javascript/

+

GBL v4 adds a new Leaflet control: Leaflet.fullzoom. If you previously added this feature to your local GBL instance, you'll want to remove your custom implementation. This control can be added to your maps via the settings.yml file (see documentation above).

+

Homepage

+

The _homepage_text.html.erb view partial has been updated to use a view component for rendering the featured facets feature. You should update any local customizations to this file to use the components.

+
    <div class='col-sm'>
+      <%= content_tag :h3, t('geoblacklight.home.category_heading') %>
+      <div class='row'>
+        <%= render(Geoblacklight::HomepageFeatureFacetComponent.new(icon: 'home', label: 'geoblacklight.home.institution', facet_field: Settings.FIELDS.PROVIDER, response: @response)) %>
+
+        <%= render(Geoblacklight::HomepageFeatureFacetComponent.new(icon: 'arrow-circle-down', label: 'geoblacklight.home.data_type', facet_field: Settings.FIELDS.RESOURCE_TYPE, response: @response)) %>
+      </div>
+      <div class='row'>
+        <%= render(Geoblacklight::HomepageFeatureFacetComponent.new(icon: 'globe', label: 'geoblacklight.home.placename', facet_field: Settings.FIELDS.SPATIAL_COVERAGE, response: @response)) %>
+
+        <%= render(Geoblacklight::HomepageFeatureFacetComponent.new(icon: 'tags', label: 'geoblacklight.home.subject', facet_field: Settings.FIELDS.SUBJECT, response: @response)) %>
+      </div>
+    </div>
+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/user_authentication/index.html b/docs/user_authentication/index.html new file mode 100644 index 00000000..fa877031 --- /dev/null +++ b/docs/user_authentication/index.html @@ -0,0 +1,2101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + User Authentication - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

User Authentication

+

GeoBlacklight facilitates the creation of user accounts and stores these in the database configured in config/database.yml. User accounts allow users to "bookmark" records for easy retrieval in the future. (Anonymous users can also bookmark records, though the the bookmarks will be lost at the end of a browser session.) In some implementations, user authentication is linked with institutional identities, which can drive access to certain privileged datsets (more on this below).

+

These capabilities are all inherited directly from Blacklight, which uses the devise and devise-guests gems to handle user accounts. For more about customizing this aspect of GeoBlacklight, see the relevant Blacklight documentation.

+
+

Cleaning up guests

+

By default, the devise-guests gem will automatically create user records every time an anonymous user visits GeoBlacklight, so it is advisable to schedule a regular cleanup task. See "Useful Cron Tasks" in Implementation Recommendations for more information.

+
+

Using Institutional Authentication Backends

+

Typically, institutions will configure GeoBlacklight to use an existing user authentication backend instead of using the default configuration. This type of integration also allows for data access permissions to be linked to authenticated users.

+

Devise can be set up to use OmniAuth, which in turn can be extended to use various authentication providers, for example:

+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/gbl-favicon.png b/images/gbl-favicon.png new file mode 100644 index 00000000..56074b19 Binary files /dev/null and b/images/gbl-favicon.png differ diff --git a/images/gbl-multipage.png b/images/gbl-multipage.png new file mode 100644 index 00000000..bb1d20af Binary files /dev/null and b/images/gbl-multipage.png differ diff --git a/images/geoblacklight-homepage.jpg b/images/geoblacklight-homepage.jpg new file mode 100644 index 00000000..30d74f40 Binary files /dev/null and b/images/geoblacklight-homepage.jpg differ diff --git a/images/geoblacklight-itempage.jpg b/images/geoblacklight-itempage.jpg new file mode 100644 index 00000000..2f3a9ff8 Binary files /dev/null and b/images/geoblacklight-itempage.jpg differ diff --git a/images/geoblacklight-logo-small.png b/images/geoblacklight-logo-small.png new file mode 100644 index 00000000..00b9d191 Binary files /dev/null and b/images/geoblacklight-logo-small.png differ diff --git a/images/geoblacklight-logo-small@2x.png b/images/geoblacklight-logo-small@2x.png new file mode 100644 index 00000000..3adb4cb1 Binary files /dev/null and b/images/geoblacklight-logo-small@2x.png differ diff --git a/images/geoblacklight-logo.png b/images/geoblacklight-logo.png new file mode 100644 index 00000000..cebf77f3 Binary files /dev/null and b/images/geoblacklight-logo.png differ diff --git a/images/geoblacklight-logo@2x.png b/images/geoblacklight-logo@2x.png new file mode 100644 index 00000000..10196187 Binary files /dev/null and b/images/geoblacklight-logo@2x.png differ diff --git a/index.html b/index.html new file mode 100644 index 00000000..2f13affa --- /dev/null +++ b/index.html @@ -0,0 +1,2099 @@ + + + + + + + + + + + + + + + + + + + + + + + GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + +

Home

+ + + +

GeoBlacklight Logo

+

A multi-institutional open-source collaboration building a
better way to find and share geospatial data

+ +

+

How can the Geoblacklight Community help you?

+ +
+ +
+

Participating institutions

+
    +
  • GeoBlacklight provides a customizable platform for delivering geospatial information seamlessly to end users
  • +
  • Participants can connect to other contributors through our active communication channels and learn best practices for software and metadata implementation.
  • +
+
+ +
+

End users

+
    +
  • GeoBlacklight makes geospatial resources accessible to all users regardless of expertise with user-friendly searches through a map interface, keyword searches, and faceted browsing.
  • +
  • GeoBlacklight provides easy download and export options for reusing geospatial resources in a wide variety of academic ventures.
  • +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/javascripts/tablesort.js b/javascripts/tablesort.js new file mode 100644 index 00000000..6a5afcf2 --- /dev/null +++ b/javascripts/tablesort.js @@ -0,0 +1,6 @@ +document$.subscribe(function() { + var tables = document.querySelectorAll("article table:not([class])") + tables.forEach(function(table) { + new Tablesort(table) + }) +}) diff --git a/pdfs/GeoBlacklight-Concept-Design.pdf b/pdfs/GeoBlacklight-Concept-Design.pdf new file mode 100644 index 00000000..f0f15df0 Binary files /dev/null and b/pdfs/GeoBlacklight-Concept-Design.pdf differ diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 00000000..2bff4f77 --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"A multi-institutional open-source collaboration building a better way to find and share geospatial data How can the Geoblacklight Community help you? Participating institutions
  • GeoBlacklight provides a customizable platform for delivering geospatial information seamlessly to end users
  • Participants can connect to other contributors through our active communication channels and learn best practices for software and metadata implementation.
End users
  • GeoBlacklight makes geospatial resources accessible to all users regardless of expertise with user-friendly searches through a map interface, keyword searches, and faceted browsing.
  • GeoBlacklight provides easy download and export options for reusing geospatial resources in a wide variety of academic ventures.
"},{"location":"about/","title":"About","text":"

GeoBlacklight is an open-source software application for discovering geospatial content, including GIS datasets, web services, and digitized paper maps.

"},{"location":"about/#the-geoblacklight-ecosystem","title":"The GeoBlacklight Ecosystem","text":""},{"location":"about/#technical-core-stack","title":"Technical Core Stack","text":"
  • GeoBlacklight: A Ruby on Rails engine
  • Blacklight: A widely-used open-source discovery framework
  • Apache Solr: Search index to make geospatial metadata searchable.
  • SQL database: For production uses, GeoBlacklight installations require an SQL database such as MySQL, MariaDB, or PostgreSQL.\u00a0
"},{"location":"about/#external-services","title":"External Services","text":"

One of GeoBlacklight's strengths is its ability to serve as a bridge to geospatial content hosted on various platforms, simplifying the way users find and interact with data. Instead of storing data directly, GeoBlacklight focuses on integrating with existing data repositories and web services.GeoBlacklight does this through providing useful preview, download, and exports of open standards-based services, including Web Mapping Services (WMS), Web Feature Services (WFS), ArcGIS Rest API, and International Image Interoperability Framework (IIIF). There is also support for externally referenced metadata viewing and file download support.

This includes offering previews, downloads, and access to data through web standards such as Web Mapping Services (WMS), Web Feature Services (WFS), the ArcGIS Rest API, and the International Image Interoperability Framework (IIIF). Additionally, GeoBlacklight enables supplemental metadata views and downloads.

Examples of technology providing external services:

  • GeoServer: previews and generated downloads via OGC Web Services
  • ArcGIS Platforms: previews via ArcGIS REST Services
  • Various IIIF Servers: previews and generated downloads of scanned images
  • Digital Repositories: direct downloads of datasets and related files
"},{"location":"about/#metadata","title":"Metadata","text":"

GeoBlacklight uses the OpenGeoMetadata Aardvark Metadata Schema by default, which has been designed to privilege discovery use cases. It supports text searching, faceted searching and refinement, and spatial searching to improve relevance and findability of data. Visit OpenGeoMetadata for more information and full documentation.

"},{"location":"about/#key-features","title":"Key Features","text":"
  • Text and spatial search with ranking
  • Facet by institution, year, publisher, data type, access, format
  • Facet by place, subject
  • Results list view with icons, snippets, and map view of bounding boxes
  • Spatial search on map in result list
  • Detail map view for WMS features with feature inspection
  • IIIF scanned map viewer
  • Download the original file (Shapefile, GeoTIFF, GeoJSON, Esri Geodatabase, GeoPackage, or other SQLite database)
  • Download generated Shapefile/GeoTIFF/KML/GeoJSON
  • Built-in sample Solr 8.3+ index
  • Built on top of Blacklight platform
  • Search history
  • Bookmark layers
  • Share link via email
  • Sort by relevance, year, title
  • Customizable skin and facets
"},{"location":"about/#technical-values","title":"Technical Values","text":"
  • Our core focus is geospatial discovery. This focus initially was limited to discretely catalogued data objects, but has expanded over time to include a wider range of information sources.

  • We emphasize end-user experience, including inclusivity and accessibility in design features.

  • We prioritize stability by semantically versioning our application releases and metadata schemas.

  • We aim for GeoBlacklight to be simple to adopt and easy to maintain. We recognize that many adopters are in the cultural heritage space where metadata and software development resources can be limited.

  • We make GeoBlacklight customizable for common use cases, and extensible to a plugin for a less-common use case.

  • We leverage existing communities. Building on established standards gives us more bandwidth to focus on discovery and developing plugins.

  • Excellent geospatial analysis and mapping tools already exist. Rather than build new ones in GeoBlacklight, we focus on integration with these existing tools.

"},{"location":"about/#our-development-practices","title":"Our Development Practices","text":"
  • Open source model: GeoBlacklight is an open source software project licensed using the Apache License, version 2.0. Our development practices have been codified in a contribution guide since December 2015 and we use semantic versioning to release the Ruby on Rails engine to RubyGems. Changes are made to the codebase using pull requests to the GitHub source code repository.

  • Connected frameworks: Many of the development practices for the GeoBlacklight project have foundations in other open source software communities. A strategic design decision was made to build on existing pools of expertise in organizations with Blacklight and Samvera rather than build a completely custom system. The project also relies heavily on configuration and extensibility as useful patterns for adopters making customizations.

  • Decision-making: Much of the technical decision-making is driven from the original GeoBlacklight Concept Design document and has been further distilled into our GeoBlacklight Technical Values. Major and minor decisions are made using informal consensus.

  • Testing: GeoBlacklight has Continuous Integration Testing, and tests are expected to be written with code contributions to the project. The project also implements both Ruby and JavaScript style guides to ensure a stylistically similar codebase.

  • Funding: There is no funding model for GeoBlacklight, and most development comes through volunteered or assigned time from contributing organizations. Some projects have received grants or dedicated funds to build their GeoBlacklight applications. Our community also includes private vendors and independent freelancers that have contributed to the project through contracted work.

  • GeoBlacklight Software Versioning: GeoBlacklight follows the practice of Semantic Versioning for software releases. The declared semantically versioned public API includes:

    • the public GeoBlacklight Ruby codebase classes
    • the GeoBlacklight JavaScript interface
    • the GeoBlacklight view interface
"},{"location":"about/#connected-projects","title":"Connected Projects","text":"

The GeoBlacklight software stack consists of several open source software projects which work together to enable a better discovery experience.

"},{"location":"about/#geoblacklight","title":"GeoBlacklight","text":"

GeoBlacklight is the main discovery interface for geospatial data. It is developed as a Ruby on Rails engine and built on top of the popular open-source discovery interface Blacklight.

"},{"location":"about/#dockerized-geoblacklight","title":"Dockerized GeoBlacklight","text":"

Developers from Harvard University have created a built instance of GeoBlacklight in a Docker context. This will allow new and existing users to test and develop an instance of GeoBlacklight within the Docker environment.

https://github.com/harvard-lts/GeoBlacklightDockerized

"},{"location":"about/#opengeometadata","title":"OpenGeoMetadata","text":"

GeoBlacklight is built to use the OpenGeoMetadata schema, which is designed for GIS resource discovery and focuses mainly on discovery use cases. Text search, faceted search and refinement, and spatial search and relevancy are among the primary features that the schema enables.

https://opengeometadata.org

"},{"location":"about/#openindexmaps","title":"OpenIndexMaps","text":"

A community format for sharing index maps in GeoBlacklight and a repository that hosts community-produced GeoJSON index maps that facilitate discovery within GeoBlacklight portals.

https://openindexmaps.org/

"},{"location":"about/#geoblacklight-sidecar-images","title":"GeoBlacklight Sidecar images","text":"

This GeoBlacklight plugin captures remote images from geographic web services and saves them locally.

https://github.com/geoblacklight/geoblacklight_sidecar_images

"},{"location":"about/#geomonitor","title":"Geomonitor","text":"

GeoMonitor is a Ruby on Rails application used to monitor geowebservices. It was built out of the premise that users who are looking for and find data should actually be able to access and use it. The application is setup to periodically monitor WMS web services and log data on a layers availability.

https://github.com/geoblacklight/geo_monitor

"},{"location":"community/","title":"Community","text":"

Participants in the GeoBlacklight community come from a variety of professional and intellectual backgrounds (including librarians, software developers, metadata specialists, applied researchers, and others), but we share a common interest in making reliable and high-quality geospatial data easily accessible to members of the research community and the broader public. Many of us work in libraries and other cultural heritage institutions that deploy (or are planning to deploy) GeoBlacklight instances to disseminate and publicize their spatial data collections.

Discovery services and metadata are key challenges for organizations who provide geospatial data. GeoBlacklight connects expertise from the digital library and geospatial communities to provide a better experience for users to find geospatial data.

Anyone interested in spatial data infrastructures, libraries, GIS, maps, data curation, open source software, and related topics, is welcome to join us. Depending on their skills and interests, participants contribute to the community in any number of ways (for instance, by attending meetings, writing documentation, developing metadata best practices, engaging in outreach, and writing code). Participating in the community is especially beneficial to those who are implementing or maintaining GeoBlacklight as a spatial data discovery interface within their own home institutions.

"},{"location":"community/#join-the-community","title":"Join the Community","text":""},{"location":"community/#volunteer-for-a-community-role","title":"Volunteer for a Community Role","text":"

We have several designated roles for building and sustaining our community.

Read about our roles and view vacancies.

"},{"location":"community/#join-a-workgroup-or-interest-group","title":"Join a Workgroup or Interest Group","text":"

We have short term and ongoing groups devoted to specific topics. Or, start a new one!

View our current groups

"},{"location":"community/#follow-our-google-group","title":"Follow our Google Group","text":"

Follow general project discussions and feature announcements.

Join the GeoBlacklight Google Group

"},{"location":"community/#chat-on-slack","title":"Chat on Slack","text":"

Chat with others in the community and to work through technical questions.

Join GeoBlacklight Slack

"},{"location":"community/#attend-a-monthly-community-meeting","title":"Attend a Monthly Community Meeting","text":"

The GeoBlacklight Community Meeting takes place on the second Wednesday of every month. The meetings are open to anyone interested in hearing more about the status of the GeoBlacklight software and associated projects.

Read past meeting agendas

"},{"location":"community/#participate-in-a-community-sprint","title":"Participate in a Community Sprint","text":"

The GeoBlacklight Community collaborates on a 2-week sprint every Winter and Summer. Participation in the sprint is open to anyone interested in working on GeoBlacklight. While similar to a traditional code sprint, the GeoBlacklight Community Sprints also include tackling issues around documentation, metadata, community governance, and more.

Read about our sprints

"},{"location":"community/#share-metadata","title":"Share Metadata","text":"

GeoBlacklight gets even better when organizations share metadata. Contribute your metadata to OpenGeoMetadata or use it to populate your own GeoBlacklight installation.

Visit OpenGeoMetadata

"},{"location":"community/#contribute-to-the-software","title":"Contribute to the Software","text":""},{"location":"community/#report-an-issue","title":"Report an Issue","text":"

Find a bug you want to report? Have an idea for a new feature? Anyone can create an issue in GeoBlacklight's GitHub repository for the community to address.

Browse and submit issues

"},{"location":"community/#contribute-code","title":"Contribute Code","text":"

As open-source software, GeoBlacklight depends on contributions from our developer community. Anyone is welcome to code for the community to review.

Read our software contribution guide

"},{"location":"community/#write-documentation","title":"Write Documentation","text":"

Help keep our documentation up-to-date or fix information that is missing or unclear.

Read our website contribution guide

"},{"location":"community/#events","title":"Events","text":""},{"location":"community/#community-roles","title":"Community Roles","text":""},{"location":"community/#administrative-roles","title":"Administrative Roles","text":"

Meeting Facilitators: Karen Majewicz + 1 vacancy

  • Manage Google calendar
  • Set up and host monthly meetings
  • Prepare agendas

Platform Management Team: 2 vacancies

  • Manage GitHub users and roles
  • Manage Google group permissions, roles, etc.
  • Manage Google Drive members
  • Maintain Slack
"},{"location":"community/#documentation-roles","title":"Documentation Roles","text":"

Public Content Manager: 1 vacancy

  • Keep the public website(s) up to date
  • Write periodic blog posts, including sprint wrap-up posts

Technical Writing Lead: 1 vacancy

  • Keep track of what code, metadata, and tutorials need updating
  • Manage GitHub issues related to documentation
  • Coordinate volunteers to update documentation as necessary
"},{"location":"community/#technical-roles","title":"Technical Roles","text":"

Designated Code Mergers: see GitHub Teams

  • Members of the GeoBlacklight Developers team in GitHub
  • Review pull requests on behalf of the team, or assign individual code reviewers based on expertise

Release Coordinators: Eric Larson + 1 vacancy

  • Cut tags and releases in the GitHub repository, and write release notes
  • Publish the GeoBlacklight Ruby gem
  • Publish Javascript to npm
"},{"location":"community/#workgroups-and-interest-groups","title":"Workgroups and Interest Groups","text":""},{"location":"community/#issue-triage-workgroup","title":"Issue Triage Workgroup","text":"

Meets quarterly to review and prioritize GitHub issues related to code.

  • Eliot Jordan
  • Eric Larson
  • Karen Majewicz
  • Keith Jenkins
  • Maura Carbone
"},{"location":"community/#governance-workgroup","title":"Governance Workgroup","text":"

Improves our communication around change management, connected projects, and community roles.

  • Karen Majewicz
  • Becky Seifried
  • Eliot Jordan
"},{"location":"community/#georeferencing-interest-group","title":"Georeferencing Interest Group","text":"

Discusses georeferencing projects and strategies to enhance GeoBlacklight\u2019s georeferenced map capabilities.

  • Adam Cox
  • Alex Marden
  • Eliot Jordan
  • Kim Leaman
  • Marc McGee
  • Michael Shensky
  • Stephen Appel
"},{"location":"community/#geoblacklight-institutional-contributors","title":"GeoBlacklight Institutional Contributors","text":"

GeoBlacklight has been collaboratively developed by affiliates of the following institutions:

  • Auraria Library
  • Big Ten Academic Alliance
  • Cornell University
  • MIT Libraries
  • Harvard University Library
  • Johns Hopkins University
  • New York University Libraries
  • Princeton University Library
  • Stanford University Libraries
  • University of California San Diego
  • University of California Berkeley
  • University of California Santa Barbara
  • University of Illinois at Urbana-Champaign
  • University of Massachusetts Amherst
  • University of Minnesota Libraries
  • University of Pennsylvania Libraries
  • The University of Texas at Austin
  • University of Wisconsin-Milwaukee

Feedback

Did we miss your institution? Have a suggestion for this website? Create an issue on the GeoBlacklight Website Github page here.

"},{"location":"blog/","title":"Blog","text":""},{"location":"blog/2015/01/introducing-geoblacklight/","title":"Introducing GeoBlacklight","text":"

A quick introduction to the GeoBlacklight project and this blog.

This blog serves as an announcement and engineering blog for developers and the larger GeoBlacklight and geospatial communities. We aim to share our experience with creating the GeoBlacklight project, preserving geospatial data in a repository, engineering challenges to overcome, and other related topics.

GeoBlacklight is an open-source collaboration between several institutions, with a goal of building a better way to discover, share, and access geospatial data. The main software project (GeoBlacklight) is a powerfull discovery and geospatial search application. Built as a Ruby on Rails gem, GeoBlacklight based on the popular open-source discovery software Project Blacklight. We hope you will take a minute to check out the project, connect on Twitter, or join the Google Group.

"},{"location":"blog/2015/02/using-geocombine-to-harvest-and-index-opengeometadata/","title":"Using GeoCombine to harvest and index OpenGeoMetadata","text":"

A quick tutorial on how to harvest and index OpenGeoMetadata for your GeoBlacklight installation.

Warning

This tutorial from 2015 may be outdated. Please refer to the GeoCombine repo for up to date instructions.

Sharing, collaborating, and harvesting geospatial metadata is not really easy. A recent development in the world of geospatial metadata sharing is the new project OpenGeoMetadata. OpenGeoMetadata aims to be a shared repository for institutions looking to share, collaborate, and harvest geospatial metadata. For more details on how the project is structured and why we think this is really cool, see this readme.

We started work on software focused on harvesting, converting, and indexing this metadata called GeoCombine.

"},{"location":"blog/2015/02/using-geocombine-to-harvest-and-index-opengeometadata/#geocombine-a-ruby-toolkit-for-geospatial-metadata","title":"GeoCombine - A ruby toolkit for geospatial metadata","text":"

GeoCombine is envisioned as an easy to use toolkit for metadata conversions with integration into applications and projects like GeoBlacklight, GeoMonitor, and OpenGeoMetadata.

Currently (as of 2015-02-05), GeoCombine really just does three things:

  • Clones OpenGeoMetadata repositories
  • Updates the local cloned repositories (using git pull)
  • Indexes into Solr geoblacklight.xml files from the cloned repositories
"},{"location":"blog/2015/02/using-geocombine-to-harvest-and-index-opengeometadata/#getting-started","title":"Getting started","text":"

This guide assumes a few things already.

  • You have Git installed
  • You have Ruby installed
  • You have Solr running locally on port 8983 (default Solr port) and it is configured with GeoBlacklight-Schema configuration
"},{"location":"blog/2015/02/using-geocombine-to-harvest-and-index-opengeometadata/#install-geocombine","title":"Install GeoCombine","text":"

If you have already have a GeoBlacklight application, skip steps 1 and 2. You can just add gem 'geo_combine' to your GeoBlacklight application's Gemfile

  1. To get started, first clone the GeoCombine repository

    $ git clone https://github.com/OpenGeoMetadata/GeoCombine.git\n
  2. Switch to its folder

    $ cd GeoCombine\n
  3. Install GeoCombine's dependencies

    $ bundle install\n
  4. Create a tmp directory (if it doesn't already exist)

    $ mkdir tmp\n
  5. Clone all of the 'edu.*' repositories to tmp.

    $ rake geocombine:clone\n

    Since other software projects live in OpenGeoMetadata we only want to clone the metadata repositories. All of these are currently namespaced with \"edu.institution.subdomain\".

  6. Index all of the geoblacklight.xml documents located in cloned repositories.

    $ rake geocombine:index\n

    Go grab a coffee or lunch, because this might take a while! But afterwards your index should have +30,000 new records in it.

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/","title":"A hands on introduction to GeoBlacklight - GeoBlacklight Workshop","text":"

Welcome to the workshop, \"A hands on introduction to GeoBlacklight\"!

Warning

This tutorial was designed for GeoBlacklight versions before 4.0 and may be outdated. Please refer to the Documentation pages for up to date instructions.

Workshop Dates:

  • Code4Lib 2015 - February 9, 2015
  • Open Repositories 2015 - June 8, 2015
  • DLF Forum - October 28, 2015
  • Geo4Lib Camp - January 25, 2016

Workshop facilitators:

Stanford University

  • Jack Reed
  • Darren Hardy

Princeton University

  • Eliot Jordan
"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#workshop-agenda","title":"Workshop Agenda","text":"

This workshop will focus primarily on getting a working development version of GeoBlacklight up and going for you. This will be broken down into the following sessions.

  • Part 1: GeoBlacklight Overview (20 minutes)
  • Part 2: Setting up your environment (45 minutes)
  • Part 3: Create your GeoBlacklight application (45 minutes)
  • Part 4: Index Solr Documents (20 minutes)
  • Part 5: Customize your application (extra credit)
"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#part-1-geoblacklight-overview","title":"Part 1: GeoBlacklight Overview","text":"
  • Why and what is GeoBlacklight?
  • Software projects overview and technology
  • GeoBlacklight feature set
"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#why-and-what-is-geoblacklight","title":"Why and what is GeoBlacklight?","text":"

GeoBlacklight is a Ruby on Rails engine, based on the popular open-source project Blacklight. The aim of the project is to provide a simple, effective open-source application for discovery of geospatial data. Many institutions are using GeoBlacklight to provide a search engine across a federated catalog of geospatial data.

Discovery services and metadata have been key challenges for many organizations who provide geospatial data. GeoBlacklight hopes to build on the successes of projects like OpenGeoPortal and Blacklight by integrating with an ecosystem of plugins and an already active developer community. Bridging the gap between the digital library and geospatial communities, GeoBlacklight aims to bring expertise from both fields to provide a better experience for finding geospatial data.

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#what-do-you-mean-by-geospatial-data","title":"What do you mean by geospatial data?","text":"

We primarily mean Geographic Information Systems (GIS) data which are structured data in specific file formats (Shapefiles, Rasters, etc.). That is, GIS data you would be serving through a spatial data infrastructure (SDI).

GeoBlacklight is flexible enough, however, to act as a discovery service for a variety of sources, but is designed for GIS data specifically. Traditionally GIS and SDI software have not done great job at discovery and has always felt like an afterthought, thus the opportunity for GeoBlacklight to advance the state of GIS data discovery.

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#but-i-dont-have-a-spatial-data-infrastructure","title":"But I don't have a spatial data infrastructure?","text":"

That's ok. GIS data indexed into GeoBlacklight becomes progressively more useful based on the services that back them. For example, the minimum required metadata for GeoBlacklight is a bounding box, title, and description, and no references to services that will actually provide that data are required. GeoBlacklight can also help with serving static files available through a URL. Moreover, GeoBlacklight also natively supports IIIF objects, so organizations who have IIIF servers for scanned maps can start using GeoBlacklight today.

The GeoBlacklight Schema, Version 1.0 uses a field dct_references_s to define external services and references. See the Reference URIs section for a list of possible key:value pairs and instructions on how to apply them.

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#software-projects-and-communities","title":"Software projects and communities","text":"

GeoBlacklight (the Ruby on Rails, Blacklight based application) is part of a larger effort to provide library services to geospatial data users. Several additional software augment GeoBlacklight and discovery capabilities.

  • GeoBlacklight-Schema OpenGeoMetadata - the metadata schema used in GeoBlacklight
  • GeoMonitor - a WMS service monitor that provides atomic updates to Solr
  • GeoBlacklight Sidecar Images - Store local copies of remote imagery in GeoBlacklight
  • OpenIndexMaps - a community and format for sharing index maps
"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#metadata-sharing","title":"Metadata Sharing","text":"

Worth mentioning is a collaborative effort, OpenGeoMetadata, which aims to share geospatial metadata in an open way. Instead of focusing on building an application that must be deployed at multiple institutions, OpenGeoMetadata uses GitHub as a common, highly available repository. Using GitHub as a platform allows for software development to focus on conversion tools and harvesting tools.

OpenGeoMetadata builds on the groundwork laid by the OpenGeoPortal Metadata Working group.

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#geoblacklight-feature-set","title":"GeoBlacklight Feature Set","text":"

GeoBlacklight extends the functionality of Blacklight by providing the following:

  • spatial search with a spatial relevancy algorithm
  • download functionality for geospatial web services
  • map view of search results
  • easily customizable
  • extendable to new types of data and functionality
  • and more...
"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#part-2-setting-up-your-environment","title":"Part 2. Setting up your environment","text":"
  • Development requirements
  • Local attendees setup VirtualBox/Vagrant
"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#development-requirements","title":"Development requirements","text":"

GeoBlacklight has similar prerequisites to [Blacklight][bldependencies]. It diverges from Blacklight requirements by using a customized Solr schema and configuration, Geoblacklight Schema, Version 1.0.

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#software-you-should-have-installed-on-your-development-computer","title":"Software you should have installed on your development computer","text":"
  • Ruby > 2.6.6
  • Rails > 6.0
  • Git
  • Java > 1.8] (Download JDK for local Solr server)
  • Node.js > 14.15 LTS
  • Yarn > 1.13

Tip

It is recommended to install the latest versions of Ruby, Rails, and Node.js. We strive to keep GeoBlacklight updated with these versions. A great, almost always up-to-date, tutorial on getting a Ruby on Rails development environment is available here: https://gorails.com/setup. If you are not following this tutorial as part of an in person workshop, you can skip to the next section once you have these dependencies installed.

Local attendees of the workshop have the option of just using the pre-created environment on the provided thumb-drive.

Note

You can complete this tutorial without Vagrant as long as you already have the above mentioned software on your machine. If you do, you may skip ahead to Part 3 - Create your application

Also, if you are not at the workshop (or perhaps if you want to prepare your own environment for a workshop you are facilitating), you can create the virtual machine for the workshop, by following this guide.

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#in-person-attendees-setup-virtualboxvagrant","title":"In-person attendees setup VirtualBox/Vagrant","text":"

Good news for in-person workshop participants: the process of setting up your environment has already done for you using VirtualBox and Vagrant. On the thumb-drive underneath a directory titled 'geoblacklight_workshop'. Thanks to Justin Coyne and Data Curation Experts for this approach that is used at HydraCamps.

For those interested in what was installed on this machine and how it was created checkout this gist.

  • Vagrant for OS X and Linux
  • Vagrant for Windows
"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#vagrant-quick-tips","title":"Vagrant Quick Tips","text":"

After you have your virtual machine up and going, you will want to stop it. Here are a few commands that will help out.

$ vagrant halt # stops the virtual machine\n$ vagrant destroy # stops and deletes the virtual machine\n
"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#vagrant-for-os-x-and-linux","title":"Vagrant for OS X and Linux","text":"
  1. Install the Mac (.dmg) version of VirtualBox and Vagrant on your machine. If you are using Linux, please download and install appropriately. VirtualBox Downloads, Vagrant Downloads

  2. If not already on your Desktop, copy the geoblacklight_workshop directory to your ~/Desktop directory

  3. Move to your ~/Desktop/geoblacklight_workshop directory

    $ cd ~/Desktop/geoblacklight_workshop\n
  4. Start vagrant

    $ vagrant up # This command creates and configures guest machines according to your Vagrantfile.\n
  5. SSH to the VM

    $ vagrant ssh # This will SSH into a running Vagrant machine and give you access to a shell.\n
"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#vagrant-for-windows","title":"Vagrant for Windows","text":"

Thanks to Zach Vowell who contributed this guide for Windows.

Note: Please install a Windows ssh client installed such as PuTTY.

  1. Install the Windows (.exe) version of VirtualBox and Vagrant on your machine.

  2. If not already on your Desktop, copy the geoblacklight_workshop directory to your Desktop C:\\Users\\[username]\\Desktop (for Windows 7)

  3. Open a Windows Command Prompt (cmd)

  4. Move to the geoblacklight_workshop directory on the Desktop

    C:\\Users\\[username]> cd Desktop\\geoblacklight_workshop\n
  5. Start Vagrant

    C:\\Users\\[username]\\Desktop\\geoblacklight_workshop> vagrant up\n# This command creates and configures guest machines according to your Vagrantfile.\n
  6. Open up PuTTY

  7. SSH into the Vagrant box by entering the following parameters into the \"Basic Options for Your PuTTY session\" window:

    • Host Name (or IP address): 127.0.0.1
    • Port: 2222
  8. When PuTTY shell prompts for a username and password, enter \"vagrant\" for both. You should now see a command prompt.

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#part-3-create-your-geoblacklight-application","title":"Part 3: Create your GeoBlacklight application","text":"

Before beginning this section, make sure your environment is setup (as shown in Part 2).

In this section of the tutorial, we will cover the following steps:

  1. Generate your Rails application
  2. Install GeoBlacklight
  3. Install RSpec
"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#generating-your-rails-application","title":"Generating your Rails application","text":"

For more information about generating a Rails application see the Getting Started with Rails guide. Before, make sure you have Rails installed by running the following:

$ rails -v\n
If you get an error run:

$ gem install rails\n
Now you should have Rails installed on your machine and are ready to proceed.

  1. Create a new Rails application using the GeoBlacklight template, in which \"your_app_name\" can be anything you want to name your app.

    $ rails new your_app_name\n## For example, you could also use `rails new mockup_geoblacklight`\n
    1. Switch to its folder

    $ cd your_app_name\n
  2. Run the rails server.

    $ rails s -b 0.0.0.0\n

    We are running the Rails server with the \"-b\" option which is binding the server to the 0.0.0.0 IP address. This is only necessary if your are running the application on your Vagrant virtual machine.

    Now you can visit the Rails application at http://127.0.0.1:3000. You should see \"Yay! You're on Rails\" CTRL + c will stop server.

Note You will need to leave the Terminal window open while the Rails server is running.

  1. Optional Initialize your git repository and commit your changes

    $ git init\n$ git add .\n$ git commit -m 'initial commit of Rails application'\n
"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#install-geoblacklight","title":"Install GeoBlacklight","text":"

Now that we have started our Rails application, we need to install GeoBlacklight.

  1. Add GeoBlacklight to your Gemfile. To do this, navigate to the Gemfile in your application, open it with a text editor, and paste the following in to add it to the list of gems:

    # In ./Gemfile\ngem 'blacklight'\ngem 'geoblacklight'\n
  2. Install required gems and their dependencies

    $ bundle install\n
  3. Run Blacklight generator (with devise authentication)

    $ rails g blacklight:install --devise\n
  4. Run GeoBlacklight generator (overrides Blacklight default Solr config)

    $ rails g geoblacklight:install -f\n

    Depending on how your machine is setup you may need to prepend the rails or rake command with bundle exec.

  5. Run database migrations

    $ rake db:migrate\n

    Quick tip: All of these tasks (1 - 5) are included as part of template to generate a new GeoBlacklight application.

    To run that generator just run:

    $ DISABLE_SPRING=1 rails new your_app_name -m https://raw.githubusercontent.com/geoblacklight/geoblacklight/main/template.rb\n

    Remember to cd your_app_name into the directory before starting the server if you are using the template generator.

  6. Start the Solr and Rails server.

    $ rake geoblacklight:server\n

    Running this command will download and start the Solr server. Other commands available to control Solr include:

    $ rake solr:clean # Useful when seeing a \"core already exists\" error.\n$ rake solr:start # Starts Solr independently of the Rails server in the background (without loading core)\n$ rake solr:stop # Stops Solr\n$ rake solr:restart # Stops and restarts an already running background Solr server\n
  7. Navigate to http://127.0.0.1:3000. You should see the GeoBlacklight homepage. CTRL + c will stop both the Solr and Rails server.

  8. Optional Commit your work

    $ git add .\n$ git commit -m 'installed Blacklight and GeoBlacklight'\n

    Great job for making it this far. You now have a working GeoBlacklight application!

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#install-rspec","title":"Install RSpec","text":"

RSpec is a behavior-driven development framework for Ruby. It is the recommended way to test your application and is used by both the Blacklight and GeoBlacklight projects.

  1. Add rspec-rails to both the :development and :test groups in the Gemfile. Again, open the Gemfile with a text editor and paste the line below into the respective groups.

    # In ./Gemfile\ngroup :development, :test do\n  gem 'rspec-rails', '~> 3.0'\nend\n
  2. Download and install RSpec

    $ bundle install\n
  3. Initialize the spec/ directory (where specs will reside) with

    $ rails generate rspec:install\n
  4. Run your tests (specs) by running

    $ bundle exec rspec\n

    Writing tests for your application is outside the scope of this guide, but there are plenty of great examples out there. Check out the Thoughtbot blog.

  5. Optional Commit your work

    $ git add .\n$ git commit -m 'Installed RSpec'\n
"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#next-steps","title":"Next Steps","text":"

So far you have a working GeoBlacklight application with a test framework installed. One thing missing is an index to have your application search against. The next section focuses on indexing Solr documents.

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#part-4-index-solr-documents","title":"Part 4: Index Solr documents","text":"
  1. Overview
  2. Download fixture documents
  3. Index documents
"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#overview","title":"Overview","text":"

GeoBlacklight uses the GeoBlacklight Schema, Version 1.0 as a template for metadata documents indexed by Solr.

GeoBlacklight provides a rake task to index documents as fixtures for tests. We will use this rake task to index several documents as an example.

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#download-fixture-metadata-documents","title":"Download fixture metadata documents","text":"
  1. Assuming that you have already navigated to the directory of your GeoBlacklight app, create a directory for some Solr documents and then move to it:

    $ mkdir -p spec/fixtures/solr_documents && cd spec/fixtures/solr_documents\n
  2. Download some metadata documents (in JSON format) to the directory

    $ curl -O https://gist.githubusercontent.com/mejackreed/84abc598927c43af665b/raw/geoblacklight-documents.json\n
  3. Move back to app root directory

    $ cd - # Or cd ../../../\n
  4. Make sure your Solr server and Rails application are started.

    $ rake geoblacklight:server\n
  5. Optional Commit your work

    $ git add .\n$ git commit -m 'Adds in JSON fixtures'\n

The fixtures directory is useful for quickly indexing a small number documents in Solr (built specifically for populating Solr for testing). I would caution though in using this task for large scale indexing and committing, as we've developed other best practices for production-scale indexing.

Now you should see facets listed on the lower left hand part of the page. Try a search! You can search for * to search for everything.

Want to index some more documents? Check out this tutorial on how to easily index metadata from OpenGeoMetadata.

There are many ways to customize your GeoBlacklight application, and unfortunately we can't cover them all with this tutorial. GeoBlacklight tries to stick to similar patterns as Blacklight, so most of the Blacklight customization techniques should hold true.

Blacklight Customization - from the Project Blacklight Wiki

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#part-5-customizing-your-geoblacklight-application","title":"Part 5: Customizing your GeoBlacklight Application","text":"
  1. Basic principles
  2. Customize metadata shown
  3. Changing the style of your application
  4. Overriding a partial
"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#basic-principles","title":"Basic principles","text":"

When it comes to customizing your GeoBlacklight application there are some basic principles to keep in mind.

  1. The less you override the easier it is to upgrade.

    Blacklight and GeoBlacklight adhere to some basic principles which all adopters to override helper methods, view partials, and even classes. A lot of things are configurable out of the box. However, there are some best practices around what to override and where to do it to make sure your application can take advantage to improvements to both Blacklight and GeoBlacklight. Both projects use semantic versioning to make this upgrade path easier for adopters.

    Providing your own view templates - Blacklight Wiki (Customizing the User Interface)

  2. Reach out and ask for help on the Blacklight Developers or GeoBlacklight Google Groups

    Asking questions, reaching out to others, and getting feedback from experienced developers is a great practice in general. In this particular case its helps foster the community and start conversations that might help others.

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#customize-metadata-shown","title":"Customize metadata shown","text":"

In this example we are going to change the way the GeoBlacklight is configured to show certain metadata fields on an items page. This is the same way a Blacklight application would configure these fields.

  1. Make sure your Solr server and Rails application are started.

    $ rake geoblacklight:server\n
  2. Open the app/controllers/catalog_controller.rb file in your text editor.

    Hint: catalog_controller.rb is located at \"app/controllers/catalog_controller.rb\" in your application

  3. Scroll down to lines 137 - 144

    ...\nconfig.add_show_field Settings.FIELDS.CREATOR, label: 'Author(s)', itemprop: 'author'\nconfig.add_show_field Settings.FIELDS.DESCRIPTION, label: 'Description', itemprop: 'description', helper_method: :render_value_as_truncate_abstract\nconfig.add_show_field Settings.FIELDS.PUBLISHER, label: 'Publisher', itemprop: 'publisher'\nconfig.add_show_field Settings.FIELDS.PART_OF, label: 'Collection', itemprop: 'isPartOf'\nconfig.add_show_field Settings.FIELDS.SPATIAL_COVERAGE, label: 'Place(s)', itemprop: 'spatial', link_to_facet: true\nconfig.add_show_field Settings.FIELDS.SUBJECT, label: 'Subject(s)', itemprop: 'keywords', link_to_facet: true\nconfig.add_show_field Settings.FIELDS.TEMPORAL, label: 'Year', itemprop: 'temporal'\nconfig.add_show_field Settings.FIELDS.PROVENANCE, label: 'Held by', link_to_facet: true\n...\n
    These configuration options relate to fields that are indexed in Solr. You can disable a metadata field being shown on an items show page. If you navigate to an items page, it will currently show field called publisher. Maybe you would like to rename that field to \"Data publisher\".

  4. Modify the label in line 139

    # change this\nconfig.add_show_field Settings.FIELDS.PUBLISHER, label: 'Publisher', itemprop: 'publisher'\n# to this\nconfig.add_show_field Settings.FIELDS.PUBLISHER, label: 'Data publisher', itemprop: 'publisher'\n

    Save the file and reload the page. You should see the label change.

  5. Next we will remove the \"Author(s)\" metadata field from being shown. Comment out or remove the Settings.FIELDS.CREATOR line (137).

    # config.add_show_field Settings.FIELDS.CREATOR, label: 'Author(s)', itemprop: 'author'\n
    Save the file and reload the page. You should no longer see the \"Author(s)\" field.

    You have now customized a layer's show page!

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#changing-the-style-of-your-application","title":"Changing the style of your application","text":"

GeoBlacklight uses Twitter Bootstrap as a base for UI components and is implemented using the bootstrap-sass gem. This approach should make things easier for adopters wanting to customize the look and feel of their application. Bootstrap variables can easily be modified which will change how the application looks.

  1. You can update Bootstrap variables in your _customizations.scss!

    Change link color

    // in app/assets/stylesheets/_customizations.scss\n// Links\n$link-color: green;\n

    Change default border style

    // in app/assets/stylesheets/bootstrap-variables.scss\n// Borders\n$border-width: 2px;\n$border-radius: .4rem;\n

    Great job! You can configure a whole host of options using this Bootstrap customization technique. Once again, here is the list of Bootstrap variables you can customize https://github.com/twbs/bootstrap/blob/v4.6.0/scss/_variables.scss .

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#overriding-a-partial","title":"Overriding a partial","text":"

Lets say you want to override the homepage that is shown in GeoBlacklight that can easily be done by just creating same-named a partial at the same path in your GeoBlacklight application.

  1. Check out home page partial on Github. https://github.com/geoblacklight/geoblacklight/blob/main/app/views/catalog/_home_text.html.erb

  2. This same partial is being overriden from Blacklight https://github.com/projectblacklight/blacklight/blob/master/app/views/catalog/_home_text.html.erb

  3. Create a file with the same name and path in your application.

    $ mkdir -p app/views/catalog\n$ touch app/views/catalog/_home_text.html.erb\n
  4. Edit this file and add some custom text

    This is the home page.\n\nSearch bar:\n<%= render_search_bar %>\n

    See how easy that was? You even included another partial!

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#customize-i18n-keys","title":"Customize i18n keys","text":"

Blacklight and GeoBlacklight use i18n for internationalization support. These keys provide a quick way to customize your app without having to override partials.

  1. Open up file config/locales/blacklight.en.yml

    en:\n  blacklight:\n    application_name: 'Blacklight'\n
  2. Edit the file, adding a customization to the search form label

    en:\n  blacklight:\n    application_name: 'Your Geo App'\n    search:\n      form:\n        submit: 'Search this'\n

    Refresh your home page and you should see the submit button text changed. If you inspect the html you will notice the HTML title attribute changed.

    More configurable keys are available, check out what you can customize using this approach: - Blacklight Configurable Keys - GeoBlacklight Configurable Keys

"},{"location":"blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/#wrapping-up","title":"Wrapping Up","text":"

Thanks for attending this workshop. You can contribute feedback about this workshop by filling out a quick survey.

This workshop is open source, and you can contribute back to it.

Don't forget to stop your virtual machine.

$ vagrant halt # stops the virtual machine\n
This work is licensed under a Creative Commons Attribution 4.0 International License."},{"location":"blog/2015/04/deploying-geoblacklight/","title":"Deploying GeoBlacklight","text":"

At Stanford, we have been preparing to launch our new GeoBlacklight instance, EarthWorks. Over the past few weeks we have had several questions from others about what it takes to deploy GeoBlacklight. This blog post hopes to address these questions. A follow up post will discuss enhancements to this scheme for added functionality.

"},{"location":"blog/2015/04/deploying-geoblacklight/#system-requirements","title":"System Requirements","text":"

At its core, GeoBlacklight is a Ruby on Rails application. It depends on having access to two things:

  • a relational database
    • usually PostgreSQL or MySQL
    • used for storing session, bookmarks, and user data
  • a Solr index
    • used as the search engine for metadata
"},{"location":"blog/2015/04/deploying-geoblacklight/#simple-geoblacklight-application-architecture","title":"Simple GeoBlacklight application architecture","text":""},{"location":"blog/2015/04/deploying-geoblacklight/#geoblacklight-application","title":"GeoBlacklight Application","text":"

EarthWorks is Stanford's deployment of GeoBlacklight. Our virtualized VMWare infrastructure hosts the application on several load balanced virtual machines. Phusion Passenger and Apache httpd to serve the application from the vm's. This setup is pretty standard for Ruby on Rails applications in our infrastructure.

If you don't have a server to deploy the application on you have other options! Because GeoBlacklight is a Ruby on Rails application, there are many options available. You can deploy your application to Heroku, Digital Ocean or other hosting services.

"},{"location":"blog/2015/04/deploying-geoblacklight/#relational-database","title":"Relational database","text":"

At Stanford we use PostgreSQL for our GeoBlacklight application. This can be configured in the Ruby on Rails application in the config/database.yml file. While we use PostgreSQL for EarthWorks we also use MySQL for other projects. Blacklight and GeoBlacklight have deployments using PostgreSQL and MySQL. By default, the development environment of GeoBlacklight uses SQLite. SQLite is not recommended for production.

For a comparison between these relations databases, see this extensive tutorial from Digital Ocean.

"},{"location":"blog/2015/04/deploying-geoblacklight/#solr-index","title":"Solr index","text":"

GeoBlacklight uses Solr for as an index for advanced querying and faceting. Documents indexed into Solr need to be in the geoblacklight-schema format. You can also configure your Solr deployment using the geoblacklight-schema Solr configuration files.

"},{"location":"blog/2015/04/deploying-geoblacklight/#solr-version","title":"Solr version","text":"

GeoBlacklight requires Solr version 4.7 or later. This hard version rule is due to new advanced spatial support introduced in this release.

"},{"location":"blog/2015/04/deploying-geoblacklight/#solr-security","title":"Solr security","text":"

The GeoBlacklight software accesses Solr only from the server. This means that you can deploy your Solr server behind a firewall. The front end of the application should never query Solr directly.

"},{"location":"blog/2015/04/deploying-geoblacklight/#wrap-up","title":"Wrap up","text":"

That's all that is needed for a basic GeoBlacklight deployment! I hope this is helpful as you plan your GeoBlacklight deployment. Coming soon, a post on enhancing your GeoBlacklight deployment by adding additional services.

"},{"location":"blog/2015/04/deploying-geoblacklight/#useful-links","title":"Useful links:","text":""},{"location":"blog/2015/04/deploying-geoblacklight/#rails-and-blacklight","title":"Rails and Blacklight","text":"
  • Understanding Rails and Blacklight from the Blacklight Wiki
  • Configuring Rails Applications from RailsGuides
  • Getting Started with Rails 4.x on Heroku from Heroku
  • How To Deploy a Rails App with Passenger and Nginx on Ubuntu 14.04 from Digital Ocean
"},{"location":"blog/2015/04/deploying-geoblacklight/#relational-databases","title":"Relational Databases","text":"
  • SQLite vs MySQL vs PostgreSQL: A Comparison Of Relational Database Management Systems from Digital Ocean
"},{"location":"blog/2015/04/deploying-geoblacklight/#solr","title":"Solr","text":"
  • Solr configuration from the Blacklight Wiki
  • How To Install Solr on Ubuntu 14.04 from Digital Ocean
  • Spatial Search in Solr from the Solr Wiki
"},{"location":"blog/2015/11/open-in-cartodb-now-in-geoblacklight/","title":"Open in CartoDB, now in GeoBlacklight","text":"

Open in CartoDB was recently added to GeoBlacklight.

One of the primary goals of GeoBlacklight is to make finding geospatial data a simple, pleasant experience for end users. By removing visualization and analysis features, we can focus on discovery and getting our users quicker access to data they need. A recent enhancement to GeoBlacklight furthers this goal by providing a painless way for users to import public data into CartoDB.

CartoDB is an open source software as a service application for visualization and analysis of geospatial data. CartoDB recently introduced an import API which allows applications to provide their users a way to import data directly into their CartoDB account. \"Open in CartoDB\" was first implemented in Data.gov and we were excited to add the same functionality in GeoBlacklight. This feature in GeoBlacklight is implemented using the Blacklight document actions framework.

Integrating with CartoDB was quite painless. One of the best parts of working on open source and integrating with an open company like CartoDB was we were able to provide feedback directly to developers who work on their platform. They were able to improve their product quickly (which improves our user's experience) which allowed this feature to come in so quickly.

You can checkout \"Open in CartoDB\" now in EarthWorks by viewing this Natural Earth dataset. We are interested in hearing your feedback about this. The \"Open in CartoDB\" feature is enabled for all of the public datasets in EarthWorks.

"},{"location":"blog/2016/01/using-packer-to-create-a-development-virtual-machine-for-geoblacklight/","title":"Using Packer to create a development virtual machine for GeoBlacklight","text":"

A tutorial on how we use Packer to create a working virtual machine for the GeoBlacklight workshop

Warning

This tutorial may be outdated. Please refer to the Documentation pages for up to date instructions.

When we run the GeoBlacklight workshop, we provide attendees with a VirtualBox virtual machine (vm) so that they can participate without having download software. Having attendees connect to the internet to download large files doesn't always work in conference environments. To keep up to date with software dependencies we have rebuilt this virtual machine several times. Recently, the process for creating and updating the virtual machine has been automated using Packer. From Packer's website:

Packer is a tool for creating machine and container images for multiple platforms from a single source configuration.

This tutorial outlines how we create the GeoBlacklight workshop vm, so that others can create this virtual machine from scratch. This tutorial could also be adapted to create an image for Amazon EC2, Digital Ocean, Docker, and others.

"},{"location":"blog/2016/01/using-packer-to-create-a-development-virtual-machine-for-geoblacklight/#getting-started","title":"Getting started","text":"

To get started, you will need to install several pieces of software that our Packer template will use. Go ahead and install this software for your system.

Required software:

  • Packer installation
  • VirtualBox download
  • Vagrant download

Once you have installed all of the software, make sure that you have Packer available on your path.

$ packer -v\n0.8.6\n
"},{"location":"blog/2016/01/using-packer-to-create-a-development-virtual-machine-for-geoblacklight/#using-the-geoblacklight-packer-template","title":"Using the GeoBlacklight Packer template","text":"

Next, you will need to clone down the Packer template that was created for the GeoBlacklight Workshop from https://github.com/mejackreed/packer-templates.

$ git clone https://github.com/mejackreed/packer-templates.git\n

Next change directory (cd) into the git repository you just cloned.

$ cd packer-templates\n

And checkout the geoblacklight branch.

$ git checkout geoblacklight\n

Finally, change directory (cd) into the ubuntu-16.04.3 directory. This is the image I based the GeoBlacklight Workshop image on.

$ cd ubuntu-16.04.3\n
It should be noted that I opted for the 32-bit version of this release of Ubuntu for maximum compatibility. You may want to change the version of Ubuntu used and thus you must also update the checksum."},{"location":"blog/2016/01/using-packer-to-create-a-development-virtual-machine-for-geoblacklight/#exploring-the-packer-template","title":"Exploring the Packer template","text":"

The template.json file is where Packer takes its directions from. I won't go through everything in this file, but I will point out some of the customizations I made for the GeoBlacklight workshop.

The major customizations added, were the addition of four scripts that run during creation.

"},{"location":"blog/2016/01/using-packer-to-create-a-development-virtual-machine-for-geoblacklight/#rubysh","title":"ruby.sh","text":"

Installs rbenv, Ruby, bundler, and specifies no rdoc documentation.

"},{"location":"blog/2016/01/using-packer-to-create-a-development-virtual-machine-for-geoblacklight/#railssh","title":"rails.sh","text":"

Installs nodejs and Ruby on Rails.

"},{"location":"blog/2016/01/using-packer-to-create-a-development-virtual-machine-for-geoblacklight/#geoblacklightsh","title":"geoblacklight.sh","text":"

Creates a GeoBlacklight application and downloads and configures jetty/solr.

"},{"location":"blog/2016/01/using-packer-to-create-a-development-virtual-machine-for-geoblacklight/#opengeosuitesh","title":"opengeosuite.sh","text":"

Installs OpenGeoSuite.

"},{"location":"blog/2016/01/using-packer-to-create-a-development-virtual-machine-for-geoblacklight/#creating-the-vagrant-box","title":"Creating the Vagrant box","text":"

You can modify any of these scripts to meet your customization needs. After doing so, you will want to create your VirtualBox vm. To do so run the following command from the ubuntu-16.04.3 directory:

$ packer build template.json\n

This command will take a while so it might be best to go get a coffee. What the command is doing:

  • Downloading a fresh copy of Ubuntu
  • Creating a VirtualBox image with that copy of Ubuntu
  • Updating the vm's packages
  • Installing our custom software

After the command finishes you should have your virtual machine box waiting for you in the same directory.

To create a Vagrantfile to use with this box:

$ vagrant init ubuntu-14-04-3-x32-virtualbox.box\n

Now you can start up your fresh box using:

$ vagrant up\n

And to ssh in

$ vagrant ssh\n
"},{"location":"blog/2016/01/using-packer-to-create-a-development-virtual-machine-for-geoblacklight/#conclusion","title":"Conclusion","text":"

I hope this tutorial is useful to others who run technical workshops. It can sometimes be painful in trying to support workshops on multiple platforms with limited network connectivity. This workflow of using Packer, Vagrant, and VirtualBox has been successful for us.

"},{"location":"blog/2016/01/geoblacklight-at-nyu-a-blog-series-by-andrew-battista/","title":"GeoBlacklight at NYU, A blog series by Andrew Battista","text":"

NYU GIS Librarian, Andrew Battista, releases a blog post series on implementing GeoBlacklight at NYU.

NYU's GIS Librarian Andrew Battista created blog post series on his experience with implementing GeoBlacklight, spatial data services, and integrating with an existing repository at NYU. This series is a great read and provides a great deal of context and information not only about GeoBlacklight, but offers fresh perspectives about repositories and GIS in libraries.

You can read the series at on Andrew's GitHub pages account.

Blog Posts:

  • Post 1 - GeoBlacklight at NYU: A Project Overview
  • Post 2 \u2013 DSpace and the Institutional Repository: Preservation and the Spatial Data Infrastructure
  • Post 3 \u2013 Creating GeoBlacklight Metadata Records
  • Post 4 - The Technology Stack: Amazon Web Services Products & Open Source GIS
"},{"location":"blog/2016/02/vecnet-digital-library-redesign-and-geospatial-search-using-geoblacklight/","title":"VecNET Digital Library redesign and geospatial search using GeoBlacklight","text":"

Adding geospatial search to the VecNET digital library using GeoBlacklight.

We got on the GeoBlacklight bus when we joined the VecNET project. VecNET was a Gates Foundation initiative to simulate malaria vectors, transmission and interventions using parameters from current literature. To provide the latest and greatest parameters, VecNET used a curated digital library with a web based search interface.

"},{"location":"blog/2016/02/vecnet-digital-library-redesign-and-geospatial-search-using-geoblacklight/#what-was-the-problem","title":"What was the problem?","text":"

The search interface wasn't spatial, had some usability issues, and needed some design love.

  • Spatial searching was limited to certain named locations; there was no map to visually define a search region, and we all love maps.
  • Entries in the search results didn't show enough information, so users had to keep checking their search filters to understand what they were looking at. AirBnB called this 'pogosticking'.
  • Filters that could help to narrow down a search were present only in a side column, and underused.

"},{"location":"blog/2016/02/vecnet-digital-library-redesign-and-geospatial-search-using-geoblacklight/#drawing-board","title":"Drawing board","text":"

With broad changes in mind, rough designs were pencilled up and debated around the office whiteboard. Hopefully the designer in you enjoys these as much as we did.

"},{"location":"blog/2016/02/vecnet-digital-library-redesign-and-geospatial-search-using-geoblacklight/#map-and-fancy-notch-thing","title":"Map and fancy notch thing","text":"

The notch made it into the final design, the floating panel and vignette shading didn't.

"},{"location":"blog/2016/02/vecnet-digital-library-redesign-and-geospatial-search-using-geoblacklight/#simple-facet-design","title":"Simple facet design","text":"

We decided early to move the facet selection into a single column with the results.

"},{"location":"blog/2016/02/vecnet-digital-library-redesign-and-geospatial-search-using-geoblacklight/#ui-with-map-and-facets","title":"UI with map and facets","text":"

The single search/results column let us plan a nice clean map display.

"},{"location":"blog/2016/02/vecnet-digital-library-redesign-and-geospatial-search-using-geoblacklight/#look-at-that","title":"Look at that","text":"

After the early work of original thought, we checked out related works. It turns out the original VecNET search bore a striking resemblance to the old AirBnB search! AirBnB had recently done a redesign to fix what they have concluded were large design and usability issues.

We could learn from their experience and benefit from work done by a talented team. Our final proposal was iteratively formed with feedback from VecNET librarians based around simple UX principles:

  • Big searchable map
    • A well designed map can clearly display information about where papers were from; users can easily find data from a specific location
  • Intuitive top to bottom page flow
    • Natural progression means less 'pogosticking'
  • Simple search filters
    • Use ordinary words instead of obscure terms

"},{"location":"blog/2016/02/vecnet-digital-library-redesign-and-geospatial-search-using-geoblacklight/#enter-geoblacklight","title":"Enter GeoBlacklight","text":"

The stack at VecNET was inherited from a prototype Ruby on Rails app with an old Solr and Fedora backend. For a spatial search we would need to integrate spatial values of records with the existing dataset. On the front end we wanted to use Leaflet and GeoJSON and uses Rail's API ability to serve data to a modern Javascript powered frontend with the UI goodness that users enjoy.

Just as the Backbone.js structure was being laid down, we noticed a new project with strong ideas about user friendly geospatial discovery.

The GeoBlacklight project was being developed in the open and was based on the Blacklight project which was a basis of the legacy app. In addition backwards compatibility was emphasised which is a high virtue when choosing an upstream. Normally you don't get excited about Rails apps at version 0.4 but with institutions like Stanford, MIT and Princeton contributing to an active development community that was encouraging new users \u2013 it was as close as software gets to alluring.

"},{"location":"blog/2016/02/vecnet-digital-library-redesign-and-geospatial-search-using-geoblacklight/#customising-geoblacklight","title":"Customising GeoBlacklight","text":"

Adding VecNET-specific functionality to the existing GeoBlacklight platform can with a bit of a learning curve but thankfully was well documented with an active Google groups community. Our VecNET specific additions included:

  • displaying several locations associated with a single 'record' on a map
  • search by map area and bounding box
  • point clustering for when density was too high

Other customisations to fit our design idea included:

  • Unified search page with AJAX search form and results
  • Shiny rounded CSS3 corners (because Steve Jobs amiright...)
  • Friendly animation and transitions between results and map marker
  • Enhanced metadata on the results page for better selection
  • Progressive enhancement with fallback to rendered HTML for SEO.

We implemented the changes with vanilla Javascript and Rails templates to minimise the learning curve for future maintenance.

"},{"location":"blog/2016/02/vecnet-digital-library-redesign-and-geospatial-search-using-geoblacklight/#enhancing-metadata-with-micro-standards","title":"Enhancing metadata with Micro Standards","text":"

Geoblacklight uses an geospatial schema extended from prior standards, documented here, and explained best here(imho). It encodes the metadata and geospatial characteristics of the stored records. The Ruby on Rails server in GeoBlacklight will embed the data into rendered HTML with a schema.org style which looks the 'div' element in the HTML below:

    <div class=\"document \" itemscope=\"\" itemtype=\"http://schema.org/Dataset\">\n    <div class=\"documentHeader row\" data-layer-id=\"urn:arrowsmith.mit.edu:MIT.000932529\" data-bbox=\"-83.5 41.5 -79.0 43.0\">\n

The value, data-bbox, for instance, will be be used by Leaflet.js to manipulate the map or generate a marker. It is a simplified explanation but you get the idea.

We want to make searching easy and efficient by displaying the most relevant data. Using the mechanism above, we can extend our digital library to show users important details such as journal, date, author. The image below is an example of those micro schema values turned into nice UI elements for users.

"},{"location":"blog/2016/02/vecnet-digital-library-redesign-and-geospatial-search-using-geoblacklight/#part-ii","title":"Part II?","text":"

There was plenty more fun in customising GeoBlacklight but that is all for now. If you have any feedback, we would love to hear from you. We promise to take criticism well :) Promise.

"},{"location":"blog/2016/08/geoblacklight-version-10-released/","title":"GeoBlacklight version 1.0 released!","text":"

As part of an ongoing community last week, GeoBlacklight v1.0 was released! GeoBlacklight v1.0 contains work by 11 contributors, from five institutions, making 706 commits. Thanks to everyone who has contributed!

GeoBlacklight v1.0 contains many enhancements including:

  • Blacklight 6 compatibility
  • Autocomplete and spelling suggestions on by default
  • Enhanced UI Components
  • Greater ability to customize metadata fields
  • A simplified metadata schema
  • Customizable Leaflet map and plugins

GeoBlacklight adopters who are interested in the upgrading to GeoBlacklight v1.0 should read the update wiki.

The community work continues and we have seen several additional versions of GeoBlacklight released in the past few days. These patch versions contain bug fixes and minor enhancements. For more information about GeoBlacklight or the ongoing work connect with us at: http://geoblacklight.org/connect.

"},{"location":"blog/2016/09/big-ten-academic-alliance-geoportal-launches/","title":"Big Ten Academic Alliance Geoportal Launches","text":"

Congratulations to the Big Ten Academic Alliance that just launched their new GeoBlacklight instance.

The Big Ten Academic Alliance team just launched their new GeoBlacklight instance! This project entailed over 20 team members from ten different institutions working together to create metadata, build policies, publish papers, and develop software. Make sure to checkout the new site here: https://geo.btaa.org/.

The undertaking by this team is truly remarkable. You can read more about the project in this blog post by Karen Majewicz. Also note worthy is that all of the metadata powering their GeoBlacklight instance is now up on OpenGeoMetadata.

"},{"location":"blog/2017/08/summer-code-concludes-and-geoblacklight-version-160-released/","title":"Summer Code concludes and GeoBlacklight version 1.6.0 released","text":"

The second annual GeoBlacklight Community Code has concluded and a new version (1.6.0) has been released with improved views for standards-based metadata files along with several bug fixes.

"},{"location":"blog/2017/08/summer-code-concludes-and-geoblacklight-version-160-released/#summarizing-the-geoblacklight-community-code-summer-2017","title":"Summarizing the GeoBlacklight Community Code: Summer 2017","text":"

Developers and metadata specialists from New York University, Princeton University, and the Big Ten Academic Alliance participated in the second GeoBlacklight Summer Code from July 24 - August 4, 2017. With input from developers at Stanford University, Lewis & Clark College, VecNet, and Cornell University, theers selected a number of goals to work on, including bug fixes, upgrading dependent libraries, improving the user interface, and exploring changes to the metadata schema. As of August 23, GeoBlacklight version 1.6.0 has been released. Major thanks to Eliot Jordan, Stephen Balogh, James Griffin, Eric Larson, and Jack Reed for contributing code actively during the summer.

"},{"location":"blog/2017/08/summer-code-concludes-and-geoblacklight-version-160-released/#changes-and-implementations","title":"Changes and Implementations","text":"

The code and release of version 1.6.0 resulted in a significant new feature, an amazing tabbed viewer that displays standards based XML metadata as HTML in the interface. This was primarily developed by James Griffin, and represents an enhancement that has long been requested by many GeoBlacklight users. Now, users can see a cleaner rendering of ISO or FGDC XML documents in context and download them.

The were also a number of behind the scenes minor code changes, including updates to fixture records, validation of bounding boxes, and a webkit deprecation. To simplify organizational structure, the separate GeoBlacklight Schema repository was deprecated and instead placed as a folder within the main repository.

"},{"location":"blog/2017/08/summer-code-concludes-and-geoblacklight-version-160-released/#forthcoming-work-changes-in-the-schema","title":"Forthcoming Work: Changes in the Schema","text":"

Exploration of a larger development was initiated during the, which led the team to discuss migrating GeoBlacklight metadata to a DCAT based metadata schema. The biggest evolution is the adoption of fields and concepts associated with the Data Catalog Vocabulary (DCAT) standard and the Project Open Data Metadata Schema, V. 1.1, including distribution fields. Distribution fields allow for increased flexibility in providing links to download multiple formats, pointing to reference codebooks and documentation, and integrating seamlessly with web APIs, such as GeoServer and IIIF endpoints. In the coming months, we expect the user interface of GeoBlacklight to reflect some of these new possibilities.

For now, the team has created a development branch of GeoBlacklight for the new schema that includes example fixture documents of DCAT-compliant records. There are implications for moving toward the DCAT standard, the most significant of which is the need to \"flatten\" fully realized .JSON-LD metadata into .JSON files that comply with the Solr cores behind GeoBlacklight. GeoCompile, developed by Eliot and Stephen, is an initial step at making sure this happens seamlessly. Major thanks to Karen Majewicz, Andrew Battista, Stephen Balogh, and Eliot Jordan for doing some thinking and mocking-up during the.

These projects are still in progress and will be completed in the coming months. When we finish mocking up the proposed changes, which are gestured in the fixture record, we will follow up with a more complete post that explains the rationale for choices made and solicits further feedback from the community before any adoptions take place. Thanks to everyone who contributed to the. In the meantime, we welcome comments and questions; follow or contribute to the GeoBlacklight development work on GitHub!

"},{"location":"blog/2018/03/geoblacklight-winter-code-2018--new-release-18/","title":"GeoBlacklight Winter Code 2018 & New Release 1.8","text":"

The GeoBlacklight Community Winter Code has resulted in a new release (1.8.0) that includes a new feature for index maps, bug fixes, and improved metadata documentation.

Developers from Stanford, Princeton, NYU, Cornell, and the University of Minnesota participated in a two-week GeoBlacklight Winter Code during January 2018. The developers completed work on enabling a new format for index maps, squashing several bugs, and an expansion to the GeoBlacklight Metadata 1.0 schema documentation. Progress was also made on the display of attribute table information and the development of a GeoBlacklight plugin that would enable harvesting thumbnail images from geospatial web services or IIIF image servers.

"},{"location":"blog/2018/03/geoblacklight-winter-code-2018--new-release-18/#openindexmaps","title":"OpenIndexMaps","text":"

GeoBlacklight\u2019s latest release includes a major new feature that enables OpenIndexMaps based on the GeoJSON format. This feature will display a spatial index in the map preview box that allows users to select an overlay object, such as a grid section. This will bring up a preview of the item\u2019s basic metadata, a thumbnail image, and access links. See Configuring Index Maps for Use in GeoBlacklight for more information on how to add index maps to metadata records.

"},{"location":"blog/2018/03/geoblacklight-winter-code-2018--new-release-18/#check-out-this-index-map-in-earthworks-from-stanford-libraries","title":"Check out this index map in EarthWorks from Stanford Libraries:","text":""},{"location":"blog/2018/08/2018-summer-code-for-geoblacklight/","title":"2018 Summer Code for GeoBlacklight","text":"

The GeoBlacklight Community Summer 2018 Code has resulted in a new release (1.9.0) that includes compatibility with Blacklight 7 and numerous other upgrades.

"},{"location":"blog/2018/08/2018-summer-code-for-geoblacklight/#overview","title":"overview","text":"

From July 23 - August 3, 2018, developers from Stanford, Princeton, NYU, Cornell, and the University of Minnesota participated in a GeoBlacklight Summer Code that resulted in the release of GeoBlacklight 1.9.0. Overall, the was a highly efficient, concentrated burst of activity that led to many upgrades in the code and a variety of improvements to our development workflow. The team also reviewed all recent and backlogged GitHub issues and was able to address over 50 of them during the course of the.

Here's a summary of some upgrades and changes:

  • BLACKLIGHT 7 UPGRADE: The majority of the activities involved ensuring compatibility with Blacklight (the underlying architecture for GeoBlacklight) and its upcoming Blacklight 7 release. This included removing anticipated deprecations and incorporating Bootstrap 4.0 principles, such as a stylistic switch from pixels to rems.

  • DOWNLOAD PANEL: The work to stay current with Bootstrap resulted a redesign of the Download tool to display a link to a direct download of the original dataset in one panel, with generated exports from a geospatial web server available underneath. This new Export Formats panel also features more semantically accurate text with the inclusion of the coordinate reference system (EPSG:4326) for generated exports. Here is a screenshot of the new layout:

  • METADATA BUGS: A few long standing bugs or omissions involving metadata interactions were addressed. The latest release allows certain characters, such as colons, in the record slugs, and handles empty values for the Geometry Type field. A new key was also added for the dct_references element to display native HTML records in the metadata viewer.

  • INTERNATIONALIZATION: There is now better support for internationalization on view page elements - We fixed many lingering elements that had been hard-coded into English and turned them into translatable elements, which ultimately makes GeoBlacklight more accessible for use in other languages.

  • TESTING WORKFLOWS: The GeoBlacklight GitHub repository was migrated to CircleCI instead of Travis CI for continuous integration and the metadata fixture records used for testing numerous functions were cleaned up and supplemented with new examples.

  • LEAFLET MAP: Leaflet was upgraded to version 1.0, which required a few updates to the code. (Note - users looking to upgrade to the latest release of GeoBlacklight should check the release notes for potential issues with Leaflet 1.0 compatibilities.) The Leaflet map preview also includes new default basemaps - Now, users can toggle between seven default, open-source basemaps to use within the application.

"},{"location":"blog/2018/08/2018-summer-code-for-geoblacklight/#moving-forward","title":"Moving forward","text":"

The work on the summer has left us in a good place to cut a major 2.0 release of GeoBlacklight that will rely on upgraded frameworks. Thanks to everyone who participated with code development and review - Consider joining us for the next!

"},{"location":"blog/2019/02/geoblacklight-20-is-here/","title":"GeoBlacklight 2.0 is Here!","text":"

Developers from Princeton, NYU, Cornell, and the University of Minnesota participated in a two-week GeoBlacklight Winter Codesprint that resulted in a major new version, GeoBlacklight 2.0. This version is compatible with the latest release of the underlying framework, Blacklight 7, which itself includes several significant component upgrades for Bootstrap 4 and Rails 5.2 support. Visit the GeoBlacklight 2.0 Upgrade Guide for more information.

"},{"location":"blog/2019/02/geoblacklight-20-is-here/#notable-enhancements-for-geoblacklight-20","title":"Notable enhancements for GeoBlacklight 2.0","text":"
  • SPATIAL SEARCH RELEVANCY: A long standing issue was addressed that implements a bounding box ratio relevancy strategy for the map search. This means that items that most closely fit the selected map extent will rise to the top of the search results.

  • INDEX MAPS: Developers from Cornell University led the effort during the to improve several aspects of how GeoBlacklight handles OpenIndexMaps. This enhancement includes selected feature highlighting, a download link for the selected feature, and adjustments to the layout of the attribute table.

  • SEARCH SUPPRESSION: This offers a method for handling parent-child records that suppresses child records in a search query. For example, an atlas with hundreds of records representing individual pages would only show up once in the search results. The pages are then accessed from the parent record entry.

  • METADATA DOCUMENTATION: In an effort to provide guidance for new and existing GeoBlacklight metadata authors, we have added a new document, Schema Commentary, that discusses how several elements in the GeoBlacklight 1.0 Metadata Schema interact with the application and recommendations from the GeoBlacklight community of practice.

Thanks to everyone who participated with code development and review. Consider joining us for the next!

"},{"location":"blog/2020/09/geoblacklight-30-is-here/","title":"GeoBlacklight 3.0 is Here!","text":"

Our Summer Community Sprint concluded in August 2020 and resulted in a new version of GeoBlacklight! Version 3 brings us the long awaited multiple downloads option, support for Rails 6, and many improvements for accessibility.

"},{"location":"blog/2020/09/geoblacklight-30-is-here/#notable-activities-and-enhancements","title":"Notable Activities and Enhancements","text":"
  • MULTIPLE DOWNLOADS: To learn more about how to format metadata for multiple downloads, review How to configure multiple download links

  • NEW NAME FOR THE DEFAULT BRANCH: We changed the name of the default branch to \u201cMain.\u201d There is a growing consensus in the development world for projects to change the default branch of their code in GitHub away from \"master,\" as this is a loaded, potentially offensive, term. Read more about it here, including comments from our community: https://github.com/geoblacklight/geoblacklight/issues/940

  • ICONS: We deprecated the geoblacklight-icons project, which relied upon font-awesome for the icons. Now, new SVG images can be submitted directly to the GeoBlacklight repository or added in your own GeoBlacklight application.

  • METADATA SCHEMA WORKGROUP: During the sprint, we convened a Metadata Schema Workgroup to assess the current GeoBlacklight Schema 1.0 and develop an upgraded version. The workgroup will continue to meet throughout 2020.

  • COMMUNITY GOVERNANCE & VALUES: We also kicked off new discussions of how to be an equitable and inclusive community. These discussions are ongoing - check out our new About page, where we will be adding more soon.

Thanks to everyone who participated with code development, feature review, and metadata assessments. Consider joining us for the next sprint!

"},{"location":"blog/2021/02/announcing-the-geoblacklight-community-winter-2021-sprint/","title":"Announcing the GeoBlacklight Community Winter 2021 Sprint","text":"

The GeoBlacklight Community collaborates on a 2-week sprint every Winter and Summer. Participation in the sprint is open to anyone and can include coding new features, fixing bugs, writing documentation, troubleshooting issues, and more.

Each sprint starts with a kickoff meeting where we will review our goals and volunteer to contribute towards various issues. This will be followed by short standup meetings every day to check-in and plan our next activities.

"},{"location":"blog/2021/02/announcing-the-geoblacklight-community-winter-2021-sprint/#event-dates","title":"Event Dates","text":"
  • The sprint will take place from February 16-26, 2021
  • Kickoff Meeting: Tuesday, February 16 @ 11am Central Time
  • Daily standup meetings: Weekdays, February 17-26 @ 1-1:30 pm Central Time
"},{"location":"blog/2021/02/announcing-the-geoblacklight-community-winter-2021-sprint/#how-to-join","title":"How to Join","text":"

Send us your name and email using this Google form. The GeoBlacklight Community Coordinator will then send you the Zoom links with instructions on how to join.

"},{"location":"blog/2021/02/announcing-the-geoblacklight-community-winter-2021-sprint/#tracks-for-the-winter-2021-community-sprint","title":"Tracks for the Winter 2021 Community Sprint","text":"

This sprint will encompass three tracks:

"},{"location":"blog/2021/02/announcing-the-geoblacklight-community-winter-2021-sprint/#track-1-metadata","title":"Track 1. Metadata","text":"

The GeoBlacklight Metadata Schema Workgroup is developing a new version of the metadata schema. During the sprint, we will need developers and metadata specialists to review and implement the new schema.

"},{"location":"blog/2021/02/announcing-the-geoblacklight-community-winter-2021-sprint/#track-2-community","title":"Track 2. Community","text":"

Our community is growing! We are working on developing a governance framework, documenting our values, and developing a 2021 Roadmap. During the sprint, we will need people with project management and documentation skills to help move us towards improvements in our community organization.

"},{"location":"blog/2021/02/announcing-the-geoblacklight-community-winter-2021-sprint/#track-3-software-maintenance","title":"Track 3. Software maintenance","text":"

This one is especially for Ruby on Rails programmers and any GeoBlacklight users who like to troubleshoot! The sprint is a good time to fix bugs and look at dependency upgrades.

"},{"location":"blog/2021/09/new-releases-after-the-geoblacklight-community-summer-2021-sprint/","title":"New releases after the GeoBlacklight Community Summer 2021 Sprint","text":"

The GeoBlacklight Community completed another Summer Sprint, held from August 30 to September 3, 2021.

The overarching goal of this sprint was to develop a Version 4 release to support the Aardvark Metadata Profile. We ended up settling on a pre-release to indicate that more testing and enhancements still need to be done. However, we did address several maintenance issues and updated the current release to version 3.4.

Other activities included updating the project website, assessing existing metadata tools, and improving the application's fixture (sample) metadata records.

The sprint was divided into 3 overlapping tracks.

"},{"location":"blog/2021/09/new-releases-after-the-geoblacklight-community-summer-2021-sprint/#1-usability","title":"1. Usability","text":"

The Usability team focused on how to integrate Aardvark metadata into GeoBlacklight's default user interface. Activities: - added new metadata fields to the item view page and search page facets - created a mockup showing how to display the new item relationships available with the new Aardvark fields - updated the default order of the search facets to reflect the user studies reported in two journal articles: - Mapping Search Queries to Metadata Fields in a GeoBlacklight Repository - Usability Analysis of the Big Ten Academic Alliance Geoportal: Findings and Recommendations for Improvement of the User Experience - determined the rules and configuration options for icon display

"},{"location":"blog/2021/09/new-releases-after-the-geoblacklight-community-summer-2021-sprint/#2-application-development-software-maintenance-coding","title":"2. Application Development (Software maintenance & coding)","text":"

The Application Development team focused on fixing deprecation warnings and incorporating the changes specified by the Usability track group.

"},{"location":"blog/2021/09/new-releases-after-the-geoblacklight-community-summer-2021-sprint/#current-release-version-340","title":"Current release: Version 3.4.0","text":"

This minor release includes maintenance issues and a few enhancements: - addressed numerous deprecation warnings inherited from the Blacklight software that will enable us to use Blacklight's Version 8 (still in development) in the future. - fixed a homepage map bug - updated the default interface with improved search result sorting options

New search results sort options

"},{"location":"blog/2021/09/new-releases-after-the-geoblacklight-community-summer-2021-sprint/#pre-release-version-400-alpha","title":"Pre-release: Version 4.0.0-alpha","text":"

This major pre-release includes several global changes: - fully incorporates the latest Aardvark metadata profile - features new facet names and ordering - includes multiple item relation widgets

New item relation widgets (outlined in purple box)

"},{"location":"blog/2021/09/new-releases-after-the-geoblacklight-community-summer-2021-sprint/#3-documentation-metadata-website-and-migration-guides","title":"3. Documentation (Metadata, website, and migration guides)","text":"

The Documentation team made assessments of existing metadata documentation and began the process of migrating the content to a new GitHub pages site in the OpenGeoMetadata organization. The latest documentation can be found here: https://opengeometadata.org

More documentation work is on the way in the form of workflow recommendations for converting metadata from the GeoBlacklight 1.0 schema to the newer Aardvark profile.

For more details, view our running notes from the daily standups, as well as the GitHub projects boards here and here.

"},{"location":"blog/2022/02/announcing-the-geoblacklight-community-winter-2022-sprint/","title":"Announcing the GeoBlacklight Community Winter 2022 Sprint","text":"

The GeoBlacklight Community collaborates on a 2-week sprint every Winter and Summer. Participation in the sprint is open to anyone and can include coding new features, fixing bugs, writing documentation, troubleshooting issues, and more.

Each sprint starts with a kickoff meeting where we will review our goals and volunteer to contribute towards various issues. This will be followed by short standup meetings every day to check-in and plan our next activities.

"},{"location":"blog/2022/02/announcing-the-geoblacklight-community-winter-2022-sprint/#event-dates","title":"Event Dates","text":"
  • The sprint will take place from February 14-25, 2022
  • Daily standup meetings: Weekdays, February 14-25 @ 1:00pm Central Time
"},{"location":"blog/2022/02/announcing-the-geoblacklight-community-winter-2022-sprint/#how-to-join","title":"How to Join","text":"

Send us your name and email using this Google form. The GeoBlacklight Community Coordinator will then send you the Zoom links with instructions on how to join.

"},{"location":"blog/2022/02/announcing-the-geoblacklight-community-winter-2022-sprint/#tracks-for-the-winter-2022-community-sprint","title":"Tracks for the Winter 2022 Community Sprint","text":"

Here is the proposed sprint work broken up into sections:

"},{"location":"blog/2022/02/announcing-the-geoblacklight-community-winter-2022-sprint/#coding","title":"Coding","text":"
  • Description: Creating code updates, fixes, and enhancements to commit to the GeoBlacklight code base
  • Useful experience: application development, Ruby, Ruby on Rails
  • Example tasks:
  • Refining GeoBlacklight\u2019s Version 4 Alpha release to a Beta version
  • Addressing bugs
  • Performing maintenance on framework and dependency support (i.e. Bootstrap, Rails)
  • Cleaning up Blacklight deprecation warnings
"},{"location":"blog/2022/02/announcing-the-geoblacklight-community-winter-2022-sprint/#metadata","title":"Metadata","text":"
  • Description: Assessing and documenting the metadata schema for GeoBlacklight
  • Useful experience: metadata, documentation
  • Example tasks:
  • creating an upgrade to Aardvark guide
  • creating new or updating existing sample metadata files
  • Updating OpenGeoMetadata and GeoCombine
"},{"location":"blog/2022/02/announcing-the-geoblacklight-community-winter-2022-sprint/#documentation","title":"Documentation","text":"
  • Description: Improving the public website, tutorials, and guides
  • Useful experience: writing, design, website editing
  • Example tasks:
  • improving the website design and content
  • creating a \u201cHow to launch GeoBlacklight\u201d guide for the website
  • organizing and updating the project wiki
"},{"location":"blog/2022/02/announcing-the-geoblacklight-community-winter-2022-sprint/#other-work","title":"Other Work","text":"
  • Description: Other GeoBlacklight related work is welcome at this sprint!
"},{"location":"blog/2022/03/geoblacklight-community-sprint-recap-winter-2022/","title":"GeoBlacklight Community Sprint Recap, Winter 2022","text":"

The latest GeoBlacklight Community Sprint took place from February 14-25, 2022. Long time GeoBlacklight sprinters have noted that this was likely our most productive event and undoubtedly featured the most active participants. We had over a dozen participants representing Cornell, Harvard, Johns Hopkins University, Princeton, Stanford, UMass Amherst, and the University of Minnesota / Big Ten Academic Alliance. Moreover, five contributors made their very first code commits to GeoBlacklight during this event.

Here are links to five tangible outcomes that we developed during the two-week sprint:

  1. Production release of GeoBlacklight, version 3.6.0
  2. Release candidate of GeoBlacklight, version 4.0.0-rc1
  3. New metadata website: opengeometadata.org
  4. Migrated guides for GeoBlacklight development: geoblacklight.org/guides
  5. New version of GeoCombine
"},{"location":"blog/2022/03/geoblacklight-community-sprint-recap-winter-2022/#highlights","title":"Highlights","text":""},{"location":"blog/2022/03/geoblacklight-community-sprint-recap-winter-2022/#new-locations-for-documentation","title":"New locations for documentation","text":"

All documentation for the GeoBlacklight community has been moved away from the GitHub Wiki. Information about GeoBlacklight, development, and customization can now be found on the Guides section of this website. All metadata documentation has been migrated to opengeometadata.org.

"},{"location":"blog/2022/03/geoblacklight-community-sprint-recap-winter-2022/#new-metadata-website-tools","title":"New metadata website & tools","text":"

Last year, our community developed a new metadata schema for GeoBlacklight, lovingly called Aardvark. This new schema is a significant change: it emphasizes interoperability and the unique characteristics of geodata & maps, so we decided to begin promoting it as an all-purpose discovery schema for geospatial resources. To that end, we launched a new website, opengeometadata.org that includes detailed schema documentation, guides, and examples.

We also released a new update of GeoCombine, a tool within OpenGeoMetadata for programmatically ingesting and converting metadata. This update includes improvements in (1) how it harvests from OpenGeoMetadata repositories and (2) the transformation template for FGDC to HTML.

"},{"location":"blog/2022/03/geoblacklight-community-sprint-recap-winter-2022/#spatial-geometry-for-search-and-display","title":"Spatial geometry for search and display","text":"

We finally cracked a long-standing spatial problem while simultaneously enabling a long-desired enhancement.

  • A Problem: bounding boxes that crossed the antimeridian would appear flipped or backward in search previews.
  • A related desired enhancement: to be able to display complex or multiple geometries in search previews instead of just a single bounding box.
  • Our solution: We incorporated a new option for a spatial metadata field that can use any WKT POLYGON or MULTIPOLYGON for display and searches. This enhancement was built with Geo3D for Solr and opened up the possibilities for what kind of geometries the metadata can feature. This also solves the antimeridian problem, as the metadata can now have two adjacent bounding boxes that will display correctly. Read more about how to format the Geometry field in OpenGeoMetadata to take advantage of this new feature.

Image Caption - Two bounding boxes

"},{"location":"blog/2022/03/geoblacklight-community-sprint-recap-winter-2022/#accessibility-user-interface-improvements","title":"Accessibility & user interface improvements","text":"

We improved the layout of the item pages in the default GeoBlacklight user interface in a few ways:

  1. The map preview is now above the metadata
  2. Added a full-screen option to the map preview
  3. Web services and downloads were converted from text links to \"Call-to-Action\" type buttons
  4. Updated the index map to use a more accessible color palette and added a legend

Image Caption - The map is now directly below the title and has a full-screen option. There is also a new button for web services and an adjusted dropdown button for Downloads

Image Caption - Shows improved colors and a legend above the index map

"},{"location":"blog/2022/03/geoblacklight-community-sprint-recap-winter-2022/#blacklight-future-alignment-plans","title":"Blacklight + future alignment plans","text":"

Whenever possible, we make a concentrated effort to align with the development practices of our underlying framework application, Blacklight. During the sprint, we collaborated with Blacklight developers to clear up deprecation warnings and remove an override customization used for bounding boxes.

For the future, we decided to eventually remove jQuery dependencies. This will help us stay consistent with Blacklight and eventually upgrade to Bootstrap 5. We also plan to rewrite our JavaScript code using the more modern version, ES6.

"},{"location":"blog/2022/03/geoblacklight-community-sprint-recap-winter-2022/#future-plans","title":"Future plans","text":"
  • Over the next six months, selected institutions will test the release candidate 4.0, which includes full Aardvark metadata support and all of the new features developed during the sprint.
  • We plan to schedule a dedicated documentation sprint that will include a broad cross-section of skills within our community.

For even more details, view our running notes from the daily standups and the GitHub projects boards for development and metadata.

"},{"location":"blog/2022/08/announcing-the-geoblacklight-community-summer-2022-sprint/","title":"Announcing the GeoBlacklight Community Summer 2022 Sprint","text":"

The GeoBlacklight Community collaborates on a 2-week sprint every Winter and Summer. Participation in the sprint is open to anyone and can include coding new features, fixing bugs, writing documentation, troubleshooting issues, and more.

Each sprint starts with a kickoff meeting where we will review our goals and volunteer to contribute towards various issues. This will be followed by short standup meetings every day to check-in and plan our next activities.

"},{"location":"blog/2022/08/announcing-the-geoblacklight-community-summer-2022-sprint/#event-dates","title":"Event Dates","text":"

This sprint is taking place over two non-consecutive weeks:

  • August 8-12, 2022 and September 12-16, 2022
  • Daily standup meetings: Weekdays @ 1:05pm Central Time
"},{"location":"blog/2022/08/announcing-the-geoblacklight-community-summer-2022-sprint/#how-to-join","title":"How to Join","text":"

Send us your name and email using this Google form. The GeoBlacklight Community Coordinator will then send you the Zoom links with instructions on how to join.

"},{"location":"blog/2022/08/announcing-the-geoblacklight-community-summer-2022-sprint/#tracks-for-the-summer-2022-community-sprint","title":"Tracks for the Summer 2022 Community Sprint","text":"

Here is the proposed sprint work broken up into sections:

"},{"location":"blog/2022/08/announcing-the-geoblacklight-community-summer-2022-sprint/#1-coding","title":"1. Coding","text":""},{"location":"blog/2022/08/announcing-the-geoblacklight-community-summer-2022-sprint/#primary-goal-upgrade-geoblacklight-version-4-from-pre-release-status-to-stable-version","title":"Primary Goal: Upgrade GeoBlacklight, Version 4 from pre-release status to stable version","text":"
  • Description: Creating code updates, fixes, and enhancements to commit to the GeoBlacklight code base
  • Useful experience: application development, Ruby, Ruby on Rails
  • Example tasks:
  • Addressing bugs
  • Performing maintenance on framework and dependency support (i.e. Bootstrap, Rails)
  • Cleaning up Blacklight deprecation warnings
"},{"location":"blog/2022/08/announcing-the-geoblacklight-community-summer-2022-sprint/#2-documentation","title":"2. Documentation","text":""},{"location":"blog/2022/08/announcing-the-geoblacklight-community-summer-2022-sprint/#primary-goal-improve-documentation-for-getting-started","title":"Primary Goal: Improve documentation for getting started","text":"
  • Description: Improving the public website, tutorials, and guides
  • Useful experience: writing, design, website editing
  • Example tasks:
  • improving the website design and content
  • creating a \u201cHow to launch GeoBlacklight\u201d guide for the website
  • organizing and updating the project wiki
"},{"location":"blog/2022/08/announcing-the-geoblacklight-community-summer-2022-sprint/#3-metadata","title":"3. Metadata","text":""},{"location":"blog/2022/08/announcing-the-geoblacklight-community-summer-2022-sprint/#primary-goal-improve-documentation-for-migrating-to-opengeometadata-schema-version-aardvark","title":"Primary Goal: Improve documentation for migrating to OpenGeoMetadata schema, version Aardvark","text":"
  • Description: Assessing and documenting the metadata schema for GeoBlacklight
  • Useful experience: metadata, documentation
  • Example tasks:
  • creating a metadata migration guide
  • creating new or updating existing sample metadata files
  • Updating the OpenGeoMetadata website
"},{"location":"blog/2022/08/announcing-the-geoblacklight-community-summer-2022-sprint/#other-work","title":"Other Work","text":"
  • Description: Other GeoBlacklight-related work is welcome at this sprint!
"},{"location":"blog/2022/09/the-wait-is-over---geoblacklight-40-is-here/","title":"The wait is over - GeoBlacklight 4.0 is here!","text":"

The GeoBlacklight Community is excited to announce the release of our next major version: GeoBlacklight 4.0. This version features official support for OGM Aardvark, and we have enriched the documentation for GeoBlacklight and OpenGeoMetadata.

KEY LINKS

  • The RubyGems GeoBlacklight v4.0.0 Release (GitHub)

  • Guide for Upgrading to GeoBlacklight Version 4

  • Guide for Upgrading Metadata to the OpenGeoMetadata (OGM) Aardvark Schema

  • gbl2aardvark: A new web-hosted application to automatically convert GeoBlacklight 1.0 JSON files to the OGM Aardvark schema

"},{"location":"blog/2022/09/the-wait-is-over---geoblacklight-40-is-here/#1-official-support-for-the-latest-opengeometadata-schema-ogm-aardvark","title":"1. Official support for the latest OpenGeoMetadata schema, OGM Aardvark.","text":"

OGM Aardvark is a geospatial metadata schema that was collaboratively developed by geospatial metadata specialists and librarians from twelve university libraries. Launched in 2021, it replaces the GeoBlacklight metadata schema version 1.0 (GBL 1.0). Compared to GBL 1.0, OGM Aardvark features syntactical updates to improve interoperability and incorporates additional fields for better descriptions of a wider range of resources. It is now the recommended schema for all institutions adopting GeoBlacklight.

By default, GeoBlacklight 4.0 displays all human-readable OGM Aardvark metadata values on the item view page, including the new fields for:

  • specifying rights and licenses (see figure 1)
  • categorizing records by class and type (see figure 2)
  • interactive widgets connecting records via seven different types of relationships

Figure 1

Image caption: Sample record item view page displaying new OGM Aardvark metadata fields and values

Note: Learn how to customize your item view pages with the Application Configuration section of the new Guide for Upgrading to GeoBlacklight Version 4

Figure 2

Image caption: The new Resource Class facet

Note: Read more about the Resource Class field on its schema documentation page

"},{"location":"blog/2022/09/the-wait-is-over---geoblacklight-40-is-here/#2-documentation-improvements","title":"2. Documentation improvements","text":"

We focused our Summmer Community Sprint 2022 on improving the technical documentation for GeoBlacklight and OpenGeometadata.

"},{"location":"blog/2022/09/the-wait-is-over---geoblacklight-40-is-here/#21-geoblacklight-documentation","title":"2.1 GeoBlacklight documentation","text":"

Check out our new application documentation site at https://geoblacklight.org/docs. Notable additions include:

  • a detailed guide for upgrading to version 4.0
  • an updated and vetted Quickstart Guide and Developer's Guide for installing GeoBlacklight locally
  • a new section called Running in Production, which includes example environments and maintenance recommendations
  • new guides for adding customizations to Leaflet: Homepage Map Centroid Clusters and Dynamic Basemap Switching
"},{"location":"blog/2022/09/the-wait-is-over---geoblacklight-40-is-here/#23-opengeometadata-documentation","title":"2.3 OpenGeoMetadata documentation","text":"

Visit our improved metadata documentation site at https://opengeometadata.org. Recent additions include:

  • easier navigation for finding OGM Aardvark schema elements, including a grouped chart and an alphabetical list
  • metadata crosswalk tables

    • OGM Aardvark - GBL 1.0 crosswalk table
    • OGM Aardvark - FGDC - ISO 19139 crosswalk table
  • new conversion tools

    • a draft XSLT for converting FGDC metadata to OGM Aardvark
    • gbl2aardvark: a new web hosted tool to automatically convert GeoBlacklight 1.0 JSON files to OGM Aardvark

Figure 3

Image caption: screenshot of gbl2aardvark

"},{"location":"blog/2022/09/the-wait-is-over---geoblacklight-40-is-here/#3-credits","title":"3. Credits","text":"

This release represents substantial and concerted efforts from many in our community: over the past one to two years, fourteen community members committed code to our GitHub repository for this release, while nearly twenty individuals contributed to creating our related project, the OGM Aardvark metadata schema.

Thank you to everyone for participating in this evolution!

"},{"location":"blog/2023/03/winter-sprint-2023/","title":"Winter sprint 2023","text":"

The latest sprint resulted in several new outcomes: a metadata field, multiple documentation pages, a release of the Sidecar Images plugin, and a dockerized container for GeoBlacklight.

KEY LINKS

  • Documentation for new Aardvark field, Display Note
  • GeoBlacklight release and technology dependency matrix.
  • Sidecar Images Plugin
  • Dockerized GeoBlacklight

Seven members of the GeoBlacklight Community participated in a one-week sprint for the week of February 27 - March 3, 2023. We focused on organizing the open issues for OpenGeoMetadata as well as how to plan for upcoming challenges with adopting the beta version of Blacklight 8.0.

The sprint resulted in several new outcomes: a metadata field, multiple documentation pages, a release of the Sidecar Images plugin, and a dockerized container for GeoBlacklight.

"},{"location":"blog/2023/03/winter-sprint-2023/#new-metadata-field-display-note-added-to-opengeometadata-version-aardvark","title":"New metadata field, \"Display Note\", added to OpenGeoMetadata, version Aardvark.","text":"

Many institutions using GeoBlacklight have implemented custom fields to display warnings or tips to users. These fields are used to highlight important information about a resource that users need to be aware of before accessing or using it. For example, a warning might indicate that the resource is incomplete or may be difficult to use due to technical limitations. A tip might provide guidance on how to use the resource effectively, such as suggesting specific software or tools that are compatible with the data.

The main downside of custom fields is that they are not interoperable; when we share metadata across institutions, this information is lost. Another drawback is that custom fields require local development time to implement as opposed to functionality that comes with GeoBlacklight out-of-the-box.

During the sprint, we devised a single field to capture this information, called \"Display Note\". This versatile field is inspired by website widgets known variably as callouts (general usage), admonitions (Python Markdown), or alerts (Bootstrap). Callouts are generally highlighted in colors and/or icons that indicate the type of information being offered, such as a red background for a Warning.

Details about the new field:

  • URI: gbl_displayNote_sm
  • Multiplicity: this field is an array that can have multiple values
  • Syntax: type of callout: text of callout
  • Example value:
    [\n\"Warning: This text (starting with 'This text') will be displayed in a red box\",\n\"Info: This text (starting with 'This text') will be displayed in a blue box\",\n\"Tip: This text (starting with 'This text') will be displayed in a green box\",\n\"This is text without a tag and it will be assigned the default 'note' style\"\n]\n

Right now, the field can be added to your GeoBlacklight Solr instance as a simple string field. In the next sprint, we plan to add functionality to GeoBlacklight that will automatically read the first part of the field (ex. Warning:) and display the text with highlight background prominently on the page.

"},{"location":"blog/2023/03/winter-sprint-2023/#versioning-and-roadmaps","title":"Versioning and roadmaps","text":"

We have a new Technology Matrix that maps out our GeoBlacklight release plans and technology dependencies. This matrix takes into account compatiblity with the Blacklight Project, which recently pre-released a beta version 8.0..

"},{"location":"blog/2023/03/winter-sprint-2023/#new-release-of-geoblacklight-sidecar-images-plugin","title":"New Release of GeoBlacklight Sidecar Images Plugin","text":"

We upgraded the GeoBlacklight Sidecar Images Plugin, which can display image thumbnails on search result pages. The plugin now uses the same rake tasks as GeoBlacklight for running the test suite.

"},{"location":"blog/2023/03/winter-sprint-2023/#new-dockerized-version-of-geoblacklight","title":"New Dockerized version of GeoBlacklight","text":"

Developers from Harvard University have created a built instance of GeoBlacklight in a Docker context. This will allow new and existing users to test and develop an instance of GeoBlacklight within the Docker environment.

Thank you to everyone who participated during this sprint! Please consider joining up at the next community sprint coming in June 2023.

"},{"location":"blog/2023/06/geoblacklight-v41-released-along-with-new-documentation-pages/","title":"GeoBlacklight v4.1 released along with new documentation pages","text":""},{"location":"blog/2023/06/geoblacklight-v41-released-along-with-new-documentation-pages/#summary","title":"Summary","text":"

The GeoBlacklight Community held a community sprint in June 2023 and produced a new minor release of GeoBlacklight, new & improved documentation pages, and a platform upgrade of the GeoBlacklight website.

"},{"location":"blog/2023/06/geoblacklight-v41-released-along-with-new-documentation-pages/#new-geoblacklight-features","title":"New GeoBlacklight Features","text":""},{"location":"blog/2023/06/geoblacklight-v41-released-along-with-new-documentation-pages/#display-note-field","title":"Display Note field","text":"

Version 4.1 supports the latest OpenGeoMetadata Aardvark field, Display Note. This field presents a prominent shaded box with a message to the user that can be coded by color and icon. The messages can be labeled as Danger (red), Info (blue), Tip (green), Warning (yellow), or nonspecific (grey).

Display note examples"},{"location":"blog/2023/06/geoblacklight-v41-released-along-with-new-documentation-pages/#location-inset-for-scanned-map-images","title":"Location inset for scanned map images","text":"

We added a location inset map that appears on the item view page for scanned maps hosted with IIIF. This shows users a bounding box of the area represented on the scanned map.

Location map with IIIF image"},{"location":"blog/2023/06/geoblacklight-v41-released-along-with-new-documentation-pages/#additional-built-in-icons","title":"Additional built-in icons","text":"

As new institutions join the GeoBlacklight community, we add their institutional logo to our icon library for interoperable display alongside their metadata records. New icons added to this release include:

  • University of Arizona
  • American Geographical Society Library - UWM Libraries
  • University of Nebraska
  • Rutgers University

Screenshot preview of the GeoBlacklight icon library"},{"location":"blog/2023/06/geoblacklight-v41-released-along-with-new-documentation-pages/#improved-item-relations-widget","title":"Improved item relations widget","text":"

Version 4.0 (September 2022) introduced item relations widgets to capture a variety of relationships as set out by the OpenGeoMetadata Aardvark schema. The latest release improves the display in several ways:

  • All widget labels now match their metadata field names
  • For parent pages, the widget will now include a truncated list of child items along with a list to browse all child items; fixed issue #1290
  • For items using Replaces and Replaced By, GeoBlacklight will only query the Replaces field, but will display relationship widgets for each item; addressed issue #1264

Info

To read more about version 4.1 and how to upgrade, read the release notes on GitHub.

"},{"location":"blog/2023/06/geoblacklight-v41-released-along-with-new-documentation-pages/#new-documentation-pages","title":"New Documentation Pages","text":""},{"location":"blog/2023/06/geoblacklight-v41-released-along-with-new-documentation-pages/#geoblacklight-docs","title":"GeoBlacklight Docs","text":"
  • New information on our Getting Started page about using an external Solr instance.
  • New guide for configuring the Settings (settings.yml) file to set the logo, default paths, and other local customizations.
  • New page for configuring the Catalog Controller (catalog_controller.rb) file to customize facets, metadata fields, search behavior, and basemaps.
  • Clarifications to the Upgrading to 4.0 Guide that list the item relationships to display.
"},{"location":"blog/2023/06/geoblacklight-v41-released-along-with-new-documentation-pages/#opengeometadata","title":"OpenGeoMetadata","text":"
  • New page documenting all relationship fields
  • Established Aardvark field URIs as distinct from field names; PR #95 & #96
  • Integrated controlled values into the main Aardvark field documentation; PR #91
  • Various copyediting and link fixes
  • Improved color schema and enabled dark mode; PR #94
opengeometadata.org light and dark mode"},{"location":"blog/2023/06/geoblacklight-v41-released-along-with-new-documentation-pages/#geoblacklight-website-glow-up","title":"GeoBlacklight Website Glow-up","text":"

We refreshed this website (https://geoblacklight.org) with a different platform (Material for MkDocs) that includes an updated showcase page and a modernized blog.

GeoBlacklight administrators can now use simpler instructions for contributing their instance to the Showcase page.

Thank you to everyone who participated during this sprint! Please consider joining us at the next community sprint coming this winter. -Karen

"},{"location":"blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/","title":"Winter 2024 Sprint Wrap-up - GeoBlacklight v4.2","text":""},{"location":"blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/#summary","title":"Summary","text":"

The GeoBlacklight Community conducted a two-week sprint from January 29 to February 9, 2024, culminating in the release of a new version of GeoBlacklight. This update not only brings maintenance enhancements but also introduces new viewers for COGs, PMTiles, and IIIF Manifests. Additionally, the community has made substantial improvements to the documentation, making it easier to maintain and more informative for new users.

"},{"location":"blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/#new-viewers-in-geoblacklight","title":"New Viewers in GeoBlacklight","text":""},{"location":"blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/#openlayers-for-viewing-modern-data-formats-cogs-and-pmtiles","title":"OpenLayers for viewing modern data formats: COGS and PMTiles","text":"

Version 4.2.0 of GeoBlacklight introduces new viewers for two web-based geospatial data formats: Cloud Optimized GeoTIFFs (COGs) and PMTiles. COGs are an extension of the traditional GeoTIFF file format, optimized for cloud storage and web access, while PMTiles offer a method for storing and accessing numerous map tiles in a single, streamlined file. Both formats are designed to improve efficiency and speed across the web by utilizing HTTP range requests. This allows users to request only parts of the data, rather than entire datasets or images, distinguishing these formats from traditional geospatial web services like WFS or ArcGIS REST services. Both of the new viewers use OpenLayers instead of Leaflet, which, up until now, has been the only mapping library for GeoBlacklight.

Overall, the integration of IIIF Manifests, COGs, and PMTiles into the latest version of GeoBlacklight reflects a growing trend in data sharing, moving away from the convention of offering discrete, downloadable files and toward a more dynamic model of streaming data.

  • COGs viewer in GeoBlacklight

  • PMTiles viewer in GeoBlacklight

"},{"location":"blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/#clover-for-iiif-manifests","title":"Clover for IIIF Manifests","text":"

In earlier versions, GeoBlacklight was limited to displaying single-part images through the IIIF Image APIs, requiring anyone interested in showcasing multipart images via IIIF Manifests to integrate a custom viewer. With the release of Version 4.2, this capability is now included by default with Clover, a viewer developed by the Samvera community.

"},{"location":"blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/#baseline-support-for-latest-openindexmaps-spec-v10","title":"Baseline support for latest OpenIndexMaps spec v1.0","text":"

The OpenIndexMaps project has released version 1.0, an expanded spec that replaced the legacy version 0. There are minor semantic differences between the specs, and we updated GeoBlacklight to support either version.

"},{"location":"blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/#maintenance-framework-updates","title":"Maintenance & Framework updates","text":"
  • Support for Ruby on Rails version 7.1
  • Deprecated Faraday
  • Implemented Vite Ruby, via the vite-rails Ruby gem
"},{"location":"blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/#documentation-improvements","title":"Documentation Improvements","text":"
  • Merged the GitHub repositories of the public GeoBlacklight website and the dedicated docs. This will make it easier to maintain over time and makes searching for information from the main site more fruitful.
  • Reorganized the navigation categories and added a Reference section for quick look-up charts
  • New or updated pages
    • For Developers: Added more details and a diagram for new developers
    • User Authentication: New page about configuring user accounts and logins
    • JSONs and GeoJSONs: New page clarifying the difference between the file formats
    • Redesigned the Showcase grid to include public code repositories.

More details

"},{"location":"blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/#to-read-more-about-version-42-and-how-to-upgrade-read-the-release-notes-on-github","title":"To read more about version 4.2 and how to upgrade, read the release notes on GitHub.","text":""},{"location":"blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/#to-read-more-about-our-sprint-activities-browse-the-github-project-board","title":"To read more about our sprint activities, browse the GitHub project board","text":""},{"location":"blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/#whats-on-the-horizon","title":"What's on the horizon?","text":""},{"location":"blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/#geoblacklight-version-5","title":"GeoBlacklight version 5","text":"

For the next major release of GeoBlacklight, our objectives require updating our dependencies and frameworks. In the recent sprint, we achieved considerable progress in updating our JavaScript library to ES6 modules and eliminating the dependency on jQuery. These updates are now part of a development branch, which we plan to further refine in the next sprint.

We also plan to upgrade to Bootstrap v5 and Blacklight v8. Review our Release Calendar for more details.

"},{"location":"blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/#investigating-openlayers","title":"Investigating OpenLayers","text":"

We have been using Leaflet since the first version of GeoBlacklight, but have begun exploring other mapping libraries, particularly OpenLayers, for some potential future features.

"},{"location":"blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/#improving-the-default-interface","title":"Improving the default interface","text":"

After we upgrade our libraries, we will finally be able to address the recommendations from 2023's Maps UX Workgroup.

Have a suggestion for a GeoBlacklight Version 5? Get in touch with the community or create an issue in our GitHub repository.

Thank you to everyone who participated during this sprint! Please consider joining us at the next community sprint coming in Summer 2024.

-Karen

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/","title":"Geo4LibCamp 2024 / GeoBlacklight Workshop","text":"

Workshop date: May 20, 2024

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#facilitators","title":"Facilitators","text":"
  • Eric Larson \u2014 Big Ten Academic Alliance
  • Eliot Jordan \u2014 Princeton University

With many thanks to all the GeoBlacklight community members who help keep our website and project documentation up to date.

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#part-1-introductions-and-attendee-goals-5-minutes","title":"Part 1: Introductions and Attendee Goals (5 minutes)","text":"

Eric and Eliot are both core contributors/maintainers of the GeoBlacklight project's source code. Let's all get to know one another and hear what each attendee wants to learn in this session.

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#part-2-geoblacklight-for-beginners-30-minutes","title":"Part 2: GeoBlacklight for Beginners (30 minutes)","text":"
  • GeoBlacklight Overview (5 minutes)
  • Setting up a GeoBlacklight Environment (5 minutes)
  • Create a GeoBlacklight Application (10 minutes)
  • Index Solr Documents (10 minutes)
"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#screenshot-geoblacklight-website","title":"Screenshot - Geoblacklight Website","text":""},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#geoblacklight-overview","title":"GeoBlacklight Overview","text":"
  • What is GeoBlacklight?
  • GeoBlacklight Features
  • GeoBlacklight Community
"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#what-is-geoblacklight","title":"What is GeoBlacklight?","text":"

GeoBlacklight is a Ruby on Rails engine, based on the popular open-source project Blacklight. The aim of the project is to provide a simple, effective open-source application for discovery of geospatial data. Many institutions are using GeoBlacklight to provide a search engine across a federated catalog of geospatial data \u2014 GeoBlacklight project showcase.

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#geoblacklight-key-features","title":"GeoBlacklight - Key Features","text":"

GeoBlacklight extends the functionality of Blacklight by providing the following:

  • Text and spatial search with ranking
  • Facet by institution, year, publisher, data type, access, format
  • Facet by place, subject
  • Results list view with icons, snippets, and map view of bounding boxes
  • Spatial search on map in result list
  • Detail map view for WMS features with feature inspection
  • IIIF scanned map viewer
  • Download the original file (Shapefile, GeoTIFF, GeoJSON, Esri Geodatabase, GeoPackage, or other SQLite database)
  • Download generated Shapefile/GeoTIFF/KML/GeoJSON
  • Built-in sample Solr index
  • Built on top of Blacklight platform
  • Search history
  • Bookmark layers
  • Share link via email
  • Sort by relevance, year, title
  • Customizable skin and facets

GeoBlacklight Key Features

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#geoblacklight-community","title":"GeoBlacklight - Community","text":"

Participants in the GeoBlacklight community come from a variety of professional and intellectual backgrounds (including librarians, software developers, metadata specialists, applied researchers, and others), but we share a common interest in making reliable and high-quality geospatial data easily accessible to members of the research community and the broader public. Many of us work in libraries and other cultural heritage institutions that deploy (or are planning to deploy) GeoBlacklight instances to disseminate and publicize their spatial data collections.

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#get-involved","title":"Get Involved","text":"
  • Volunteer for a Community Role
  • Join a Workgroup or Interest Group
  • Follow our Google Group
  • Chat on Slack
  • Attend a Monthly Meeting
  • Participate in a Community Sprint
  • Share Metadata

GeoBlacklight Community

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#setting-up-a-geoblacklight-environment","title":"Setting Up a GeoBlacklight Environment","text":"

You should have the following installed before beginning:

  • Ruby \u2014 For Ruby on Rails
  • Java - Apache Solr runs on Java 11 or greater.

Follow the GoRails Setup steps to install a Ruby on Rails environment. Homebrew can help you install Java on macOS or Windows Subsystem for Linux.

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#create-a-geoblacklight-application","title":"Create a GeoBlacklight Application","text":"

Bootstrap a new GeoBlacklight Ruby on Rails application using the template script:

DISABLE_SPRING=1 rails new gbl-app -m https://raw.githubusercontent.com/geoblacklight/geoblacklight/main/template.rb\n

Then run the geoblacklight:server rake task to run the application:

cd gbl-app\nbundle exec rake geoblacklight:server\n
  • Visit your GeoBlacklight application at: http://localhost:3000
  • Visit the Solr admin panel at: http://localhost:8983/solr/#/blacklight-core
"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#screenshot-geoblacklight-homepage","title":"Screenshot - GeoBlacklight Homepage","text":""},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#screenshot-solr-admin-panel","title":"Screenshot - Solr Admin Panel","text":""},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#index-solr-documents","title":"Index Solr Documents","text":"

Time to add some data to our application. GeoBlacklight uses OpenGeoMetadata's Aardvark metadata schema.

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#opengeometadata-aardvark","title":"OpenGeoMetadata / Aardvark","text":"

OpenGeoMetadata (OGM) is a discovery metadata schema for geospatial resources and an open platform for sharing metadata files.

OGM Aardvark is a discovery metadata schema for geospatial resources. It was intentionally developed with cross-application in mind and can be used to describe geospatial assets of all kinds. It is also the newest metadata application profile schema for GeoBlacklight. Launched in 2021, it replaces the GeoBlacklight metadata schema version 1.0 (GBL 1.0). Compared to GBL 1.0, Aardvark incorporates additional fields for better descriptions of a wider range of resources, as well as syntactical updates in order to improve interoperability between institutions and between schemas.

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#rake-tasks","title":"Rake Tasks","text":"

With your Solr server and Rails server already running (via the geoblacklight:server rake task above), open a new terminal window and index the GeoBlacklight project's test fixtures (OGM Aardvark JSON files) via the rake task below:

bundle exec rake \"geoblacklight:index:seed[:remote]\"\n
"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#geocombine","title":"GeoCombine","text":"

Another option for indexing data is GeoCombine. GeoCombine is a Ruby toolkit for managing geospatial metadata, including:

  • Tasks for cloning, updating, and indexing OpenGeoMetadata metadata
  • Library for converting metadata between standards

Example harvest from a single repository

bundle exec rake geocombine:clone\\[edu.umn\\]\n

Index your GeoCombine data. Here we're setting our schema version to Aardvark, which is the GBL v4.0+ metadata schema.

SCHEMA_VERSION=\"Aardvark\" bundle exec rake geocombine:index\n
"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#break-and-qa-session-10-minutes","title":"BREAK and Q&A Session (10 minutes)","text":"

Eric will return to this tutorial to list the attendee questions and discussion topics from this break (I promise).

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#part-2-advanced-geoblacklight-40-minutes","title":"Part 2: Advanced GeoBlacklight (40 minutes)","text":"

Using local configuration options and adding community plugins to customize your GeoBlacklight instance.

  • Local config: settings.yml (5 minutes)
  • Local config: catalog_controller.rb (5 minutes)
  • Plugin: Blacklight::Allmaps (10 minutes)
  • Plugin: GeoBlacklight Sidecar Images (10 minutes)
  • Plugin: GeoBlacklight Admin (10 minutes)
"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#local-config-settingsyml-5-minutes","title":"Local config: settings.yml (5 minutes)","text":"

In your local GeoBlacklight application, the first file that provides considerable customization and configuration options is settings.yml. This file is specific to GeoBlacklight and it sets constant variables for the application to use, including:

  • APPLICATION_LOGO_URL
  • BBOX_WITHIN_BOOST
  • HOMEPAGE_MAP_GEOM
  • FIELDS
  • WEBSERVICES_SHOWN
  • DISPLAY_NOTES_SHOWN
  • RELATIONSHIPS_SHOWN
  • and more...

This file is loaded into the application via the Ruby config gem.

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#local-config-catalog_controllerrb","title":"Local config: catalog_controller.rb","text":"

The next significant place where customizations and configuration options occur is within the app/controllers/catalog_controller.rb file.

Within this file, you'll find options to define/set:

  • Maps > Default Leaflet basemap (config.basemap_provider)
  • Solr > Default solr params (config.default_solr_params)
  • Search Results > Per Page option (config.default_per_page)
  • Search Results > Sort options (config.add_sort_field)
  • Search Results > Define your list of facets (config.add_facet_field)
  • Search Results > Define your displayed metadata fields (config.add_index_field)
  • Show Page > Define your displayed metadata fields (config.add_show_field)
  • Show Page > Define Sidebar Tool options (config.add_show_tools_partial)
"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#plugin-blacklightallmaps","title":"Plugin: Blacklight::Allmaps","text":"

Let's add support for Allmaps georeferenced maps to our example application \u2014 we had two lighting talks on Allmaps this morning: Open-Source Georeferencing and Curating with Allmaps | Blacklight::Allmaps (slidedeck).

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#install","title":"Install","text":"

Follow the blacklight_allmaps installation steps to add this plugin to our Gemfile.

# Gemfile\ngem \"blacklight_allmaps\"\n

And then execute:

bundle install\n

Run the generator script:

# For GeoBlacklight\nLIGHT=geoblacklight bundle exec rails generate blacklight:allmaps:install\n
"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#seed-fixtures","title":"Seed Fixtures","text":"

To populate Solr with some example data, you can run these tasks

# For GeoBlacklight...\nLIGHT=geoblacklight rake blacklight_allmaps:index:gbl_fixtures\n
"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#harvest-allmaps-iiif-annotation-data","title":"Harvest Allmaps IIIF Annotation Data","text":"

We harvest and store Allmaps IIIF Annotation data locally. The rake task here kicks off a background harvest process that walks through your Solr index documents (using CursorMark) and checks each document for georeferenceable? - the presence of a IIIF Manifest. If the document is indeed georeferenceable? (true) we ping the Allmaps API to determine if the map/item has already been georeferenced in Allmaps.

# For Blacklight or GeoBlacklight\nbundle exec rake blacklight_allmaps:sidecars:harvest:allmaps\n
"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#populate-the-georeferenced-facet","title":"Populate the Georeferenced Facet","text":"

We expose the georeferenced items in the Blacklight user interface via a Georeferenced facet:

# For Blacklight or GeoBlacklight\nbundle exec rake blacklight_allmaps:index:georeferenced_facet\n
"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#screenshot-item-show-page","title":"Screenshot - Item Show Page","text":"

Example

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#plugin-geoblacklight-sidecar-images","title":"Plugin: GeoBlacklight Sidecar Images","text":"

This plugin adds thumbnails to search results. Let's follow the installation steps to add this feature to our example application.

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#install_1","title":"Install","text":"

Add this gem to our Gemfile.

# Gemfile\ngem \"geoblacklight_sidecar_images\", \"~> 1.0\"\n

Bundle.

bundle install\n

Run the generator.

bin/rails generate geoblacklight_sidecar_images:install\n

Run the database migration.

bin/rails db:migrate\n

Set your variant processor to :vips in config/application.rb. This will use libvips to generate our thumbnails.

    # Image Processing\n    config.active_storage.variant_processor = :vips\n
"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#harvest-thumbnails","title":"Harvest Thumbnails","text":"

Spawn some background jobs to harvest images for all the documents in our Solr index. In development mode, these will run \"inline\". In a production environment, you'd want to use

bundle exec rake gblsci:images:harvest_all\n
"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#screenshot-item-show-page_1","title":"Screenshot - Item Show Page","text":"

Example

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#plugin-geoblacklight-admin","title":"Plugin: GeoBlacklight Admin","text":"

Notice: This is going to be a full pivot from our current example codebase we've been building.

GeoBlacklight Admin is a GeoBlacklight plugin, built on Kithe, that provides a CSV-based import/export workflow for OpenGeoMetadata's Aardvark schema, rich web-forms for editing documents, and publication lifecycle tools for working with draft documents or published data. GBL Admin is also the Big Ten Academic Alliance's production workflow tool.

"},{"location":"blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/#installation","title":"Installation","text":"
  • Create PG database:
psql postgres\nCREATE DATABASE geoblacklight_development;\n
  • Run application template install script below:
rails _7.1.3.2_ new gbl-admin -m https://raw.githubusercontent.com/geobtaa/geoblacklight_admin/develop/template.rb\n

Run the Application

bundle exec rake geoblacklight:server\n

"},{"location":"docs/","title":"Welcome to the GeoBlacklight Documentation","text":"

The GeoBlacklight documentation aims to help anyone interested in implementing the software, or developers working to customize or upgrade an existing implementation.

"},{"location":"docs/adding_mirador_viewer/","title":"Add Mirador IIIF Manifest Viewer","text":""},{"location":"docs/adding_mirador_viewer/#what-is-mirador","title":"What is Mirador?","text":"

Mirador is an open source IIIF image and IIIF manifest viewer. Core GeoBlacklight contributors have contributed significantly to Mirador. Learn more on the Project Mirador website.

"},{"location":"docs/adding_mirador_viewer/#install","title":"Install","text":"

Add the Mirador 3 javascript and stylesheet assets to your project. If you are using Yarn, you can just add mirador via:

yarn add mirador\n

Or you can edit your package.json file like so, and run:

  ...\n  \"dependencies\": {\n    \"@babel/cli\": \"^7.5.5\",\n    \"@babel/core\": \"^7.5.5\",\n    \"@rails/webpacker\": \"^4.0.7\",\n    \"babel-loader\": \"^8.0.6\",\n    \"bloodhound-js\": \"^1.2.3\",\n    \"bootstrap\": \"^4.3.1\",\n    \"jquery\": \"^3.4.0\",\n    \"mirador\": \"^3.3.0\",\n    \"readmore-js\": \"^3.0.0-beta-1\",\n    \"typeahead.js\": \"^0.11.1\",\n    \"lodash\": \"^4.17.13\",\n    \"lodash.template\": \"^4.5.0\"\n  }\n
yarn install\n
"},{"location":"docs/adding_mirador_viewer/#configure","title":"Configure","text":"

With Mirador installed, you need to add the javascript library to your application.

application.js

// Mirador\n//= require mirador/dist/mirador.min.js\n

"},{"location":"docs/adding_mirador_viewer/#add-a-geoblacklight-viewer","title":"Add a GeoBlacklight Viewer","text":"

Within app/assets/javascripts/geoblacklight/viewers add a new iiif_manifest.js viewer, and specify your Mirador configuration values.

//= require geoblacklight/viewers/viewer\n\nGeoBlacklight.Viewer.IiifManifest = GeoBlacklight.Viewer.extend({\n  load: function() {\n    var manifest_uri = document.getElementById('map').getAttribute('data-url');\n\n    var miradorInstance = Mirador.viewer({\n       id: 'map',\n       themes: {\n         light: {\n           palette: {\n             type: 'light',\n             primary: {\n               main: '#0088ce',\n             },\n           },\n         },\n       },\n       windows: [{\n         manifestId: manifest_uri,\n         thumbnailNavigationPosition: 'far-bottom',\n       }],\n       window: {\n         hideSearchPanel: false,\n         hideWindowTitle: true,\n         hideAnnotationsPanel: true,\n         allowClose: false,\n         allowMaximize: false,\n         allowFullscreen: true,\n       },\n       workspace: {\n         showZoomControls: true,\n       },\n       workspaceControlPanel: {\n         enabled: false,\n       }\n     });\n  }\n});\n

Override the GeoBlacklight ItemViewer to add support for your iiif_manifest viewer. In the B1G Geoportal we keep our local ItemViewer customizations in config/initializers/item_viewer.rb

module Geoblacklight\n  class ItemViewer\n    def initialize(references)\n      @references = references\n    end\n\n    def viewer_protocol\n      return 'map' if viewer_preference.nil?\n      viewer_preference.keys.first.to_s\n    end\n\n    def viewer_endpoint\n      return '' if viewer_preference.nil?\n      viewer_preference.values.first.to_s\n    end\n\n    def wms\n      @references.wms\n    end\n\n    def iiif\n      @references.iiif\n    end\n\n    # HERE - Added viewer\n    def iiif_manifest\n      @references.iiif_manifest\n    end\n\n    def tiled_map_layer\n      @references.tiled_map_layer\n    end\n\n    def dynamic_map_layer\n      @references.dynamic_map_layer\n    end\n\n    def feature_layer\n      @references.feature_layer\n    end\n\n    def image_map_layer\n      @references.image_map_layer\n    end\n\n    def index_map\n      @references.index_map\n    end\n\n    # HERE - also need to specify viewer preference\n    def viewer_preference\n      [index_map, wms, iiif, iiif_manifest, tiled_map_layer, dynamic_map_layer,\n       image_map_layer, feature_layer].compact.map(&:to_hash).first\n    end\n  end\nend\n
"},{"location":"docs/adding_mirador_viewer/#enjoy","title":"Enjoy!","text":"

Add a IIIF Manifest-based fixture to your spec fixtures and reload the application (rake geoblacklight:server).

"},{"location":"docs/adding_svg_icons/","title":"SVG Icons","text":""},{"location":"docs/adding_svg_icons/#add-new-svgs-to-geoblacklight-or-your-local-gbl-application","title":"Add new SVGs to GeoBlacklight or your local GBL application:","text":"
  1. Add your new or replacement SVG icon into the /app/assets/images/blacklight directory
  2. Add an :en I18n translation entry for the SVG icon in /config/locales/geoblacklight.en.yml, following this pattern:
    ...\n      blacklight:\n        icon:\n          arrow-circle-down: Arrow within a circle, pointing down\n          baruch-cuny: Baruch College\n          berkeley: University of California, Berkeley\n          your-new-icon-filename-without-the-extension: Your new icon description\n

Render your new SVG icon using the blacklight_icon helper like so:

<%= blacklight_icon('icon-filename') %>\n
"},{"location":"docs/adding_svg_icons/#svg-icon-maintenance","title":"SVG Icon Maintenance","text":"

The stock GeoBlacklight SVG icons come from institutional partners and Font Awesome. The IcoMoon App was used to generate a working project board of icons.

To view the IcoMoon project board:

  1. Visit https://icomoon.io/app/#/select
  2. Drag and drop the geoblacklight-icons.json file onto the page

Now you'll see all icons in the project. You can add additional icons, change the default color value, or export to other formats here, ex. PNGs.

"},{"location":"docs/catalog_controller/","title":"Update the Catalog Controller","text":"

A number of other configurations you may need to update are handled in your app/controllers/catalog_controller.rb file. This file has plenty of code comments to help you out with the details, so the following is just a brief summary of the main configurations you can make within it.

Configurations you can alter include:

  • Search Facet Management
    • Determine which fields are exposed to users in the search facet panel, and how they are labeled.
  • Search Results Fields
    • Determine which fields will be displayed for each item in the search results list.
  • Item View Fields
    • Show/hide fields from the catalog view for each item. Note that a number of default fields are not displayed directly in the catalog view because they are used to power different aspects of the presentation.
  • Default Search Behavior
    • Alter search ranking, sorting, and filtering.
  • Basemap Style
    • See Switching the default basemap
"},{"location":"docs/data_relations_widget/","title":"Data Relations Widget","text":"

Warning

This documentation is for GeoBlacklight versions 1.3 to versions 3.x only. Beginning with version 4.0, GeoBlacklight performs this function by default.

Beginning with v1.3.0, GeoBlacklight supports simple visualization of parent/children relations between records in a catalog. When records that were derived from a parent record point back to that parent, it enables a toolbar widget that displays the relation.

To make use of this, we have introduced support for a \"source\" field in GeoBlacklight metadata records. The actual key of this field is arbitrary \u2013\u2013\u00a0just make sure that Settings.FIELDS.SOURCE properly reflects what you want to use \u2013\u2013\u00a0but the GeoBlacklight schema allocates the Dublin Core field dc_source_sm for this purpose.

The responsibility of this field is to point to a parent document (a document from which the current one is derived). The value should be the layer_slug_s of that parent (or parents), which also resides in your catalog. No modifications need to be made to the parent record in order to point back towards the derived records.

Here is an example use of the dc_source_sm field, from a GeoBlacklight schema record:

  \"dc_source_sm\": [\n    \"nyu_2451_34635\",\n    \"nyu_2451_34636\"\n  ],\n
In the above case, the record being described is derived from two different records in the catalog (namely, nyu_2451_34635 and nyu_2451_34636).

Now, when navigating to the show page for either the current record, or one of the two parent records, a user will see something like this:

This functionality also provides a HTML and JSON API for viewing all parent/child datasets for a record. The route used is: localhost:3000/catalog/fake-record-001/relations

A sample JSON response for a record with two parents and no children might look like this:

{\n  \"ancestors\": {\n    \"numFound\": 2,\n    \"start\": 0,\n    \"docs\": [\n      {\n        \"dc_title_s\": \"2016 NYC Geodatabase, ArcGIS Version (jan2016)\",\n        \"layer_slug_s\": \"nyu_2451_34636\"\n      },\n      {\n        \"dc_title_s\": \"2016 NYC Geodatabase, Open Source Version (jan2016)\",\n        \"layer_slug_s\": \"nyu_2451_34635\"\n      }\n    ]\n  },\n  \"descendants\": {\n    \"numFound\": 0,\n    \"start\": 0,\n    \"docs\": []\n  },\n  \"current_doc\": \"nyu_2451_34513\"\n}\n

"},{"location":"docs/developers/","title":"For Developers","text":"

This page is for software developers looking to build GeoBlacklight from source, especially to contribute code to the core application.

Creating a custom application

If you are looking to start a new, branded and customized GeoBlacklight application for your institution, follow the Quick Start instructions. The application you create will inherit from the latest stable GeoBlacklight release. See the architecture diagram below for more context.

"},{"location":"docs/developers/#dependencies","title":"Dependencies","text":"

You should have the following installed before beginning:

  • Ruby > 3.0.0
  • Java > JRE version 11 or higher
"},{"location":"docs/developers/#build-the-application","title":"Build the Application","text":"

Clone the code base from the official GeoBlacklight repository or from your own fork if you plan to make upstream pull requests.

$ git clone git@github.com:geoblacklight/geoblacklight.git\n

Once cloned, enter the repository and install dependencies:

$ cd geoblacklight\n$ bundle install\n

Now initialize and start the application:

$ bundle exec rake geoblacklight:server\n

This command will executes all of the following steps and leave you with a running a local instance of GeoBlacklight:

  • Download, configure, and start a local Solr instance
    • Located in tmp/solr
  • Seed this Solr instance with test fixtures
    • JSON file fixtures located in spec/fixtures/solr_documents/
  • Create a test application
    • Located in .internal_test_app/
  • Create a development database within the test application
    • Located at .internal_test_app/storage/development.sqlite3
    • Database connection defined in .internal_test_app/config/database.yml
    • ActiveRecord supports PostgreSQL, SQLite, and MySQL (learn more)
  • Run the Rails server

Troubleshooting

If you run into issues running this rake task, try removing your Gemfile.lock file and removing the test app with rm -R .internal_test_app. Then run bundle install before running the above command again.

You should now be able to visit http://localhost:3000/ in a web browser and see the test application. If you modify content in the test application, these changes will be reflected on browser refresh. This may be a good time to learn more about the GeoBlacklights's structure:

In the diagram above, \"My Application\" is actually the local test application, .internal_test_app, and it is analogous to the standalone Rails application that you would create through the Quick Start instructions.

"},{"location":"docs/developers/#running-solr-and-rails-server-separately","title":"Running Solr and Rails server separately","text":"

You may decide to run Solr and the Rails server separately. Solr can be run separately using either Docker or solr_wrapper.

To use solr_wrapper use the following rake task, which starts Solr and seeds the index with data:

$ rake geoblacklight:solr\n

To use docker instead, start the server:

$ docker compose up\n
If using docker to run Solr, you need to manually seed the index with data. This seed task must be run after the rails server has been started (see below).
$ rake geoblacklight:internal:seed\n

Lastly start the rails server. Open another Terminal window, navigate to the place where your app is located, and run:

$ rake geoblacklight:server_only\n

Once the server is running, you can open a web browser and visit the URL it prompts, usually http://localhost:8983/solr/#/blacklight-core to see the admin interface of your test instance of Solr. As before, remember that ^C (ctrl + c) stops the server.

You may also want to use an external Solr instance, especially in production. You can read more about that here.

"},{"location":"docs/developers/#unit-testing","title":"Unit Testing","text":""},{"location":"docs/developers/#running-all-the-tests","title":"Running all the tests","text":"

As you develop and make changes, you may want to run tests on parts of the app to see if any warning occur. You can run the following to test the app

$ rake ci\n
Note that a test like this could take up to 5-6 minutes to complete, or longer. Warnings, deprecations, and other messages will be printed on your Terminal screen.

"},{"location":"docs/developers/#running-the-tests-separately","title":"Running the tests separately","text":"

$ rake geoblacklight:solr\n
Then, in another terminal window:
$ rspec spec/\n
Note: It is not necessary to run tests after every change you make. You can, for instance, change the name of a facet field, save your file, and then refresh your browser to see the change. However, if you add a new fixture metadata record, you will have to stop the servers and then restart them so the new file will be indexed.

"},{"location":"docs/developers/#browser-testing","title":"Browser Testing","text":"

Cross-browser testing provided by:

"},{"location":"docs/developers/#helpful-development-tools","title":"Helpful Development Tools","text":""},{"location":"docs/developers/#version-managers","title":"Version Managers","text":"

Using version management tools for compatible versions of Ruby (rvm, rbenv, asdf) and Node (nvm, asdf) can make development easier.

"},{"location":"docs/developers/#asdf","title":"asdf","text":"

Many developers like asdf because you can manage versions for Ruby and Node in a single utility. For developers who use asdf, it is helpful to add a .tool-versions file for each app.

Example:

ruby 2.7.5\nnodejs 17.4.0\njava openjdk-11.0.2\n

"},{"location":"docs/external_solr/","title":"Using an External Solr Instance","text":"

In some cases you may need to install Solr through a different method than described above, or link your GeoBlacklight installation to an existing Solr installation. You can learn more about installing Solr in the Apache documentation.

"},{"location":"docs/external_solr/#configure-the-solr-core","title":"Configure the Solr Core","text":"

Once you have Solr installed, you will need to create a new core and configure it for GeoBlacklight. How you create the core may depend on your installation method, but will likely be something like

$ bin/solr -c blacklight-core\n

Now rename/remove the core's conf directory and replace it with the solr/conf directory from GeoBlacklight: github.com/geoblacklight/geoblacklight/tree/main/solr/conf.

You can alter the core's configuration here as well, generally in the schema.xml file.

You can find the installation location of your Solr instance through the web admin interface: http://yourdomain.com:8983/solr/

"},{"location":"docs/external_solr/#set-solr_url-environment-variable","title":"Set SOLR_URL Environment Variable","text":"

GeoBlacklight will use the SOLR_URL environment variable (if present) to look for Solr. For example, assuming your core is named blacklight-core:

$ export SOLR_URL=http://yourdomain.com:8983/solr/blacklight-core\n

Now run the rails server and your external Solr will be used

$ rake engine_cart:server\n
"},{"location":"docs/framework-recommendations/","title":"Framework Recommendations","text":"

This page lists the recommended languages and frameworks to use with your installation of GeoBlacklight

"},{"location":"docs/framework-recommendations/#metadata","title":"Metadata","text":"
  • Aardvark (Recommended)
  • GBL 1.0 (Deprecated)
"},{"location":"docs/framework-recommendations/#ruby","title":"Ruby","text":"
  • 3.2 (Recommended)
  • 2.7 / Support Ends 31 Mar 2023
"},{"location":"docs/framework-recommendations/#ruby-on-rails","title":"Ruby on Rails","text":"
  • 7.0+ (Recommended)
  • 6.1
  • 6.0 / Support Ends June 1, 2023
"},{"location":"docs/framework-recommendations/#blacklight","title":"Blacklight","text":"
  • v7+ (Recommended)
  • v8 / Will be supported in GBL v5+
"},{"location":"docs/framework-recommendations/#bootstrap","title":"Bootstrap","text":"
  • v4 (Recommended)
  • v5 / Will be supported in GBL v5+
"},{"location":"docs/framework-recommendations/#viewcomponents","title":"ViewComponents","text":"
  • GBL v5 (Required)
  • GBL v4 (Recommended)
"},{"location":"docs/framework-recommendations/#javascript","title":"Javascript","text":"
  • GBL v5 - ES6 / Modern Javascript (Required)
  • GBL v4 / jQuery + ES5 (Recommended)
"},{"location":"docs/framework-recommendations/#map-library","title":"Map Library","text":"
  • Leaflet (Recommended)
  • TBD / GBL v5+
"},{"location":"docs/framework-recommendations/#apache-solr","title":"Apache Solr","text":"
  • v9+ (Recommended)
  • <8.11 versions are End Of Life (EOL)
"},{"location":"docs/framework-recommendations/#production-rdbms","title":"Production RDBMS","text":"
  • PostgreSQL (Recommended)
    • For potential adopters of GEOMG
"},{"location":"docs/framework-recommendations/#background-queue","title":"Background Queue","text":"
  • Not Required
  • Sidekiq + Redis (Recommended)
    • For potential adopters of GEOMG
    • Potential future GBL enhancement: Background Downloads
"},{"location":"docs/framework-recommendations/#geoserver","title":"GeoServer","text":"
  • Not Required
  • Used by many GeoBlacklight adopters: Harvard, Princeton, Stanford
"},{"location":"docs/geoblacklight_quick_start/","title":"GeoBlacklight Quick Start","text":"

This guide covers the quickest way to get up and running with GeoBlacklight, including:

  • How to install GeoBlacklight on your local computer.
  • How to create a new application.
  • How to add and index geospatial content.
"},{"location":"docs/geoblacklight_quick_start/#installation","title":"Installation","text":"

Bootstrap a new GeoBlacklight Ruby on Rails application using the template script:

DISABLE_SPRING=1 rails new app-name -m https://raw.githubusercontent.com/geoblacklight/geoblacklight/v4.4.0/template.rb\n
Then run the geoblacklight:server rake task to run the application:

$ cd app-name\n$ bundle exec rake geoblacklight:server\n
  • Visit your GeoBlacklight application at: http://localhost:3000
  • Visit the Solr admin panel at: http://localhost:8983/solr/#/blacklight-core
"},{"location":"docs/geoblacklight_quick_start/#index-example-data","title":"Index Example Data","text":"

With your Solr server and Rails server already running (via the geoblacklight:server rake task above), open a new terminal window and index the GeoBlacklight project's test fixtures via:

$ bundle exec rake \"geoblacklight:index:seed[:remote]\"\n
"},{"location":"docs/geopackages/","title":"GeoPackages","text":""},{"location":"docs/geopackages/#accessing-raster-and-vector-layers-in-geopackages","title":"Accessing Raster and Vector Layers in GeoPackages","text":""},{"location":"docs/geopackages/#ogc-geopackage","title":"OGC GeoPackage","text":"

GeoPackage is an encoding standard specified and maintained by the Open Geospatial Consortium, primarily directed towards the structure of SQLite geodatabases. As the GeoPackage standard provides standardization for vector features, tile matrix sets, and raster maps, it may be used as a container for either vector or raster spatial data sets.

To indicate the download is a \u201cGeoPackage\u201d, add this term to the dct_format_s OpenGeoMetadata schema field.

"},{"location":"docs/geopackages/#gis-web-services","title":"GIS Web Services","text":"

GeoPackages may be rendered using the layer viewer by providing the URL of a standard Web Map Service (WMS) or Web Feature Service (WFS) within the dct_references_s field of the schema.

"},{"location":"docs/geopackages/#geoserver-support","title":"GeoServer Support","text":"

For those who are currently using GeoServer in order to provide access to these data sets, the documentation outlines the process for uploading data sets and extracting vector or raster layers. Further, there also exists a plugin which permits one to export vector or raster data layers into GeoPackages (please see further documentation outlining the extended WMS/WFS output formats). Unfortunately (as stated above), exporting into GeoPackage in GeoServer requires that one install a plugin. Only reading is supported by GeoServer core.

"},{"location":"docs/geopackages/#arcmap-and-arcgis-pro-support","title":"ArcMap and ArcGIS Pro Support","text":"

For those using Esri's ArcMap, the process of connecting to a GeoPackage data source is identical to that of connecting to any SQLite database: https://desktop.arcgis.com/en/arcmap/latest/manage-data/databases/connect-sqlite.htm. This is the case also for users of ArcGIS Pro: https://pro.arcgis.com/en/pro-app/help/data/databases/work-with-sqlite-databases-in-arcgis-pro.htm

"},{"location":"docs/glossary/","title":"Glossary","text":"

Tip

See the Geo4LibCamp Glossary for more definitions at https://geo4libcamp.org/glossary

Aardvark Metadata schema for GeoBlacklight 4.x and beyond GBL 1.0 Metadata schema for GeoBlacklight 1.0-3.x GeoJSON Specific type of JSON for geographic features, such as OpenIndexMaps. GeoBlacklight metadata files are NOT GeoJSONs. Faraday A type of middleware for Ruby on Rails that provides a common interface, enabling different applications to send and receive data IIIF International Image Interoperability Framework. Used for displaying static images, like scanned maps. JSON JavaScript Object Notation - a flexible key:value pair file format used for GeoBlacklight metadata documents Mirador Open-Source IIIF viewer OGM OpenGeoMetadata OpenIndexMaps GeoJSON-based file specification for standardizing spatial index maps Solr The search index for GeoBlacklight Sprockets Ruby library for compiling and serving web assets"},{"location":"docs/hardware_recommendations/","title":"Hardware Recommendations","text":"

Running GeoBlacklight in production has modest hardware requirements. Your local IT system administrators and DevOps personnel will be critical in shaping your deployment environment.

"},{"location":"docs/hardware_recommendations/#example-production-environments","title":"Example Production Environments","text":""},{"location":"docs/hardware_recommendations/#b1gbtaa-geoportal","title":"B1G/BTAA Geoportal","text":"

Currently (9/2022) the B1G/BTAA Geoportal is running on AWS web services in production with these hardware specs:

"},{"location":"docs/hardware_recommendations/#solr-server","title":"Solr server","text":"
  • 4GB RAM
  • 2 CPU cores
  • Java Xmx configured for 3GB memory
  • OS base disk: small, at least 8GB for Linux but not much more needed
  • Solr data partition: 40GB (in practice, <10GB in use for BTAA)
"},{"location":"docs/hardware_recommendations/#webrails-server","title":"Web/Rails server","text":"
  • 8GB RAM
  • 2 CPU cores
  • OS base disk: at least 20GB for Linux
  • Data partition: 60GB for ample thumbnail caching space
  • Puma Rails server: Recommend 2 workers, 8 threads per worker in this configuration. More threads will necessitate more system memory
"},{"location":"docs/implementation_recommendations/","title":"Implementation Recommendations","text":"

Adhering to local IT best practices will help your GeoBlacklight install get up and running with optimal support from your IT staff. Below are some discussion points worth discussing locally as you move your GBL application to production:

"},{"location":"docs/implementation_recommendations/#analytics","title":"Analytics","text":"

You will want to collect web analytics for your application. Some institutions have policies in place to protect the anonymity of web users. Be sure to discuss how analytics will be implemented and monitored.

"},{"location":"docs/implementation_recommendations/#sitemap-and-robotstxt","title":"Sitemap and robots.txt","text":"

A sitemap and a robots.txt file will help you keep bots from crawling your application in ways that would cause significant performance issues.

For example, in the B1G Geoportal, we use the sitemap_generator rubygem and a cronjob to keep a sitemap up to date:

# config/sitemap.rb\nsolr = RSolr.connect url: Blacklight.connection_config[:url]\n\n# Select all the docs from Solr\nresponse = sol.get('select', params: {q: '*:*', fl: 'id', rows: 9999999})\n\n# Build a flat sorted array of all document slugs\nslugs = response['response']['docs'].map { |doc| doc['id'] }.sort\n\n# Set the host name for URL creation\nSitemapGenerator::Sitemap.default_host = 'https://geo.btaa.org'\nSitemapGenerator::Sitemap.create do\n  # Put links creation logic here.\n  #\n  # The root path '/' and sitemap index file are added automatically for you.\n  # Links are added to the Sitemap in the order they are specified.\n  #\n  # Usage: add(path, options={})\n  #        (default options are used if you don't specify)\n  #\n  # Defaults: :priority => 0.5, :changefreq => 'weekly',\n  #           :lastmod => Time.now, :host => default_host\n  #\n\n  slugs.each { |slug| add \"/catalog/#{slug}\" }\nend\n
# whenever gem\nevery :day, at: '12:30am', roles: [:app] do\n  rake 'sitemap:refresh'\nend\n

You'll likely want to disallow any code paths that hit Solr with a search query:

User-agent: *\nDisallow: /?q=\nDisallow: /*?q=*\nDisallow: /?f\nDisallow: /*?f*\nDisallow: /?_\nDisallow: /?bbox\nDisallow: /?page=\nDisallow: /bookmarks\nDisallow: /catalog.html?f\nDisallow: /catalog.html?_\nDisallow: /catalog.atom\nDisallow: /catalog.rss\nDisallow: /catalog/*/relations\nDisallow: /catalog/facet/*\nDisallow: /catalog/*/web_services\nDisallow: /catalog/email\nDisallow: /catalog/opensearch\nDisallow: /catalog/range_limit\nDisallow: /catalog/sms\nDisallow: /saved_searches\nDisallow: /search_history\nDisallow: /suggest\nDisallow: /users\nDisallow: /404\nDisallow: /422\nDisallow: /500\n

You might also want to disallow bots that aggressively index content as well:

User-agent: AhrefsBot\nDisallow: /\nUser-agent: SemrushBot\nDisallow: /\nUser-agent: PetalBot\nDisallow: /\nUser-agent: BLEXBot\nDisallow: /\nUser-agent: DotBot\nDisallow: /\nUser-agent: DataForSeoBot\nDisallow: /\n
"},{"location":"docs/implementation_recommendations/#uptime-and-performance-monitoring","title":"Uptime and Performance Monitoring","text":"

Your local IT staff should implement uptime and performance monitoring for your production GeoBlacklight application.

Systemd for process / uptime management and Nagios/Zabbix/CloudWatch for alerting are common tools. Third party options like AppSignal and UptimeRobot can help, too.

"},{"location":"docs/implementation_recommendations/#data-backups","title":"Data Backups","text":"

Discuss options for automatically backing up your application data from Solr and your application's relational database. Having a backup of your data will help you restore service after an unplanned interruption or corrupted index.

See the Apache Solr Reference Guide's Backup and Restore chapter for more details.

"},{"location":"docs/implementation_recommendations/#log-rolling","title":"Log Rolling","text":"

You will need to schedule your application logs to periodically rotate to maintain the size of these files. The logrotate utility can be very helpful here.

"},{"location":"docs/implementation_recommendations/#useful-cron-tasks","title":"Useful Cron Tasks","text":"

A few cronjobs will help keep your database lean. These examples use the popular whenever rubygem.

"},{"location":"docs/implementation_recommendations/#delete-old-searches","title":"Delete Old Searches","text":"
# Clean up recent anonymous search records\nevery :day, at: '2:30am', roles: [:app] do\n  rake 'blacklight:delete_old_searches[7]'\nend\n
"},{"location":"docs/implementation_recommendations/#delete-old-guest-users","title":"Delete Old Guest Users","text":"
# Cleans up anonymous user accounts created by search sessions\nevery :day, at: '1:30am', roles: [:app] do\n  rake 'devise_guests:delete_old_guest_users[2]'\nend\n
"},{"location":"docs/index_maps/","title":"Index Maps","text":""},{"location":"docs/index_maps/#index-maps-introduction","title":"Index Maps: Introduction","text":"

The 2020 Geo4LibCamp featured a workshop on index maps that provides useful information if you are new to index maps and want a basic primer. Many of the links below will lead you to relevant parts of this workshop.

For a conceptual introduction to index maps (i.e. what are index maps anyway?), see this explanation.

Here are examples of \"live\" index maps hosted within the GeoBlacklight instances of Cornell and Stanford.

"},{"location":"docs/index_maps/#making-index-maps","title":"Making Index Maps","text":"

Before making index maps for use in GeoBlacklight, it is important to be familiar with OpenIndexMaps, a specific index map standard that is used by the GeoBlacklight community. For an introduction to this standard, see here. For more detailed information on making index maps according to the OpenIndexMaps standard, see here.

Index maps that are made according to the OpenIndexMaps standard are encoded in the GeoJSON format. For more information about GeoJSON, see here. For a longer guide to GeoJSON with additional useful information, please see the Data Curation Network (DCN) primer on GeoJSON.

When working with GeoJSON, it is recommended to use QGIS. For a quick overview of QGIS features that are relevant to working with GeoJSON, see here.

The following tutorials cover how to make a polygon index map from an existing shapefile, how to make a point index map from a spreadsheet containing coordinates, how to create a grid index map from scratch, and how to create a polygon index map using virtual layer magic.

"},{"location":"docs/index_maps/#adding-customizing-and-displaying-index-maps","title":"Adding, Customizing, and Displaying Index Maps","text":"

#588 added index map discovery and preview to GeoBlacklight. Index map preview can be added to a layer by adding an accessible url to a GeoJSON file in a layer's dct_references_s section:

\"dct_references_s\": \"{\\\"https://openindexmaps.org\\\": \\\"https://gist.githubusercontent.com/mejackreed/4a44f1f7cc4fbb926068738e903a9e96/raw/fedfb0e599d647920f084627b7dca8f88a358757/stanford-fb897vt9938.geojson\\\"}\",\n

As noted above, index maps should be created using the OpenIndexMaps specification. In GeoBlacklight, the label property will be used for the tooltip that appears when the user hovers over a feature on the index map.

The index map preview can be customized by overriding the Handlebars template index_map_info.hbs and/or overriding the GeoBlacklight.Util.indexMapTemplate method.

#759 added selection styling for GeoJSON index map features and adjusted where style customizations are set. Styling for index map features can be customized in settings.yml. Any style that is set in the DEFAULT section will be applied to all feature states unless overwritten within each specific state. Style options follow the Leaflet Path Options, so any new style added should be from those available.

"},{"location":"docs/index_maps/#metadata-for-index-maps","title":"Metadata for Index Maps","text":"

Here are some recommendations to keep in mind when generating metadata for index maps:

  • The Geometry Type/Resource Type field (layer_geom_type_s in Metadata 1.0 or gbl_resourceType_sm in OpenGeoMetadata Aardvark) in the index map's metadata record should reflect the geometry type of the scanned map, aerial photo, LiDAR dataset etc. (i.e. the underlying data for which the index map serves as a contextual guide). It should not indicate the geom type of the index map itself.

  • The Subject field (dc_subject_sm or dct_subject_sm)) in the index map's metadata should include \"index map\" (in addition to other keywords relevant to the underlying data collection).

  • The Source field (dc_source_sm or dct_source_sm) in the metadata records of the underlying data should should reference the index map, since the index map can be seen as a \"source dataset\" that offers a guide to the broader collection.

"},{"location":"docs/index_maps/#committing-geoblacklight-index-maps-to-the-openindexmaps-github-repository","title":"Committing GeoBlacklight Index Maps to the OpenIndexMaps Github repository","text":"

Once you have generated your index map and its associated metadata, the index map must go \"live\" on the web. There are different ways to pursue the task of making an index map go \"live\", but the recommended approach is to commit the map/GeoJSON to OpenIndexMaps' Github Repository, which facilitates the discovery and sharing of index maps across institutions.

Once your map has been committed to your OpenIndexMaps repository, you will want to take the url for the map's blob (\"blob\" stands for binary large object, which is an object that contains the contents of your file), and add this information back to your GeoBlacklight metadata record (in particular, you'll want to add this information to the dct_references_s section).

To get the blob url, click the \"Raw\" link on your map's Github page, and copy the url of the page to which you are taken upon clicking this link.

The dct_references_s section of the index map's GeoBlacklight metadata contains relevant external links, and are organized as a serialized JSON array of key/value pairs (for more information on this section in the GeoBlacklight metadata schema, see here). In this case, the blob url which you copied (above) will be the value associated with the OpenIndexMaps url (which is the key).

The following site, from the GeoBlacklight team at NYU, provides a script that adds references to existing GeoBlacklight metadata records. This script can be adapted to add the OpenIndexMaps/Blob-url key-value pair into the metadata's dct_references_s section.

"},{"location":"docs/item_images/","title":"Add thumbnail images","text":"

The GeoBlacklight Sidecar Images plugin adds support for harvesting remote images from geographic web services.

"},{"location":"docs/item_images/#requirements","title":"Requirements","text":"

GBL Sidecar Images requires:

  • Ruby on Rails 5.2
  • ImageMagick

A background job processor like Sidekiq is optional, but highly recommended.

"},{"location":"docs/item_images/#example-screenshot","title":"Example Screenshot","text":""},{"location":"docs/item_images/#installation-and-use","title":"Installation and Use","text":"

See the plugin project repo for full installation and use documentation.

"},{"location":"docs/json-geojson/","title":"JSONs and GeoJSONs","text":"

Summary

  • GeoBlacklight metadata files are JSONs
  • OpenIndexMaps are GeoJSONs
"},{"location":"docs/json-geojson/#json","title":"JSON","text":"

JSON is a general-purpose data format.

JSON (JavaScript Object Notation) is a lightweight data-interchange format that is easy for machines to parse and generate. JSON is built on two structures:

  • A collection of name/value pairs (often realized as an object, record, structure, dictionary, hash table, keyed list, or associative arrays)
  • An ordered list of values (often realized as an array, vector, list, or sequence)

JSON is used to represent a wide variety of data structures, including GeoBlacklight metadata files. These files contain a mix of text, numbers, booleans, and arrays, organizing the metadata in a structured way for the Solr index. Although the metadata files contain geospatial coordinates, they are not in the GeoJSONs format.

Example

[\n  {\n    \"gbl_mdVersion_s\": \"Aardvark\",\n    \"dct_title_s\": \"Sample Record\",\n    \"gbl_resourceClass_sm\": [\n      \"Other\"\n    ],\n    \"gbl_resourceType_sm\": [\n      \"Aerial photographs\"\n    ],\n    \"gbl_indexYear_im\": [\n      \"1900\"\n    ],\n    \"gbl_dateRange_drsim\": [\n      \"[1900 TO 1910]\"\n    ],\n    \"dct_accessRights_s\": \"Public\",\n    \"dct_format_s\": \"JPEG\",\n    \"id\": \"2b22c800-a9fe-4fe1-aee6-f8784f4e987f\",\n  }\n]\n
"},{"location":"docs/json-geojson/#geojson","title":"GeoJSON","text":"

GeoJSON is a specialized format for representing geographic information.

GeoJSON is a specific JSON format for encoding geographic data. It extends JSON by adding geographical features, geometries, and properties. GeoJSON supports the following geometry types:

  • Point
  • LineString
  • Polygon
  • MultiPoint
  • MultiLineString
  • MultiPolygon
  • GeometryCollection

Example

{\n  \"type\": \"Feature\",\n  \"geometry\": {\n    \"type\": \"Point\",\n    \"coordinates\": [-123.365556, 48.428611]\n  },\n  \"properties\": {\n    \"name\": \"Victoria, BC\",\n    \"population\": 85792\n  }\n}\n
"},{"location":"docs/leaflet/","title":"Customizing Leaflet","text":""},{"location":"docs/leaflet/#adding-leaflet-controls","title":"Adding Leaflet controls","text":"

GeoBlacklight supports adding customized Leaflet plugin controls to the maps. This can useful for adding a geocoding or fullscreen. This guide will walkthrough adding the Leaflet.fullscreen control plugin.

"},{"location":"docs/leaflet/#add-required-javascript-and-css","title":"Add required Javascript and CSS","text":"

To add a custom control, first make sure that you require the needed JavaScript and/or CSS styles in your GeoBlacklight application.

// In your applications's app/assets/javascripts/geoblacklight.js\n\n//= require geoblacklight/geoblacklight\n//= require geoblacklight/basemaps\n//= require geoblacklight/controls\n//= require geoblacklight/viewers\n//= require geoblacklight/modules\n//= require geoblacklight/downloaders\n//= require leaflet-iiif\n//= require esri-leaflet\n//= require readmore.min\n\n//= require Leaflet.fullscreen.js\n

You should do something similar for vendor css files and images. GeoBlacklight uses the Rails asset pipeline for asset management. Vendor maintained files should usually be added under ./vendor/assets.

// In your applications's app/assets/stylesheets/geoblacklight.css.scss\n/*\n*= require geoblacklight/application\n*= require leaflet.fullscreen\n*/\n
"},{"location":"docs/leaflet/#configure-your-settings","title":"Configure your settings","text":"

Next, you need to configure your settings to tell the viewers to load your control. Your application's lib/generators/geoblacklight/templates/settings.yml should look something like this:

...\nOPACITY_CONTROL: &opacity_control\n  CONTROLS:\n    - 'Opacity'\n\nLEAFLET:\n  MAP:\n  LAYERS:\n  VIEWERS:\n    WMS:\n      <<: *opacity_control\n    TILEDMAPLAYER:\n      <<: *opacity_control\n    FEATURELAYER:\n      <<: *opacity_control\n    DYNAMICMAPLAYER:\n      <<: *opacity_control\n    IMAGEMAPLAYER:\n      <<: *opacity_control\n...\n

Let's say you want to add the fullscreen control for just your WMS viewer. You will need to update your WMS viewer controls to add it like so:

...\n  VIEWERS:\n      WMS:\n        CONTROLS:\n          - 'Opacity'\n          - 'Fullscreen'\n...\n
"},{"location":"docs/leaflet/#initialize-your-plugin","title":"Initialize your plugin","text":"

Finally you need to initialize your controls like this. You can initialize the plugin with additional options.

// In your applications's app/assets/javascripts/geoblacklight/geoblacklight.js\n...\n//= require Leaflet.fullscreen.js\n\nGeoBlacklight.Controls.Fullscreen = function() {\n  this.map.addControl(new L.Control.Fullscreen({\n    position: 'topright'\n  }));\n};\n

You should now have a working fullscreen button in your application!

"},{"location":"docs/leaflet/#adding-a-search-control","title":"Adding a Search Control","text":"

Customizing Leaflet has certain limitations which can fortunately be overcome through the usage of plugins developed by third parties. Leaflet provides the following listing of plugins for the library: https://leafletjs.com/plugins.html#search--popups

"},{"location":"docs/leaflet/#downloading-leaflet-plugins","title":"Downloading Leaflet Plugins","text":"

Firstly, in order to integrate a plugin, the JavaScript source file(s) are downloaded into the vendor/assets/javascripts directory, where names are all in the lower case, with whitespace being replaced by dash characters (e. g. vendor/assets/javascripts/esri-leaflet.js)

"},{"location":"docs/leaflet/#downloading-javascript-source-files","title":"Downloading JavaScript Source Files","text":"

Using wget

$ wget -O vendor/assets/javascripts/leaflet-search.js https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.src.js\n
Using curl
$ curl -o vendor/assets/javascripts/leaflet-search.js https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.src.js\n
For Production Builds
$ wget -O vendor/assets/javascripts/leaflet-search.js https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.min.js\n
or
$ curl -o vendor/assets/javascripts/leaflet-search.js https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.min.js\n

"},{"location":"docs/leaflet/#downloading-css-files","title":"Downloading CSS Files","text":"

wget

$ wget -O vendor/assets/stylesheets/leaflet-search.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.src.css\n\n$ wget -O vendor/assets/stylesheets/leaflet-search.mobile.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.mobile.src.css\n
curl
$ curl -o vendor/assets/stylesheets/leaflet-search.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.src.css\n\n$ curl -o vendor/assets/stylesheets/leaflet-search.mobile.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.mobile.src.css\n
Production Builds
$ wget -O vendor/assets/stylesheets/leaflet-search.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.min.css\n\n$ wget -O vendor/assets/stylesheets/leaflet-search.mobile.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.mobile.min.css\n
or
$ curl -o vendor/assets/stylesheets/leaflet-search.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.min.css\n\n$ curl -o vendor/assets/stylesheets/leaflet-search.mobile.css https://raw.githubusercontent.com/stefanocudini/leaflet-search/master/dist/leaflet-search.mobile.min.css\n

"},{"location":"docs/leaflet/#downloading-image-files","title":"Downloading Image Files","text":"

$ wget -O app/assets/images/loader.gif https://github.com/stefanocudini/leaflet-search/raw/master/images/loader.gif\n$ wget -O app/assets/images/search-icon-mobile.png https://github.com/stefanocudini/leaflet-search/raw/master/images/search-icon-mobile.png\n$ wget -O app/assets/images/search-icon.png https://github.com/stefanocudini/leaflet-search/raw/master/images/search-icon.png\n
or
$ curl -o app/assets/images/loader.gif https://github.com/stefanocudini/leaflet-search/raw/master/images/loader.gif\n$ curl -o app/assets/images/search-icon-mobile.png https://github.com/stefanocudini/leaflet-search/raw/master/images/search-icon-mobile.png\n$ curl -o app/assets/images/search-icon.png https://github.com/stefanocudini/leaflet-search/raw/master/images/search-icon.png\n

"},{"location":"docs/leaflet/#integrating-plugins-into-leaflet","title":"Integrating Plugins into Leaflet","text":"

Integrating a plugin varies depending upon precisely what is being used, but the general approach seems to often follow a pattern such as the following:

var searchLayer = L.layerGroup().addTo(map);\n//... adding data in searchLayer ...\nmap.addControl( new L.Control.Search({layer: searchLayer}) );\n
...where the map Object invokes addControl using the search L.Control Object as an argument.

"},{"location":"docs/leaflet/#configuring-geoblacklight","title":"Configuring GeoBlacklight","text":"

When integrating this GeoBlacklight, the approach above could modified by extending the previous example:

...\n  VIEWERS:\n      WMS:\n        CONTROLS:\n          - 'Opacity'\n          - 'Fullscreen'\n          - 'Search'\n...\n
// In the application app/assets/javascripts/geoblacklight.js\n...\n//= require\n//= require leaflet-search\n\nGeoBlacklight.Controls.Search = function() {\n  this.map.addControl(new L.control.search({\n    url: 'http://nominatim.openstreetmap.org/search?format=json&q={s}',\n        jsonpParam: 'json_callback',\n        propertyName: 'display_name',\n        propertyLoc: ['lat','lon'],\n        marker: L.circleMarker([0,0], { radius: 30 }),\n        autoCollapse: true,\n        autoType: false,\n        minLength: 2\n  }));\n};\n

// In the application app/assets/stylesheets/geoblacklight.scss\n/*\n*= require geoblacklight/application\n*= require leaflet-label\n*= require leaflet-search\n*= require leaflet-search.mobile\n*/\n\n// SCSS overrides for the default styles properties\n.leaflet-container {\n  .leaflet-control-search {\n    margin-top: 3.2rem;\n\n    .search-button {\n      background-image: image-url('search-icon-mobile');\n\n      &:hover {\n        background-image: image-url('search-icon-mobile');\n      }\n    }\n  }\n}\n
After refreshing your web browser, the map viewer should now have a search control integrated:

"},{"location":"docs/leaflet/#switching-the-default-basemap","title":"Switching the default basemap","text":"

GeoBlacklight comes with a default open-source basemap, Carto's Positron, but it is possible to switch to one of the seven baselayers supported within the GeoBlacklight application. They are:

  • Dark Matter
  • Positron
  • Positron Lite
  • World Antique
  • World Eco
  • Flat Blue
  • Midnight Commander

In order to toggle between them, all you need to do is go to the catalog_controller.rb file in your application and replace the config.basemap_provider value. The valid values are in the comments above this line as a helpful reminder.

"},{"location":"docs/leaflet/#dynamic-basemap-switching","title":"Dynamic Basemap Switching","text":"

Warning

This kind of customization may potentially make your future GeoBlacklight upgrades more difficult. If you choose to implement this feature, you will need to be extra vigilant when GBL JavaScript files change in future releases.

Need a dynamic basemap switcher? You can customize GeoBlacklight to add support for Leaflet's basemap switching:

"},{"location":"docs/leaflet/#1-add-javascript-cookie-to-your-application","title":"1. Add JavaScript Cookie to your application","text":"

Use yarn to install js-cookie:

$ yarn add js-cookie\n

Add the node_modules directory to your asset path:

/config/initializers/assets.rb

Rails.application.config.assets.paths << Rails.root.join('node_modules')\n

Add js-cookie to your geoblacklight.js file:

/app/assets/javascript/geoblacklight.js

//= require handlebars.runtime\n//= require geoblacklight/geoblacklight\n//= require geoblacklight/basemaps\n//= require geoblacklight/controls\n//= require geoblacklight/viewers\n//= require geoblacklight/modules\n//= require geoblacklight/downloaders\n//= require leaflet-iiif\n//= require esri-leaflet\n\n// Local Customizations\n//= require js-cookie/dist/js.cookie.js\n//= require ./local/viewers/map\n
"},{"location":"docs/leaflet/#2-add-basemap-options","title":"2. Add Basemap options","text":"

Configure the additional basemap options in your geoblacklight.js file:

/app/assets/javascript/geoblacklight.js

...\n\n// Local Customizations\n//= require js-cookie/dist/js.cookie.js\n//= require ./local/viewers/map\n\n// LOCAL Namespace\nif (!window.LOCAL){ LOCAL={}; }\n\n// Basemap select - Text: Value\nLOCAL.baseLayerMap = {\n  \"Default (Esri)\": 'esri',\n  \"OpenStreetMaps\": 'openstreetmapStandard',\n  \"World Imagery (Esri)\": 'esri_world_imagery'\n}\n\n// Additional leaflet base layers\nGeoBlacklight.Basemaps.esri =  L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', {\n  attribution: false,\n  maxZoom: 18,\n  worldCopyJump: true,\n  detectRetina: true,\n  noWrap: false\n});\n\nGeoBlacklight.Basemaps.esri_world_imagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {\n  attribution: false,\n  maxZoom: 18,\n  worldCopyJump: true,\n  detectRetina: true,\n  noWrap: false\n})\n
"},{"location":"docs/leaflet/#3-modify-geoblacklights-mapjs-file","title":"3. Modify GeoBlacklight's map.js file","text":"

Copy and move GeoBlacklight's map.js file to your local application.

Copy from GeoBlacklight: app/assets/javascripts/geoblacklight/viewers/map.js

Move to your local application here: app/assets/javascripts/local/viewers/map.js

Add a call to this.addBasemapSwitcher(); in the load block.

//= require geoblacklight/viewers/viewer\n\nGeoBlacklight.Viewer.Map = GeoBlacklight.Viewer.extend({\n\n  options: {\n    /**\n    * Initial bounds of map\n    * @type {L.LatLngBounds}\n    */\n    bbox: [[-82, -144], [77, 161]],\n    opacity: 0.75\n  },\n\n  overlay: L.layerGroup(),\n\n  load: function() {\n    if (this.data.mapGeom) {\n      this.options.bbox = L.geoJSONToBounds(this.data.mapGeom);\n    }\n    this.map = L.map(this.element).fitBounds(this.options.bbox);\n    this.map.addLayer(this.selectBasemap());\n\n    // Add initial bbox to map element for easier testing\n    if (this.map.getBounds().isValid()) {\n      this.element.setAttribute('data-js-map-render-bbox', this.map.getBounds().toBBoxString());\n    }\n\n    this.map.addLayer(this.overlay);\n    if (this.data.map !== 'index') {\n      this.addBoundsOverlay(this.options.bbox);\n    }\n\n    // Local Customizations\n    this.addBasemapSwitcher();\n  },\n\n  ...\n

Now add the functions to switch basemaps and store the current basemap in JS Cookie:

...\n\n/**\n* Selects basemap if specified in 1) cookie, 2) data options, 3) if not return mapquest\n*/\nselectBasemap: function() {\n  console.log(\"Selecting basemap\");\n  console.log(\"Cookie: \" + Cookies.get('basemap'));\n\n  var _this = this;\n  if (Cookies.get('basemap')) {\n    return GeoBlacklight.Basemaps[LOCAL.baseLayerMap[Cookies.get('basemap')]];\n  } else if (_this.data.basemap) {\n    return GeoBlacklight.Basemaps[_this.data.basemap];\n  } else {\n    return _this.basemap.mapquest;\n  }\n},\n\naddBasemapSwitcher: function() {\n  // basemaps control\n  console.log('Control: Base Layer');\n  var baseLayers = {\n    \"Default (Esri)\": GeoBlacklight.Basemaps.esri,\n    \"OpenStreetMaps\": GeoBlacklight.Basemaps.openstreetmapStandard,\n    \"World Imagery (Esri)\": GeoBlacklight.Basemaps.esri_world_imagery\n  };\n\n  L.control.layers(baseLayers, null, { position: 'bottomleft' }).addTo(this.map);\n\n  // Event listener for layer switcher\n  this.map.on('baselayerchange', function (e) {\n    Cookies.set('basemap', e.name)\n  });\n}\n\n...\n
"},{"location":"docs/leaflet/#4-add-leaflets-css-file-to-the-asset-pipeline","title":"4. Add Leaflet's CSS file to the asset pipeline","text":"

Unfortunately, Rails' asset pipeline cannot find Leaflet's Layer Group icon/images without some additional help.

Download a copy of Leaflet and copy the leaflet.css file into your local project here:

app/assets/stylesheets/leaflet/leaflet.css.erb

Add an import statement to application.scss for this new file:

...\n\n// Customizations\n@import 'leaflet/leaflet';\n

We'll need to modify this CSS file slightly to reference the images we need in the application.

At the top of this file add these lines:

//= depend_on_asset 'layer.png'\n//= depend_on_asset 'layers-2x.png'\n

Farther down the file, we'll need to edit this block too:

/* layers control */\n\n.leaflet-control-layers {\n    box-shadow: 0 1px 5px rgba(0,0,0,0.4);\n    background: #fff;\n    border-radius: 5px;\n    }\n.leaflet-control-layers-toggle {\n    background-image: url(<%= asset_url 'layers.png' %>);\n    width: 36px;\n    height: 36px;\n    }\n.leaflet-retina .leaflet-control-layers-toggle {\n    background-image: url(<%= asset_url 'layers-2x.png' %>);\n    background-size: 26px 26px;\n    }\n

We need the background-image paths to use Rails' asset_url helper so these images are fingerprinted correctly.

Lastly, from your Leaflet download copy the layers.png and layers-2x.png files into your local application here:

  • app/assets/images/layers.png
  • app/assets/images/layers-2x.png
"},{"location":"docs/leaflet/#homepage-map-centroid-clusters","title":"Homepage Map Centroid Clusters","text":"

Warning

This kind of customization may potentially make your future GeoBlacklight upgrades more difficult. If you choose to implement this feature, you will need to be extra vigilant when GBL JavaScript files change in future releases.

Want your homepage map to display centroid clusters? You can customize GeoBlacklight to add support for that:

"},{"location":"docs/leaflet/#1-add-a-rake-task-to-generate-a-centroidsjson-file","title":"1. Add a rake task to generate a centroids.json file","text":"

Create a new rake file here: /lib/tasks/generate_centroids_json.rake

This rake task will write a centroids.json file to your application's public directory. Add these lines to the file:

require 'rsolr'\n\nnamespace :geoportal do\n  desc 'Generate homepage centroids for map clustering'\n  task generate_centroids_json: :environment do\n    response = Blacklight.default_index.connection.get 'select', params: { q: \"*:*\", rows: '1000000' }\n\n    docs = []\n    response[\"response\"][\"docs\"].each_with_index do |doc, index|\n      begin\n        if doc.key?('dcat_centroid') && !doc['dcat_centroid'].empty?\n          entry = {}\n          entry['l'] = doc['id']\n          entry['t'] = ActionController::Base.helpers.truncate(doc['dct_title_s'], length: 50)\n          lat,lng    = doc['dcat_centroid'].split(\",\")\n          lat = lat.to_f.round(4) # Truncate long values\n          lng = lng.to_f.round(4) # Truncate long values\n          entry['c'] = \"#{lat},#{lng}\"\n          docs << entry\n        end\n      rescue Exception => e\n        puts \"Caught #{e}\"\n        puts \"BBox or centroid no good - #{doc['id']}\"\n      end\n    end\n\n    centroids_file = \"#{Rails.root}/public/centroids.json\"\n    File.open(centroids_file, \"w\"){ |f| f.write(JSON.generate(docs)) }\n  end\nend\n

Run this rake task via this command: bundle exec rake geoportal:generate_centroids_json

"},{"location":"docs/leaflet/#2-install-javascript-dependencies","title":"2. Install JavaScript Dependencies","text":"
  • Oboe - Oboe.js reads json, giving you the objects as they are found without waiting for the stream to finish
  • PruneCluster - Fast and realtime marker clustering for Leaflet

Use yarn to add these two new dependencies to the project:

yarn add oboe yarn add @sintef/prune-cluster

Add the node_modules directory to your asset path:

/config/initializers/assets.rb

Rails.application.config.assets.paths << Rails.root.join('node_modules')\n
"},{"location":"docs/leaflet/#3-add-our-javascript-changes-for-the-homepage-map","title":"3. Add our JavaScript changes for the Homepage Map","text":"

We need to override the GeoBlacklight app/assets/javascripts/geoblacklight/modules/home.js file to add our customization.

To override a Rails Engine's javascript (GeoBlacklight), we need to update our asset pipeline calls to require specific files from the GeoBlacklight modules directory instead of globbing all of the file from /modules/.

Change your local geoblacklight.js file to look like this:

//= require handlebars.runtime\n//= require geoblacklight/geoblacklight\n//= require geoblacklight/basemaps\n//= require geoblacklight/controls\n//= require geoblacklight/viewers\n\n// Local Customization - Start\n//= require geoblacklight/modules/bookmarks\n//= require geoblacklight/modules/download\n//= require geoblacklight/modules/geosearch\n//= require geoblacklight/modules/help_text\n//= require ./geoportal/modules/home\n//= require geoblacklight/modules/item\n//= require geoblacklight/modules/layer_opacity\n//= require geoblacklight/modules/metadata_download_button\n//= require geoblacklight/modules/metadata\n//= require geoblacklight/modules/relations\n//= require geoblacklight/modules/results\n//= require geoblacklight/modules/svg_tooltips\n//= require geoblacklight/modules/util\n// Local Customization - End\n\n//= require geoblacklight/downloaders\n//= require leaflet-iiif\n//= require esri-leaflet\n

As included in the code snippet above, add a file named app/assets/javascripts/geoportal/modules/home.js to your application.

Inside that file write these lines:

Blacklight.onLoad(function() {\n  $('[data-map=\"home\"]').each(function(i, element) {\n    var geoblacklight = new GeoBlacklight.Viewer.Map(this);\n    var data = $(this).data();\n\n    geoblacklight.map.addControl(L.control.geosearch({\n      baseUrl: data.catalogPath,\n      dynamic: false,\n      searcher: function() {\n        window.location.href = this.getSearchUrl();\n      },\n      staticButton: '<a href=\"#\" class=\"btn btn-primary\">Search here</a>'\n    }));\n\n    // Local Customization - Start\n    var pruneCluster = new PruneClusterForLeaflet();\n\n    oboe('/centroids.json')\n      .node('*', function( doc ){\n          if(typeof doc.c != 'undefined'){\n            var latlng = doc.c.split(\",\")\n\n            var marker = new PruneCluster.Marker(latlng[0],latlng[1], {popup: \"<a href='/catalog/\" + doc.l + \"'>\" + doc.t + \"</a>\"});\n            pruneCluster.RegisterMarker(marker);\n          }\n        }\n      )\n      .done(function(){\n        geoblacklight.map.addLayer(pruneCluster)\n      });\n    // Local Customization - End\n  });\n});\n
"},{"location":"docs/leaflet/#4-add-our-stylesheet-changes-for-the-homepage-map","title":"4. Add our Stylesheet changes for the Homepage Map","text":"

All that is missing now are is the CSS changes to style our clusters. Update your application.scss file to include the missing stylesheet:

@import 'customizations';\n@import 'bootstrap';\n@import 'blacklight';\n@import 'geoblacklight';\n\n// Local Customization\n@import '@sintef/prune-cluster/dist/LeafletStyleSheet';\n

Reload your homepage and you should see something like this:

"},{"location":"docs/leaflet/#configure-leaflet-for-retina-displays","title":"Configure Leaflet for retina displays","text":"

GeoBlacklight allows implementers to configure the way in which basemaps and tile layers (WMS) are displayed on high pixel density 'retina' screens. When retina detection settings are enabled, Leaflet will request larger tiles to take advantage of the increased resolution.

"},{"location":"docs/leaflet/#tile-layers","title":"Tile layers","text":"

In your application's settings.yml, find DETECT_RETINA and set it to true or false.

...\nLEAFLET:\n  MAP:\n  LAYERS:\n    DETECT_RETINA: true\n...\n

When set to true, Leaflet will load 512 pixel tiles on retina displays.

"},{"location":"docs/leaflet/#basemaps","title":"Basemaps","text":"

To configure the stock CartoDB basemaps for higher resolution display you will have to override the GeoBlacklight.Basemaps javascript module. In your application, create a geoblacklight directory in app/assets/javascripts/ and then create a new file called basemaps.js in that directory.

Now copy the contents of the Geoblacklight basemaps.js file into your new file. On any basemaps that you want to enable retina, set detectRetina to true. Your file should look something like this:

// basemaps\n\nGeoBlacklight.Basemaps = {\n  darkMatter: L.tileLayer(\n    'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}{retina}.png', {\n      attribution: '&copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors, &copy; <a href=\"http://cartodb.com/attributions\">CartoDB</a>',\n      maxZoom: 18,\n      worldCopyJump: true,\n      retina: '@2x',\n      detectRetina: true\n    }\n  ),\n  positron: L.tileLayer(\n    'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{retina}.png', {\n      attribution: '&copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors, &copy; <a href=\"http://cartodb.com/attributions\">CartoDB</a>',\n      maxZoom: 18,\n      worldCopyJump: true,\n      retina: '@2x',\n      detectRetina: true\n    }\n  )\n};\n
"},{"location":"docs/metadata/","title":"Metadata Reference","text":"

GeoBlacklight uses a lightweight metadata schema designed for geospatial resource discovery. The schema enables keyword searches, faceted refinement, and spatial map searching.

Metadata schema features:

  • based on Dublin Core, with custom elements added for spatial values
  • designed for discovery - to help users find items
  • not designed for complete technical documentation, such as a GIS dataset's processing history
  • includes elements for external links, such as downloads, web services, or supplemental metadata
  • interoperable for the OpenGeoMetadata federated metadata sharing community

Note

Metadata for GeoBlacklight is documented on the OpenGeoMetadata website . Key pages include:

  • https://opengeometadata.org/ogm-aardvark - OGM Aardvark Schema for GeoBlacklight versions 4.x
  • https://opengeometadata.org/gbl-1.0 - GBL 1.0 Metadata Schema for GeoBlacklight versions 2.x - 3.x
  • https://opengeometadata.org/upgrade-metadata - Upgrading Metadata from GBL 1.0 to OGM Aardvark
"},{"location":"docs/metadata/#metadata-functionality-in-geoblacklight-4x","title":"Metadata functionality in GeoBlacklight 4.x","text":"

Hover over the column headers for sorting options.

LabelField URI Required - will cause app to error if missing Default facet Search results display Item show default display Relation widget Notes IDid yes no no no no Functions as the slug (end part) of the item's URL Access Rightsdct_accessRights_s yes no icon no no If set to \"Restricted\", a closed padlock icon appears next to the title. Downloads and web service previews will be hidden unless a user logs in using an authentication process. If set to \"Public\", an open padlock icon appears next to the title. Downloads and web services are accessible. Formatdct_format_s conditional yes no yes no Required if a Download link is included. This value displays on the item show in the button under the download widget. Titledct_title_s no (see notes) no Clickable text yes no If a title is missing, GeoBlacklight will display the ID as the title. Alternative Titledct_alternative_sm no no no yes no - Bounding Boxdcat_bbox no no no no no Rectangular extent required for overlap ratio boosting as part of the overall relevance ranking algorithm. Centroiddcat_centroid no no no no no Can be leveraged for homepage centroid map. Not used in default GeoBlacklight. Creatordct_creator_sm no yes text on expanded view yes no - Date Issueddct_issued_s no no no yes no - Date Rangegbl_dateRange_drsim no no no yes no - Descriptiondct_description_sm no no text on expanded view yes no - Display Notegbl_displayNote_sm no no no yes no Text displays within a callout box File Sizegbl_fileSize_s no no no yes no - Geometrylocn_geometry no no no no no Required for the map search Georeferencedgbl_georeferenced_b no yes no yes no - Identifierdct_identifier_sm no no no no no - Index Yeargbl_indexYear_im no yes text on expanded view yes no Can be used with the Date Range plugin Is Part Ofdct_isPartOf_sm no no no no yes - Is Replaced Bydct_isReplacedBy_sm no no no no no Not used in GeoBlacklight Is Version Ofdct_isVersionOf_sm no no no no yes - Keyworddcat_keyword_sm no no no yes no - Languagedct_language_sm no no no yes no 3 digit code Licensedct_license_sm no no no yes no - Member Ofpcdm_memberOf_sm no no no no yes - Metadata Versiongbl_mdVersion_s no no no no no May be required by some harvesters in OpenGeoMetadata Providerschema_provider_s no yes icon yes no May control which records an authenticated user has access to. Publisherdct_publisher_sm no yes no yes no - Referencesdct_references_s no no no no no See https://opengeometadata.org/reference-uris/ for how different references function in GeoBlacklight Relationdct_relation_sm no no no no yes - Replacesdct_replaces_sm no no no no yes - Resource Classgbl_resourceClass_sm no yes icon yes no GBL will display an icon associated with the value. If multivalued, the first value will be used. Note: there is currently no icon for \"Web services\". Resource Typegbl_resourceType_sm no yes no yes no - Rightsdct_rights_sm no no no yes no - Rights Holderdct_rightsHolder_sm no no no yes no - Sourcedct_source_sm no no no no yes - Spatial Coveragedct_spatial_sm no yes no yes no - Subjectdct_subject_sm no yes no yes no - Suppressedgbl_suppressed_b no no no no no Hides items from search results. Items are only visible as child or related records in the Relations widgets. Temporal Coveragedct_temporal_sm no no no yes no - Themedcat_theme_sm no yes no yes no - WxS Identifiergbl_wxsIdentifier_s no no no no no Required if a OCG web service is included Modifiedgbl_mdModified_dt no no no no no -"},{"location":"docs/periodic_maintenance/","title":"Periodic Maintenance","text":"

Once your GBL application is running in production, you'll need to schedule some periodic maintenance sprints to keep things up to date and running happily.

"},{"location":"docs/periodic_maintenance/#test-suite-maintenance","title":"Test Suite Maintenance","text":"

The best way to future-proof your GBL install is to write and maintain a local test suite that provides coverage for basic application functionality.

As you upgrade versions of GeoBlacklight, Blacklight, Ruby on Rails, or Ruby, this test suite is your insurance that the application is running as expected after these upgrades are implemented. If upon upgrading GeoBlacklight or another core component, you test suite begins to fail, you'll know you have some development work to complete to successfully finish the upgrade.

"},{"location":"docs/periodic_maintenance/#geoblacklight-releases","title":"GeoBlacklight Releases","text":"

GBL usually releases new versions of the software after the Winter and Summer community sprints. Schedule some time for your local development team to review the latest GBL release and upgrade notes on a biannual timeframe.

"},{"location":"docs/periodic_maintenance/#blacklight-releases","title":"Blacklight Releases","text":"

Upstream releases of Blacklight can have a significant impact on GeoBlacklight installations. The Blacklight community is very good at leaving deprecation warnings in blacklight releases to help the GeoBlacklight community and local adopters keep their code maintained.

"},{"location":"docs/periodic_maintenance/#ruby-and-ruby-on-rails-releases","title":"Ruby and Ruby on Rails Releases","text":"

Blacklight and GeoBlacklight will adjust their test matrices for new releases of Ruby and Ruby on Rails to ensure proper support.

Commonly, there is no need to rush to upgrade to a new Ruby or Ruby on Rails release, unless there is a significant security issue to resolve.

"},{"location":"docs/releases/","title":"Release Calendar","text":"

GeoBlacklight release and technology dependency matrix.

GBL v3 LTS GBL v4 \u2022 Current Major Release GBL v5 \u2022 Next Major Release Support Status Released 2020; Ends 2025 Released 2022; Ends 2026 ETA 2024/2025 Metadata GBL 1.0 Aardvark Aardvark Ruby v3+ v3+ v3.3+ Ruby on Rails v6-v7 v6-v7 v8+ Blacklight v7 v7 v8+ Bootstrap v4 v4 v5 ViewComponents Few Few Many JavaScript (JS) jQuery / ES5 jQuery / ES5 ES6 (Modern JavaScript) Published to NPM JS Map Library Leaflet Leaflet TBD Asset Management Sprockets Sprockets Vite Ruby / Import Maps + Bundling Apache Solr <= v8 v8-v9+ v9+ Production RDBMS N/A N/A Recommended PostgreSQL Background Queue N/A N/A Solid Queue GeoServer Optional Optional Optional"},{"location":"docs/rendering_html_from_description/","title":"Render HTML in Metadata Fields","text":"

Blacklight includes a helper_method argument for catalog_controller.rb field configuration. You can use that helpful technique to output whatever you need from the solr field value.

An example for adding line breaks and even HTML to a dc_description_s field would work like this:

1) Add a custom helper for presenting the data, using Rails' simple_format helper

# ApplicationHelper / application_helper.rb\n\ndef render_html_description(args)\n  simple_format(Array(args[:value]).flatten.join(' '))\nend\n

2) Point the show field at your new helper_method

# CatalogController / catalog_controller.rb\n\nconfig.add_show_field Settings.FIELDS.DESCRIPTION, label: 'Description', itemprop: 'description', helper_method: :render_html_description\n

3) Example description value with line breaks (\"\\n\\n\") and some HTML markup, too:

  \"dc_description_s\": \"This table shows all 911 police emergency response and officer-initiated calls for service in the City of Detroit since September 20, 2016. Emergency response calls are the result of people calling 911 to request police services. \\n\\n Officer-initiated calls include traffic stops, street investigations and other policing activities (such as observing crimes in progress) where police officers initiate the response. The table includes all calls taken, dispatch, travel, and total response times for those calls serviced by a police agency. The data also include the responding agency, unit, call type and category of each call. Should you have questions about this dataset, you may contact the Commanding Officer of the Detroit Police Department's Crime Intelligence Unit at 313-596-2250 or <a href= \"mailto:CrimeIntelligenceBureau@detroitmi.gov\\\">CrimeIntelligenceBureau@detroitmi.gov</a>. \",\n

4) Now the show page will render like this

"},{"location":"docs/settings-yml/","title":"settings.yml fields","text":""},{"location":"docs/settings-yml/#application_logo_url","title":"APPLICATION_LOGO_URL","text":"

URL for logo image to be used in generated Carto OneClick links.

"},{"location":"docs/settings-yml/#carto_oneclick_link","title":"CARTO_ONECLICK_LINK","text":"

Optional integration with Carto OneClick Service.

"},{"location":"docs/settings-yml/#arcgis_base_url","title":"ARCGIS_BASE_URL","text":"

Used to view layers directly in ArcGIS online. More information in the ArcGIS Online documentation.

"},{"location":"docs/settings-yml/#download_path","title":"DOWNLOAD_PATH","text":"

Local path used for temporary storage of generated download files.

"},{"location":"docs/settings-yml/#bbox_within_boost","title":"BBOX_WITHIN_BOOST","text":"

The SOLR Boost Query value for spatial search matches within a bounding box.

"},{"location":"docs/settings-yml/#overlap_ratio_boost","title":"OVERLAP_RATIO_BOOST","text":"

The SOLR Boost Functions value for overlap ratio.

"},{"location":"docs/settings-yml/#homepage_map_geom","title":"HOMEPAGE_MAP_GEOM","text":"

Leave null to default to entire world, or add a stringified GeoJSON object to scope initial render of the map on the homepage of the application.

"},{"location":"docs/settings-yml/#gbl_params","title":"GBL_PARAMS","text":"

Explicit list of whitelisted URL params that can be used within the application, enforced via Rails StrongParameters.

Note

If you are trying to use a new URL param within your app, you will need to register it here. You may see \"unpermitted parameters\" errors until you update this setting.

"},{"location":"docs/settings-yml/#fields","title":"FIELDS","text":"

All metadata fields are linked with their respective identifiers in the SOLR index in this hash. To learn more about the default GeoBlacklight metadata schema, Aardvark, view the OGM Aardvark specification.

"},{"location":"docs/settings-yml/#institution","title":"INSTITUTION","text":"

This setting should hold the name of your institution, and can be used to help determine access to restricted records. In some GeoBlacklight implementations, for example, when a restricted record's schema_provider_s field matches Settings.INSTITUTION, authenticated users will be granted full access.

"},{"location":"docs/settings-yml/#metadata_shown","title":"METADATA_SHOWN","text":"

Enables links for various metadata formats in the tool panel for a record if the corresponding URI key is present in that record's dct_references_s field.

"},{"location":"docs/settings-yml/#timeout_download","title":"TIMEOUT_DOWNLOAD","text":"

(For external Download) timeout and open_timeout parameters for Faraday.

"},{"location":"docs/settings-yml/#timeout_wms","title":"TIMEOUT_WMS","text":"

(For WMS inspection) timeout and open_timeout parameters for Faraday.

"},{"location":"docs/settings-yml/#use_geom_for_relations_icon","title":"USE_GEOM_FOR_RELATIONS_ICON","text":"

Use the Geometry Type value from a record to determine what icon to use for its data relations.

Warning

This setting is only applicable for GBL 1.0 metadata and is not compatible with OGM Aardvark.

"},{"location":"docs/settings-yml/#webservices_shown","title":"WEBSERVICES_SHOWN","text":"

A list of web services that will be available for a record, if that record has a corresponding URI key in its dct_references_s field.

For example, if a record's references include a wms entry, and WEBSERVICES_SHOWN includes wms (as it does by default), a preview map will appear in the tool panel showing the WMS layer.

Supported web services:

  - 'wms'\n  - 'tms'\n  - 'wfs'\n  - 'xyz'\n  - 'wmts'\n  - 'tilejson'\n  - 'iiif'\n  - 'feature_layer'\n  - 'tiled_map_layer'\n  - 'dynamic_map_layer'\n  - 'image_map_layer'\n
"},{"location":"docs/settings-yml/#display_notes_shown","title":"DISPLAY_NOTES_SHOWN","text":"

Configuration for special rendering of gbl_displayNote_sm field values. Default note types are danger, info, tip, and warning.

You can add your own display note configuration as well. Each entry must have the follow properties:

bootstrap_alert_class Name of Bootstrap alert class to use for the note's container icon Name of GeoBlacklight SVG icon to display with note note_prefix String that will be used at the beginning of a gbl_displayNote_sm entry to trigger this particular rendering.

For example, the \"info\" note is configured like this:

DISPLAY_NOTES_SHOWN\n  info:\n    bootstrap_alert_class: alert-info\n    icon: circle-info-solid\n    note_prefix: \"Info: \"\n

Info

Display Notes will appear in GeoBlacklight in a similar manner to this admonition box.

"},{"location":"docs/settings-yml/#relationships_shown","title":"RELATIONSHIPS_SHOWN","text":"

GeoBlacklight supports many different types of relations between records. Configuration for how these are displayed is stored here. Each relationship defined must have the following properties:

field The SOLR field that the query is performed against query_type The type of query sent to SOLR icon GeoBlacklight icon to use for matched records label Label from the locale string translations file inverse The inverse relationship to this one, used to generate bidirectional linkages

For example, the MEMBER_OF_ANCESTORS relationship would be defined like so (note that the MEMBER_OF_DESCENDANTS relationship would also need to be defined as it is referenced in the inverse property):

RELATIONSHIPS_SHOWN:\n  MEMBER_OF_ANCESTORS:\n    field: pcdm_memberOf_sm\n    icon: parent-item\n    inverse: :MEMBER_OF_DESCENDANTS\n    label: geoblacklight.relations.member_of_ancestors\n    query_type: ancestors\n
"},{"location":"docs/settings-yml/#wms_params","title":"WMS_PARAMS","text":"

These parameters are appended to all WMS endpoints that your records contain. If you always want to be requesting VERSION=1.3.0 services, for example, you would update that here.

Default values:

  :SERVICE: 'WMS'\n  :VERSION: '1.1.1'\n  :REQUEST: 'GetFeatureInfo'\n  :STYLES: ''\n  :SRS: 'EPSG:4326'\n  :EXCEPTIONS: 'application/json'\n  :INFO_FORMAT: 'text/html'\n
"},{"location":"docs/settings-yml/#leaflet","title":"LEAFLET","text":"

GeoBlacklight uses Leaflet to power its web map interfaces. This setting contains many default configuration values for how these maps appear and behave.

A few common customizations of GeoBlacklight involve updates to this setting. See the Customizing Leaflet page.

"},{"location":"docs/settings-yml/#help_text","title":"HELP_TEXT","text":"

Labels shown in the popover for various viewer protocols, to provide more context for users. The values here must reference entries in the locale translation string file.

"},{"location":"docs/settings-yml/#sidebar_static_map","title":"SIDEBAR_STATIC_MAP","text":"

Show a sidebar static map for items with the listed viewer protocols.

Default values:

  - 'iiif'\n  - 'iiif_manifest'\n
"},{"location":"docs/settings/","title":"Configure the Settings","text":"

A lot of configuration for your GeoBlacklight instance will be handled in the settings.yml file.

Keep in mind, GeoBlacklight has reasonable defaults for all settings, so you do not need to change anything in order to get up and running. That said, you will eventually need to change something. Below is an annotated list of all variables in the settings file.

If you are developing a custom application, look for config/settings.yml. If you are working on the core GeoBlacklight codebase, the file is lib/generators/geoblacklight/templates/settings.yml.

Note

Settings are implemented with the config gem, and are available as properties of the Settings object throughout the application.

"},{"location":"docs/solr_search_relevancy/","title":"Configuring Search Relevancy in Solr for GeoBlacklight","text":"

In GeoBlacklight, search relevancy determins how search results are ranked based on the importance of various metadata fields. You can adjust the relevancy by modifying the solrconfig.xml file, which allows you to control which fields have more influence on the search results. The configuration uses the edismax query parser, which provides advanced relevancy tuning options.

"},{"location":"docs/solr_search_relevancy/#basic-steps-to-configure-search-relevancy","title":"Basic steps to configure search relevancy","text":"
  1. Identify important fields: Determine which metadata fields are most relevant for your application's search results.
  2. Set boost values: Adjust the boost values in the qf and pf parameters to prioritize those fields.
  3. Test and iterate: After making changes, test the search results to ensure they meet expectations. You may need to tweak the boost values or add/remove fields based on the results.
"},{"location":"docs/solr_search_relevancy/#key-sections-of-solrconfigxml","title":"Key sections of solrconfig.xml","text":""},{"location":"docs/solr_search_relevancy/#query-parameters","title":"Query parameters","text":"
  • defType: specifies the query parser (edismax)
  • qf: The query fields with boosting values that control how much weight each field has in the search relevancy.
  • pf: phrase boosting fields, used to boost exact phrase matches within the results.
"},{"location":"docs/solr_search_relevancy/#relevancy-fields-and-boosting","title":"Relevancy fields and boosting","text":"
  • The qf and pf parameters list the fields to be searched and their associated boost values. The higher the boost value, the more weight that field has in determining the relevance of the search result.

Example:

    <str name=\"qf\">\n          text^1\n          dct_description_ti^2\n          dct_creator_tmi^3\n          dct_publisher_ti^3\n          dct_isPartOf_tmi^4\n          dct_subject_tmi^5\n          dct_spatial_tmi^5\n          dct_temporal_tmi^5\n          dct_title_ti^6\n          dct_accessRights_ti^7\n          dct_provider_ti^8\n          dct_identifier_ti^10\n    </str>\n
  • dct_description_ti^2: The Description field has a boost value of 2
  • dct_creator_tmi: The Creator field has a boost value of 3, making it more relevant than the Description field
  • The most relevant field is dct_identifier_ti
  • Higher boost values indicate greater importance in the search results.
"},{"location":"docs/solr_search_relevancy/#sorting-results","title":"Sorting results","text":"
  • The sort parameter controls how the search results are orderd.

Example:

    <str name=\"sort\">score desc, dct_title_sort asc</str>\n
  • This sorts results first by score in descending order, then by dct_title_sort in ascending order.
"},{"location":"docs/tutorials/","title":"Tutorials","text":"

These tutorials were developed by members of the GeoBlacklight community and are designed to help users get started with GeoBlacklight.

Works in progress

We are currently in the process of updating these tutorials. Please contact us using one of the options listed on our Connect page with any questions or challenges you encounter.

  • Developing and customizing GeoBlacklight
  • A hands on introduction to GeoBlacklight
  • Using Packer to create a development virtual machine for GeoBlacklight
  • Using GeoCombine to harvest and index OpenGeoMetadata
"},{"location":"docs/upgrade_version_2_0/","title":"Upgrade to Version 2.0","text":""},{"location":"docs/upgrade_version_2_0/#upgrading-to-geoblacklight-20","title":"Upgrading to GeoBlacklight 2.0","text":"

While we suggest using the latest version of GeoBlacklight to take advantage of its modern features, sometimes you need to upgrade to an older release. GeoBlacklight 2.0 adds support for Blacklight 7.0, which itself includes several significant component upgrades:

  • Bootstrap 4
  • Rails 5.2 support
  • Webpacker support (see below)
  • JSON-API support
  • Solr 7.2+ support

The Bootstrap 3 to Bootstrap 4 migration will require existing GeoBlacklight installations to update any local view or layout customizations they have created. See the Blacklight guide on updating Bootstrap for additional assistance.

"},{"location":"docs/upgrade_version_2_0/#blacklight-7-upgrades","title":"Blacklight 7 upgrades","text":""},{"location":"docs/upgrade_version_2_0/#update-user-model","title":"Update User Model","text":"

With the release of Blacklight 7, the Blacklight::Utils Module has been deprecated. User Models must have the following removed:

class User < ApplicationRecord\n  ## Please remove or comment this code:\n  ##\n  # if Blacklight::Utils.needs_attr_accessible?\n  #   attr_accessible :email, :password, :password_confirmation\n  # end\n\n  # Connects this user object to Blacklights Bookmarks.\n  include Blacklight::User\n  # Include default devise modules. Others available are:\n  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable\n  devise :database_authenticatable, :registerable,\n         :recoverable, :rememberable, :validatable\n\n  # Method added by Blacklight; Blacklight uses #to_s on your\n  # user class to get a user-displayable login/identifier for\n  # the account.\n  def to_s\n    email\n  end\nend\n
"},{"location":"docs/upgrade_version_2_0/#update-catalogcontroller","title":"Update CatalogController","text":"

Release 2.0 provides the ability to request JSON representations of Solr Documents by using the path /catalog/:id/raw In other words, append /raw to the end of a catalog URL stem. Note that this is different from previous versions of GeoBlacklight and is a result of Blacklight 7 incorporating a JSON:API compliant specification. Appending .json to the end of a catalog URL stem will now return a JSON:API compliant record, which is nested and not Solr compatible. For more information, see the metadata documentation.

The JSON record return is enabled within the CatalogController by setting config.raw_endpoint.enabled to true:

  configure_blacklight do |config|\n\n    # Ensures that JSON representations of Solr Documents can be retrieved using\n    # the path /catalog/:id/raw\n    # Please see https://github.com/projectblacklight/blacklight/pull/2006/\n    config.raw_endpoint.enabled = true\n\n    ## Default parameters to send to solr for all search-like requests. See also SolrHelper#solr_search_params\n    ## @see https://lucene.apache.org/solr/guide/6_6/common-query-parameters.html\n    ## @see https://lucene.apache.org/solr/guide/6_6/the-dismax-query-parser.html#TheDisMaxQueryParser-Theq.altParameter\n    config.default_solr_params = {\n

"},{"location":"docs/upgrade_version_2_0/#webpacker","title":"Webpacker","text":"

Rails currently offers the ability for one to manage JavaScript source files and package dependencies using the Webpacker Gem. By default, this is available for usage in GeoBlacklight, but not enabled.

"},{"location":"docs/upgrade_version_2_0/#requirements","title":"Requirements","text":"

Webpacker requires that either Yarn or the Node Package Manager be installed in the environment where the GeoBlacklight implementation is deployed.

"},{"location":"docs/upgrade_version_2_0/#installing-webpacker","title":"Installing Webpacker","text":"

From within the root directory path of the GeoBlacklight application, please execute the following:

bundle exec rails generate geoblacklight:webpacker --force\n

This will create a number of directories and files, most notably: - package.json - app/javascript/packs/application.js

Running yarn install or npm install, followed by yarn upgrade/npm update would be best in order to install and update any JavaScript dependencies.

"},{"location":"docs/upgrade_version_2_0/#adding-packs","title":"Adding packs","text":"

In order to add JavaScript packs to a GeoBlacklight application, one should override the view template app/views/layouts/blacklight/base.html.erb (provided in https://github.com/projectblacklight/blacklight/blob/v7.0.1/app/views/layouts/blacklight/base.html.erb) with the following line:

    <%= javascript_include_tag \"application\" %>\n    <%= javascript_pack_tag 'application' %>\n    <%= csrf_meta_tags %>\n    <%= content_for(:head) %>\n  </head>\n

For any new JS file added to app/javascript/packs, this will need to be added with a different name. For example, app/javascript/packs/my_new_script.js would be added with:

    <%= javascript_include_tag \"application\" %>\n    <%= javascript_pack_tag 'application' %>\n    <%= javascript_pack_tag 'my_new_script' %>\n    <%= csrf_meta_tags %>\n    <%= content_for(:head) %>\n  </head>\n

"},{"location":"docs/upgrade_version_2_0/#running-the-webpack-server","title":"Running the Webpack server","text":"

Release 2.0 uses the Foreman Gem in order to run both the Rails server and Webpack development server in parallel. This is useful for development environments where the Webpack dev. server listens for source file changes, and automatically recompiles packs. A file (named Procfile) within the root path of the application should be created with the following content:

rails: bin/rails server --port=3000\nwebpack: bin/webpack-dev-server\n

This can then be executed using bundle exec foreman start.

For deployments to testing, staging, or production environments, it is perhaps preferred to simply precompile the Webpack builds. This can be achieved with the task bundle exec rails webpacker:compile

"},{"location":"docs/upgrade_version_2_0/#geoblacklight-updates","title":"GeoBlacklight updates","text":""},{"location":"docs/upgrade_version_2_0/#dropped-leaflet-rails-vendorized-a-rails-savvy-leafletjs-file","title":"Dropped leaflet-rails; Vendorized a rails-savvy leaflet.js file","text":"

To fix a Leaflet FeatureLayer asset path issue, we decided to remove leaflet-rails as a gem dependency. Instead of the gem, we're now using a slightly modified leaflet.js file in vendor/javascripts.

For existing GBL installations, you will need to remove the require leaflet-rails statement from lib/geoblacklight/engine.rb to avoid an error upon application restart.

"},{"location":"docs/upgrade_version_2_0/#added-spatial-search-bbox-overlapratio-relevancy-option","title":"Added Spatial Search BBox overlapRatio Relevancy Option","text":"

A new Settings constant was added to provide optional support for Solr's BBoxField overlapRatio relevancy boosting within spatial searches.

For existing GBL installations, you will need to add the Settings.OVERLAP_RATIO_BOOST setting to your settings.yml file.

    # The bf boost value for overlap ratio\n    OVERLAP_RATIO_BOOST: '2'\n

If this option has a value, the boost will be appended to the spatial search like so:

    if Settings.OVERLAP_RATIO_BOOST\n      solr_params[:overlap] =\n        \"{!field uf=* defType=lucene f=solr_bboxtype score=overlapRatio}Intersects(#{envelope_bounds})\"\n      solr_params[:bf] = \"$overlap^#{Settings.OVERLAP_RATIO_BOOST}\"\n    end\n
"},{"location":"docs/upgrade_version_2_0/#relevancy-is-best-tuned-locally","title":"Relevancy is Best Tuned Locally","text":"

Everyone's idea of relevancy is different. The default boost value here (\"2\") might not be the best for your collection or user needs. Please adjust this relevancy boost as necessary to ensure best results for your GBL install.

"},{"location":"docs/upgrade_version_2_0/#homepage","title":"Homepage","text":"

The _homepage_text.html.erb view partial has been updated to use a view component for rendering the featured facets feature. You should update any local customizations to this file to use the components.

"},{"location":"docs/upgrade_version_4_0/","title":"Upgrading to GeoBlacklight 4.0","text":"

There are several steps to complete this major release upgrade. Detailed notes follow for each of the following upgrade steps:

  1. Gemfile
  2. Apache Solr
  3. Data Migration
  4. Application Configuration
  5. Application Changes
"},{"location":"docs/upgrade_version_4_0/#1-gemfile","title":"1. Gemfile","text":"

Update your Gemfile to GBL v4:

  gem 'geoblacklight', '~> 4.0'\n
"},{"location":"docs/upgrade_version_4_0/#2-apache-solr","title":"2. Apache Solr","text":"

GeoBlacklight now requires Solr 8.3 or higher.

GBL's Solr configuration files are updated to reflect the Aardvark metadata element list and support new complex geometries features. See the default versions of schema.xml and solrconfig.xml and update your local files as necessary.

  • solr/config/schema.xml
  • solr/config/solrconfig.xml
"},{"location":"docs/upgrade_version_4_0/#3-data-migration","title":"3. Data Migration","text":"

Migrate your Solr documents from the GBL v1.0 metadata standard to OGM Aardvark. GBL community documentation and migration tools are listed below:

  • OGM's Guide for Upgrading Metadata
  • Full GBL 1.0 to OGM Aardvark Crosswalk
  • Tools and Techniques for Upgrading
  • gbl2aardvark: convert GeoBlacklight 1.0 json files to Aardvark
"},{"location":"docs/upgrade_version_4_0/#4-application-configuration","title":"4. Application Configuration","text":"

Review the configuration files for your GBL instance. You will need to update your settings.yml file and catalog_controller.rb file to use the new Aardvark field mappings. See the default versions of these files in GeoBlacklight v4 and alter your files as necessary:

You will also need to search your local application code for any old Settings.FIELDS.(X) mappings and update them as necessary.

"},{"location":"docs/upgrade_version_4_0/#settings","title":"Settings","text":"

config/settings.yml

Many GBLv4 configuration changes take place in the settings.yml file.

List of GBLv4 settings.yml changes:

  • Solr field mappings: Settings.FIELDS
  • GeoBlacklight Params: Settings.GBL_PARAMS
  • Relationships to display: Settings.RELATIONSHIPS_SHOWN
  • Parent/Child SVG Icon titles
"},{"location":"docs/upgrade_version_4_0/#solr-field-mappings-settingsfields","title":"Solr field mappings: Settings.FIELDS","text":"

With the adoption of the OGM Aardvark metadata schema, we need to update all the Settings.FIELDS values for Aardvark. Here are the default GBLv4 values. If you have additional local customizations here, you'll need carry those over, too.

# Solr field mappings\nFIELDS:\n  :ACCESS_RIGHTS: 'dct_accessRights_s'\n  :ALTERNATIVE_TITLE: 'dct_alternative_sm'\n  :CENTROID: 'dcat_centroid'\n  :CREATOR: 'dct_creator_sm'\n  :DATE_ISSUED: 'dct_issued_s'\n  :DATE_RANGE: 'gbl_dateRange_drsim'\n  :DESCRIPTION: 'dct_description_sm'\n  :FORMAT: 'dct_format_s'\n  :FILE_SIZE: 'gbl_fileSize_s'\n  :GEOREFERENCED: 'gbl_georeferenced_b'\n  :ID: 'id'\n  :IDENTIFIER: 'dct_identifier_sm'\n  :INDEX_YEAR: 'gbl_indexYear_im'\n  :IS_PART_OF: 'dct_isPartOf_sm'\n  :IS_REPLACED_BY: 'dct_isReplacedBy_sm'\n  :THEME: 'dcat_theme_sm'\n  :KEYWORD: 'dcat_keyword_sm'\n  :LANGUAGE: 'dct_language_sm'\n  :LAYER_MODIFIED: 'gbl_mdModified_dt'\n  :LICENSE: 'dct_license_sm'\n  :MEMBER_OF: 'pcdm_memberOf_sm'\n  :METADATA_VERSION: 'gbl_mdVersion_s'\n  :MODIFIED: 'gbl_mdModified_dt'\n  :OVERLAP_FIELD: 'solr_bboxtype'\n  :PUBLISHER: 'dct_publisher_sm'\n  :PROVIDER: 'schema_provider_s'\n  :REFERENCES: 'dct_references_s'\n  :RELATION: 'dct_relation_sm'\n  :REPLACES: 'dct_replaces_sm'\n  :RESOURCE_CLASS: 'gbl_resourceClass_sm'\n  :RESOURCE_TYPE: 'gbl_resourceType_sm'\n  :RIGHTS: 'dct_rights_sm'\n  :RIGHTS_HOLDER: 'dct_rightsHolder_sm'\n  :SOURCE: 'dct_source_sm'\n  :SPATIAL_COVERAGE: 'dct_spatial_sm'\n  :GEOMETRY: 'locn_geometry'\n  :SUBJECT: 'dct_subject_sm'\n  :SUPPRESSED: 'gbl_suppressed_b'\n  :TEMPORAL_COVERAGE: 'dct_temporal_sm'\n  :TITLE: 'dct_title_s'\n  :VERSION: 'dct_isVersionOf_sm'\n  :WXS_IDENTIFIER: 'gbl_wxsIdentifier_s'\n
"},{"location":"docs/upgrade_version_4_0/#geoblacklight-params","title":"GeoBlacklight Params","text":"

Settings.GBL_PARAMS

Add the GBL_PARAMS array to settings.yml to whitelist the GBL application params so they are appended to controller methods and search builder queries.

# Non-search-field GeoBlacklight application permitted params\nGBL_PARAMS:\n  - :bbox\n  - :email\n  - :file\n  - :format\n  - :id\n  - :logo\n  - :provider\n  - :type\n  - :BBOX\n  - :HEIGHT\n  - :LAYERS\n  - :QUERY_LAYERS\n  - :URL\n  - :WIDTH\n  - :X\n  - :Y\n
"},{"location":"docs/upgrade_version_4_0/#relationships-to-display","title":"Relationships to display","text":"

Settings.RELATIONSHIPS_SHOWN

The number of item/parent/collection relationships supported within GBLv4 has grown considerably. Add these default values to support the new relationships. You can also add additional relationship keys, fields, and query_types to support local customizations.

# Relationships to display\nRELATIONSHIPS_SHOWN:\n  MEMBER_OF:\n    field: pcdm_memberOf_sm\n    query_type: ancestors\n    icon: nil\n    label: geoblacklight.relations.member_of\n  PART_OF_ANCESTORS:\n    field: dct_isPartOf_sm\n    query_type: ancestors\n    icon: nil\n    label: geoblacklight.relations.part_of_ancestors\n  PART_OF_DESCENDANTS:\n    field: dct_isPartOf_sm\n    query_type: descendants\n    icon: child-item\n    label: geoblacklight.relations.part_of_descendants\n  RELATION:\n    field: dct_relation_sm\n    query_type: ancestors\n    icon: nil\n    label: geoblacklight.relations.relation\n  REPLACES:\n    field: dct_replaces_sm\n    query_type: ancestors\n    icon: nil\n    label: geoblacklight.relations.replaces\n  REPLACED_BY:\n    field: dct_isReplacedBy_sm\n    query_type: descendants\n    icon: nil\n    label: geoblacklight.relations.replaced_by\n  SOURCE_ANCESTORS:\n    field: dct_source_sm\n    query_type: ancestors\n    icon: parent-item\n    label: geoblacklight.relations.ancestor\n  SOURCE_DESCENDANTS:\n    field: dct_source_sm\n    query_type: descendants\n    icon: child-item\n    label: geoblacklight.relations.descendant\n  VERSION_OF:\n    field: dct_isVersionOf_sm\n    query_type: descendants\n    icon: nil\n    label: geoblacklight.relations.version_of\n
"},{"location":"docs/upgrade_version_4_0/#parentchild-svg-icon-titles","title":"Parent/Child SVG Icon titles","text":"

Replace these relationship icon file names.

SOURCE_ANCESTORS:\n  field: dct_source_sm\n  query_type: ancestors\n- icon: pagelines-brands\n+ icon: parent-item\n  label: geoblacklight.relations.ancestor\nSOURCE_DESCENDANTS:\n  field: dct_source_sm\n  query_type: descendants\n- icon: leaf\n+ icon: child-item\n  label: geoblacklight.relations.descendant\nVERSION_OF:\n  field: dct_isVersionOf_sm\n
"},{"location":"docs/upgrade_version_4_0/#viewer-controls","title":"Viewer Controls","text":"

Settings.LEAFLET.VIEWERS.*.CONTROLS

GBLv4 includes native support for the Leaflet.fullscreen plugin. Update your Leaflet configuration to include the Fullscreen viewer option.

# Settings for leaflet\nLEAFLET:\n  ...\n  VIEWERS:\n    DYNAMICMAPLAYER:\n      CONTROLS:\n        - 'Opacity'\n        - 'Fullscreen'\n    FEATURELAYER:\n      CONTROLS:\n        - 'Opacity'\n        - 'Fullscreen'\n    IIIF:\n      CONTROLS:\n        - 'Fullscreen'\n    IMAGEMAPLAYER:\n      CONTROLS:\n        - 'Opacity'\n        - 'Fullscreen'\n    INDEXMAP:\n      CONTROLS:\n        - 'Fullscreen'\n    TILEDMAPLAYER:\n      CONTROLS:\n        - 'Opacity'\n        - 'Fullscreen'\n    WMS:\n      CONTROLS:\n        - 'Opacity'\n        - 'Fullscreen'\n
"},{"location":"docs/upgrade_version_4_0/#catalogcontroller","title":"CatalogController","text":"

app/controllers/catalog_controller.rb

Besides the settings.yml configuration changes above, the catalog_controller.rb file holds a great deal of application configuration and it needs to be updated for the new Settings.FIELD values.

It may be helpful to review the diff of changes to catalog_controller.rb from v3.8.0 to v4.0.0

Here is a list of GBL v4 catalog_controller.rb changes:

"},{"location":"docs/upgrade_version_4_0/#default-solr-params","title":"Default Solr Params","text":"

config.default_document_solr_params

This uses the Settings.FIELDS.ID field now.

  ## Default parameters to send on single-document requests to Solr...\n  config.default_document_solr_params = {\n    :qt => 'document',\n    :q => \"{!raw f=#{Settings.FIELDS.ID} v=$id}\"\n  }\n
"},{"location":"docs/upgrade_version_4_0/#view-defaults","title":"View Defaults","text":"

config.view defaults

Adds the \"map\" split view for catalog#index

    # GeoBlacklight Defaults\n    # * Adds the \"map\" split view for catalog#index\n    config.view.split(partials: ['index'])\n    config.view.delete_field('list')\n
"},{"location":"docs/upgrade_version_4_0/#facet-fields","title":"Facet Fields","text":"

config.add_facet_field(s)

These are all now mapped to Aardvark fields. Note: 'icon_facet' partials are now replaced by the item_component: Geoblacklight::IconFacetItemComponent

    # FACETS\n\n    # DEFAULT FACETS\n    # to add additional facets, use the keys defined in the settings.yml file\n    config.add_facet_field Settings.FIELDS.INDEX_YEAR, :label => 'Year', :limit => 10\n    config.add_facet_field Settings.FIELDS.SPATIAL_COVERAGE, :label => 'Place', :limit => 8\n    config.add_facet_field Settings.FIELDS.ACCESS_RIGHTS, label: 'Access', limit: 8, item_component: Geoblacklight::IconFacetItemComponent\n    config.add_facet_field Settings.FIELDS.RESOURCE_CLASS, label: 'Resource Class', :limit => 8\n    config.add_facet_field Settings.FIELDS.RESOURCE_TYPE, label: 'Resource Type', :limit => 8\n    config.add_facet_field Settings.FIELDS.FORMAT, :label => 'Format', :limit => 8\n    config.add_facet_field Settings.FIELDS.SUBJECT, :label => 'Subject', :limit => 8\n    config.add_facet_field Settings.FIELDS.THEME, :label => 'Theme', :limit => 8\n    config.add_facet_field Settings.FIELDS.CREATOR, :label => 'Creator', :limit => 8\n    config.add_facet_field Settings.FIELDS.PUBLISHER, :label => 'Publisher', :limit => 8\n    config.add_facet_field Settings.FIELDS.PROVIDER, label: 'Provider', limit: 8, item_component: Geoblacklight::IconFacetItemComponent\n    config.add_facet_field Settings.FIELDS.GEOREFERENCED, :label => 'Georeferenced', :limit => 3\n
"},{"location":"docs/upgrade_version_4_0/#gbl-application-facets","title":"GBL Application Facets","text":"

Our map-based search feature is now run via a series of (Geo)Blacklight class extensions which require this configuration:

    # GEOBLACKLIGHT APPLICATION FACETS\n\n    # Map-Based \"Search Here\" Feature\n    # item_presenter       - Defines how the facet appears in the GBL UI\n    # filter_query_builder - Defines the query generated for Solr\n    # filter_class         - Defines how to add/remove facet from query\n    # label                - Defines the label used in contstraints container\n    config.add_facet_field Settings.FIELDS.GEOMETRY, item_presenter: Geoblacklight::BboxItemPresenter, filter_class: Geoblacklight::BboxFilterField, filter_query_builder: Geoblacklight::BboxFilterQuery, within_boost: Settings.BBOX_WITHIN_BOOST, overlap_boost: Settings.OVERLAP_RATIO_BOOST, overlap_field: Settings.FIELDS.OVERLAP_FIELD, label: 'Bounding Box'\n
"},{"location":"docs/upgrade_version_4_0/#item-relationship-facets","title":"Item Relationship Facets","text":"

To display item-to-item relationships, add this block below:

    # Item Relationship Facets\n    # * Not displayed to end user (show: false)\n    # * Must be present for relationship \"Browse all 4 records\" links to work\n    # * Label value becomes the search contraint filter name\n    config.add_facet_field Settings.FIELDS.MEMBER_OF, label: \"Member Of\", show: false\n    config.add_facet_field Settings.FIELDS.IS_PART_OF, label: \"Is Part Of\", show: false\n    config.add_facet_field Settings.FIELDS.RELATION, label: \"Related\", show: false\n    config.add_facet_field Settings.FIELDS.REPLACES, label: \"Replaces\", show: false\n    config.add_facet_field Settings.FIELDS.IS_REPLACED_BY, label: \"Is Replaced By\", show: false\n    config.add_facet_field Settings.FIELDS.SOURCE, label: \"Source\", show: false\n    config.add_facet_field Settings.FIELDS.VERSION, label: \"Is Version Of\", show: false\n
"},{"location":"docs/upgrade_version_4_0/#index-fields","title":"Index Fields","text":"

config.add_index_field(s)

The \"Index Fields\" are the values that appear on search results lists. These have been mapped to Aardvark fields.

    config.add_index_field Settings.FIELDS.INDEX_YEAR\n    config.add_index_field Settings.FIELDS.CREATOR\n    config.add_index_field Settings.FIELDS.DESCRIPTION, helper_method: :snippit\n    config.add_index_field Settings.FIELDS.PUBLISHER\n
"},{"location":"docs/upgrade_version_4_0/#show-fields","title":"Show Fields","text":"

config.add_show_field(s)

The \"Show Fields\" are the values that appear on an item detail page. These have been mapped to Aardvark fields, and many non-activated optional fields have been added to the default catalog_controller.rb file, too.

View \"Show Field\" configuration online

"},{"location":"docs/upgrade_version_4_0/#sort-fields","title":"Sort Fields","text":"

config.add_sort_field(s)

The GBLv4 default sort fields options have been expanded. Here is the new default value for sorting:

    config.add_sort_field 'score desc, dct_title_sort asc', :label => 'Relevance'\n    config.add_sort_field \"#{Settings.FIELDS.INDEX_YEAR} desc, dct_title_sort asc\", :label => 'Year (Newest first)'\n    config.add_sort_field \"#{Settings.FIELDS.INDEX_YEAR} asc, dct_title_sort asc\", :label => 'Year (Oldest first)'\n    config.add_sort_field 'dct_title_sort asc', :label => 'Title (A-Z)'\n    config.add_sort_field 'dct_title_sort desc', :label => 'Title (Z-A)'\n
"},{"location":"docs/upgrade_version_4_0/#web-services-changes","title":"Web Services Changes","text":"

Our web_services method is no longer a show tool partial. Migrating from GBLv3 to GBLv4, you will need to remove your config.add_show_tools_partial :web_services... line and add the new def web_services method:

View new method online

  # Custom tools for GeoBlacklight\n- config.add_show_tools_partial :web_services, if: proc { |_context, _config, options| options[:document] && (Settings.WEBSERVICES_SHOWN & options[:document].references.refs.map(&:type).map(&:to_s)).any? }\n
  def web_services\n    @response, @documents = action_documents\n\n    respond_to do |format|\n      format.html do\n        return render layout: false if request.xhr?\n        # Otherwise draw the full page\n      end\n    end\n  end\n
"},{"location":"docs/upgrade_version_4_0/#locales","title":"Locales","text":"

config/locales/geoblacklight.en.yml

We have added additional relations entries for GBLv4 config/locales/geoblacklight.en.yml.

If you have local overrides or customizations to this file, please include the new relations entries locally.

"},{"location":"docs/upgrade_version_4_0/#5-application-changes","title":"5. Application Changes","text":""},{"location":"docs/upgrade_version_4_0/#applicationcontroller","title":"ApplicationController","text":"

app/controllers/application_controller.rb

GBL installer now includes a before_action method to permit GBL application params. You'll need to add this code to your application_controller.rb file:

  before_action :allow_geoblacklight_params\n\n  def allow_geoblacklight_params\n    # Blacklight::Parameters will pass these to params.permit\n    blacklight_config.search_state_fields.append(Settings.GBL_PARAMS)\n  end\n
"},{"location":"docs/upgrade_version_4_0/#searchbuilder","title":"SearchBuilder","text":"

app/models/search_builder.rb

GBL's default search builder concerns have changed. We've added Geoblacklight::SuppressedRecordsSearchBehavior. You'll need this code in your application's search_builder.rb class:

# frozen_string_literal: true\nclass SearchBuilder < Blacklight::SearchBuilder\n  include Blacklight::Solr::SearchBuilderBehavior\n  include Geoblacklight::SuppressedRecordsSearchBehavior\n
"},{"location":"docs/upgrade_version_4_0/#stylesheets","title":"Stylesheets","text":"

app/assets/stylesheets/application.scss

GBL v4 no longer vendorizes the leaflet-label stylesheet. Check your local stylesheet files and remove any *= require leaflet-label or @import 'leaflet-label'; lines.

- /*\n- *= require leaflet-label\n- */\n
"},{"location":"docs/upgrade_version_4_0/#javascripts","title":"JavaScripts","text":"

app/assets/javascript/

GBL v4 adds a new Leaflet control: Leaflet.fullzoom. If you previously added this feature to your local GBL instance, you'll want to remove your custom implementation. This control can be added to your maps via the settings.yml file (see documentation above).

"},{"location":"docs/upgrade_version_4_0/#homepage","title":"Homepage","text":"

The _homepage_text.html.erb view partial has been updated to use a view component for rendering the featured facets feature. You should update any local customizations to this file to use the components.

    <div class='col-sm'>\n      <%= content_tag :h3, t('geoblacklight.home.category_heading') %>\n      <div class='row'>\n        <%= render(Geoblacklight::HomepageFeatureFacetComponent.new(icon: 'home', label: 'geoblacklight.home.institution', facet_field: Settings.FIELDS.PROVIDER, response: @response)) %>\n\n        <%= render(Geoblacklight::HomepageFeatureFacetComponent.new(icon: 'arrow-circle-down', label: 'geoblacklight.home.data_type', facet_field: Settings.FIELDS.RESOURCE_TYPE, response: @response)) %>\n      </div>\n      <div class='row'>\n        <%= render(Geoblacklight::HomepageFeatureFacetComponent.new(icon: 'globe', label: 'geoblacklight.home.placename', facet_field: Settings.FIELDS.SPATIAL_COVERAGE, response: @response)) %>\n\n        <%= render(Geoblacklight::HomepageFeatureFacetComponent.new(icon: 'tags', label: 'geoblacklight.home.subject', facet_field: Settings.FIELDS.SUBJECT, response: @response)) %>\n      </div>\n    </div>\n
"},{"location":"docs/user_authentication/","title":"User Authentication","text":"

GeoBlacklight facilitates the creation of user accounts and stores these in the database configured in config/database.yml. User accounts allow users to \"bookmark\" records for easy retrieval in the future. (Anonymous users can also bookmark records, though the the bookmarks will be lost at the end of a browser session.) In some implementations, user authentication is linked with institutional identities, which can drive access to certain privileged datsets (more on this below).

These capabilities are all inherited directly from Blacklight, which uses the devise and devise-guests gems to handle user accounts. For more about customizing this aspect of GeoBlacklight, see the relevant Blacklight documentation.

Cleaning up guests

By default, the devise-guests gem will automatically create user records every time an anonymous user visits GeoBlacklight, so it is advisable to schedule a regular cleanup task. See \"Useful Cron Tasks\" in Implementation Recommendations for more information.

"},{"location":"docs/user_authentication/#using-institutional-authentication-backends","title":"Using Institutional Authentication Backends","text":"

Typically, institutions will configure GeoBlacklight to use an existing user authentication backend instead of using the default configuration. This type of integration also allows for data access permissions to be linked to authenticated users.

Devise can be set up to use OmniAuth, which in turn can be extended to use various authentication providers, for example:

  • SAML: omniauth/omniauth-saml
  • LDAP: omniauth/omniauth-ldap
  • CAS: dlindahl/omniauth-cas
"},{"location":"showcase/","title":"GeoBlacklight Project Showcase","text":""},{"location":"showcase/#big-ten-academic-alliance-geoportal","title":"Big Ten Academic Alliance Geoportal","text":"

The Big Ten Academic Alliance Geoportal aggregates metadata from 12 different institutions providing a single place to find and use aerial imagery, geospatial data, and scanned maps from multiple GIS data clearinghouses and library catalogs.

https://geo.btaa.org

https://github.com/geobtaa/geoportal

"},{"location":"showcase/#colorado-geolibrary","title":"Colorado GeoLibrary","text":"

The Colorado GeoLibrary is a venture of the University of Colorado Boulder Libraries. The GeoLibrary provides geospatial data access to the University of Colorado and beyond. The purpose of the GeoLibrary is to collect and provide access to data from across the State of Colorado in service of our students and researchers, as well as the general public.

https://geo.colorado.edu/

"},{"location":"showcase/#cugir-cornell-university-geospatial-information-repository","title":"CUGIR - Cornell University Geospatial Information Repository","text":"

CUGIR provides free and open access to geospatial data for New York State, as well as worldwide geospatial data created by Cornell researchers.

https://cugir.library.cornell.edu/

"},{"location":"showcase/#dryad","title":"Dryad","text":"

The Dryad Digital Repository is a curated resource that makes research data discoverable, freely reusable, and citable. Dryad provides a general-purpose home for a wide diversity of data types, and it participates in the Data Curation Network.

https://datadryad.org/search

"},{"location":"showcase/#geodatawisconsin","title":"GeoData@Wisconsin","text":"

GeoData@Wisconsin is an online geoportal that provides discovery and access to Wisconsin geospatial data, imagery, and scanned maps. GeoData@WI represents the holdings of the University of Wisconsin Robinson Map Library archives, plus all known open data portals maintained by Wisconsin geospatial data producers.

https://geodata.wisc.edu

"},{"location":"showcase/#harvard-geospatial-library","title":"Harvard Geospatial Library","text":"

The Harvard Geospatial Library provides access to a wealth of geospatial data and maps from the Harvard Library as well as many other partner institutions. Users can conduct map based searches of HGL's catalog and find materials that are ready to use in a GIS, from modern census boundaries and data to images of early maps of the world.

https://hgl.harvard.edu/

"},{"location":"showcase/#norman-b-leventhal-map-center-collections","title":"Norman B. Leventhal Map Center - Collections","text":"

The Norman B. Leventhal Map Center at the Boston Public Library offers a customized discovery experience for its map collection. This unique application combines GeoBlacklight with the ability to georeference maps and create custom map sets for instruction.

https://collections.leventhalmap.org/search

"},{"location":"showcase/#nyu-spatial-data-repository","title":"NYU - Spatial Data Repository","text":"

The NYU Spatial Data Repository is a search and discovery platform for geospatial data created by NYU's Data Services. The application is the entry point for discovering and accessing NYU's spatial data collections.

https://geo.nyu.edu

"},{"location":"showcase/#princeton-university-digital-maps-and-geospatial-data","title":"Princeton University - Digital Maps and Geospatial Data","text":"

The Digital Maps and Geospatial Data tool allows users to search for datasets and scanned historical maps from within the Princeton University Library's own collections, as well as the collections of other institutions.

https://maps.princeton.edu

https://github.com/pulibrary/pulmap

"},{"location":"showcase/#stanford-university-earthworks","title":"Stanford University - EarthWorks","text":"

EarthWorks is Stanford University Libraries discovery tool for Geographic Information Systems (GIS) data. It combines data sources from many institutions allowing users to search through tens of thousands of geospatial datasets.

https://earthworks.stanford.edu

"},{"location":"showcase/#university-of-california-berkeley-geodata-portal","title":"University of California Berkeley \u2013 GeoData Portal","text":"

The UC Berkeley Library developed GeoData@UC Berkeley to help users find geospatial data and maps. Much of the content is freely available, but some datasets are restricted to UC Berkeley affiliated users.

https://geodata.lib.berkeley.edu/

"},{"location":"showcase/#university-of-massachusetts-amherst-portal-for-geospatial-data","title":"University of Massachusetts Amherst \u2013\u00a0Portal for Geospatial Data","text":"

UMAP GeoData is the UMass Amherst Libraries' discovery platform for finding geospatial content, including datasets, aerial photos, and scanned maps. All content is publicly available.

https://geodata.library.umass.edu/

"},{"location":"showcase/#university-of-texas-at-austin-texas-geodata-portal","title":"University of Texas at Austin - Texas GeoData Portal","text":"

The Texas GeoData portal is an online interface designed to make it easy for users to search and browse for geospatial data from the collections of the UT Libraries at the University of Texas at Austin and from the collections of other institutions.

https://geodata.lib.utexas.edu/

"},{"location":"showcase/#add-to-the-showcase","title":"Add to the Showcase","text":"

If your institution or organization has adopted GeoBlacklight and you would like to add your instance to the project page, see the instructions to contribute.

"},{"location":"showcase/submit/","title":"How to submit to the Showcase:","text":"

Info

To add your instance of GeoBlacklight to the Showcase, you will need the following:

  • title
  • thumbnail image
  • description
  • site link
  • code repository link, such as GitHub (optional)
"},{"location":"showcase/submit/#option-a-ask-a-geoblacklight-community-member-to-edit-this-site","title":"Option A: Ask a GeoBlacklight community member to edit this site","text":"
  1. Create a new issue on GitHub here.
  2. Include the title, description and GeoBlacklight site link in the body of the issue
  3. Upload an image to the issue that shows your homepage
  4. Include the label \"showcase\"
  5. Someone from the community will pick up the issue and add your instance to this site. It may be helpful to make a comment in Slack to make sure we are aware of the request.
"},{"location":"showcase/submit/#option-b-update-files-and-create-a-pull-request-in-github-diy-option","title":"Option B: Update files and create a pull request in GitHub (\"DIY\" option)","text":"
  1. Follow the instructions in the GitHub readme file on how to install mkdocs and clone this site to your desktop: https://github.com/geoblacklight/geoblacklight.github.io.
  2. Make a new branch.
  3. Navigate to this folder: docs/showcase/.
  4. Add your image to the showcase folder (jpg or png).
  5. Open docs/showcase/index.md.
  6. Create a new entry by copying and pasting the following template into the index.md file.

    Tips

    • There is a copy button at the top right of the code block.
    • Click on the text \"example\" to see an sample entry.
    templateexample
    -   ![](YOUR-IMAGE-FILE-NAME.JPG or .PNG)\n\n#### [TITLE-OF-YOUR-SITE](LINK-TO-YOUR-SITE)\n\nDESCRIPTION\n\n:octicons-link-external-16: https://LINK-TO-YOUR-SITE\n\n:simple-github: https://github.com/LINK-TO-YOUR-CODE-REPO\n
    -   ![](btaa-geoportal.png)\n\n#### [Big Ten Academic Alliance Geoportal](https://geo.btaa.org)\n\nThe Big Ten Academic Alliance Geoportal aggregates metadata from 14 institutions providing a single place to find and use aerial imagery, geospatial data, and scanned maps from multiple GIS data clearinghouses and library catalogs.\n\n:octicons-link-external-16: https://geo.btaa.org\n\n:simple-github: https://github.com/geobtaa/geoportal\n
  7. Replace all of the placeholder text and values with your own. (The text you need to replace in the template is in ALL CAPS so that you can differentiate it. Don't use ALL CAPS for your own text.)

  8. Alphabetize your entry by title so that it appears on the Showcase page in a predicatable location.
  9. Preview your changes locally with the mkdocs serve command (see the readme for instructions).
  10. Commit your changes to the GitHub branch.
  11. Publish your branch.
  12. Open a pull request to the Main branch.
  13. Someone from the community will review and/or merge your submission.
"},{"location":"blog/archive/2024/","title":"2024","text":""},{"location":"blog/archive/2023/","title":"2023","text":""},{"location":"blog/archive/2022/","title":"2022","text":""},{"location":"blog/archive/2021/","title":"2021","text":""},{"location":"blog/archive/2020/","title":"2020","text":""},{"location":"blog/archive/2019/","title":"2019","text":""},{"location":"blog/archive/2018/","title":"2018","text":""},{"location":"blog/archive/2017/","title":"2017","text":""},{"location":"blog/archive/2016/","title":"2016","text":""},{"location":"blog/archive/2015/","title":"2015","text":""},{"location":"blog/category/tutorials/","title":"tutorials","text":""},{"location":"blog/category/sprints/","title":"sprints","text":""},{"location":"blog/category/releases/","title":"releases","text":""},{"location":"blog/page/2/","title":"Blog","text":""},{"location":"blog/page/3/","title":"Blog","text":""},{"location":"blog/category/sprints/page/2/","title":"sprints","text":""}]} \ No newline at end of file diff --git a/showcase/Colorado.png b/showcase/Colorado.png new file mode 100644 index 00000000..f742bf1f Binary files /dev/null and b/showcase/Colorado.png differ diff --git a/showcase/HarvardGeospatialLibrary.png b/showcase/HarvardGeospatialLibrary.png new file mode 100644 index 00000000..ecd441b9 Binary files /dev/null and b/showcase/HarvardGeospatialLibrary.png differ diff --git a/showcase/bpl.jpg b/showcase/bpl.jpg new file mode 100644 index 00000000..2e1eb617 Binary files /dev/null and b/showcase/bpl.jpg differ diff --git a/showcase/btaa-geoportal.png b/showcase/btaa-geoportal.png new file mode 100644 index 00000000..c502b693 Binary files /dev/null and b/showcase/btaa-geoportal.png differ diff --git a/showcase/cugir.jpg b/showcase/cugir.jpg new file mode 100644 index 00000000..bbdbae2e Binary files /dev/null and b/showcase/cugir.jpg differ diff --git a/showcase/dryad.jpg b/showcase/dryad.jpg new file mode 100644 index 00000000..31618fb7 Binary files /dev/null and b/showcase/dryad.jpg differ diff --git a/showcase/earthworks.jpg b/showcase/earthworks.jpg new file mode 100644 index 00000000..e12cf684 Binary files /dev/null and b/showcase/earthworks.jpg differ diff --git a/showcase/index.html b/showcase/index.html new file mode 100644 index 00000000..c38a5c05 --- /dev/null +++ b/showcase/index.html @@ -0,0 +1,2152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + GeoBlacklight Project Showcase - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + +

GeoBlacklight Project Showcase

+
+ +
+

Add to the Showcase

+

If your institution or organization has adopted GeoBlacklight and you would like to add your instance to the project page, see the instructions to contribute.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/showcase/lc.jpg b/showcase/lc.jpg new file mode 100644 index 00000000..43b682ce Binary files /dev/null and b/showcase/lc.jpg differ diff --git a/showcase/nyu.jpg b/showcase/nyu.jpg new file mode 100644 index 00000000..15881e55 Binary files /dev/null and b/showcase/nyu.jpg differ diff --git a/showcase/princeton.jpg b/showcase/princeton.jpg new file mode 100644 index 00000000..58c16c9f Binary files /dev/null and b/showcase/princeton.jpg differ diff --git a/showcase/submit/index.html b/showcase/submit/index.html new file mode 100644 index 00000000..c9d7f76b --- /dev/null +++ b/showcase/submit/index.html @@ -0,0 +1,2131 @@ + + + + + + + + + + + + + + + + + + + + + How to submit to the Showcase: - GeoBlacklight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + +

How to submit to the Showcase:

+
+

Info

+

To add your instance of GeoBlacklight to the Showcase, you will need the following:

+
    +
  • title
  • +
  • thumbnail image
  • +
  • description
  • +
  • site link
  • +
  • code repository link, such as GitHub (optional)
  • +
+
+

Option A: Ask a GeoBlacklight community member to edit this site

+
    +
  1. Create a new issue on GitHub here.
  2. +
  3. Include the title, description and GeoBlacklight site link in the body of the issue
  4. +
  5. Upload an image to the issue that shows your homepage
  6. +
  7. Include the label "showcase"
  8. +
  9. Someone from the community will pick up the issue and add your instance to this site. It may be helpful to make a comment in Slack to make sure we are aware of the request.
  10. +
+

Option B: Update files and create a pull request in GitHub ("DIY" option)

+
    +
  1. Follow the instructions in the GitHub readme file on how to install mkdocs and clone this site to your desktop: https://github.com/geoblacklight/geoblacklight.github.io.
  2. +
  3. Make a new branch.
  4. +
  5. Navigate to this folder: docs/showcase/.
  6. +
  7. Add your image to the showcase folder (jpg or png).
  8. +
  9. Open docs/showcase/index.md.
  10. +
  11. +

    Create a new entry by copying and pasting the following template into the index.md file.

    +
    +

    Tips

    +
      +
    • There is a copy button at the top right of the code block.
    • +
    • Click on the text "example" to see an sample entry.
    • +
    +
    +
    +
    +
    +
    -   ![](YOUR-IMAGE-FILE-NAME.JPG or .PNG)
    +
    +#### [TITLE-OF-YOUR-SITE](LINK-TO-YOUR-SITE)
    +
    +DESCRIPTION
    +
    +:octicons-link-external-16: https://LINK-TO-YOUR-SITE
    +
    +:simple-github: https://github.com/LINK-TO-YOUR-CODE-REPO
    +
    +
    +
    +
    -   ![](btaa-geoportal.png)
    +
    +#### [Big Ten Academic Alliance Geoportal](https://geo.btaa.org)
    +
    +The Big Ten Academic Alliance Geoportal aggregates metadata from 14 institutions providing a single place to find and use aerial imagery, geospatial data, and scanned maps from multiple GIS data clearinghouses and library catalogs.
    +
    +:octicons-link-external-16: https://geo.btaa.org
    +
    +:simple-github: https://github.com/geobtaa/geoportal
    +
    +
    +
    +
    +
  12. +
  13. +

    Replace all of the placeholder text and values with your own. (The text you need to replace in the template is in ALL CAPS so that you can differentiate it. Don't use ALL CAPS for your own text.)

    +
  14. +
  15. Alphabetize your entry by title so that it appears on the Showcase page in a predicatable location.
  16. +
  17. Preview your changes locally with the mkdocs serve command (see the readme for instructions).
  18. +
  19. Commit your changes to the GitHub branch.
  20. +
  21. Publish your branch.
  22. +
  23. Open a pull request to the Main branch.
  24. +
  25. Someone from the community will review and/or merge your submission.
  26. +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/showcase/texas_geodata_portal.png b/showcase/texas_geodata_portal.png new file mode 100644 index 00000000..6140c59d Binary files /dev/null and b/showcase/texas_geodata_portal.png differ diff --git a/showcase/ucberkeley.png b/showcase/ucberkeley.png new file mode 100644 index 00000000..432aaece Binary files /dev/null and b/showcase/ucberkeley.png differ diff --git a/showcase/umass.png b/showcase/umass.png new file mode 100644 index 00000000..ae804272 Binary files /dev/null and b/showcase/umass.png differ diff --git a/showcase/uva.jpg b/showcase/uva.jpg new file mode 100644 index 00000000..367314e9 Binary files /dev/null and b/showcase/uva.jpg differ diff --git a/showcase/uwgeodata.png b/showcase/uwgeodata.png new file mode 100644 index 00000000..54b910d3 Binary files /dev/null and b/showcase/uwgeodata.png differ diff --git a/showcase/vecnet.jpg b/showcase/vecnet.jpg new file mode 100644 index 00000000..24c25cfe Binary files /dev/null and b/showcase/vecnet.jpg differ diff --git a/showcase/vt.png b/showcase/vt.png new file mode 100644 index 00000000..016a5735 Binary files /dev/null and b/showcase/vt.png differ diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000..3070e8c6 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,378 @@ + + + + https://geoblacklight.org/ + 2024-08-16 + daily + + + https://geoblacklight.org/about/ + 2024-08-16 + daily + + + https://geoblacklight.org/community/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2015/01/introducing-geoblacklight/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2015/02/using-geocombine-to-harvest-and-index-opengeometadata/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2015/02/a-hands-on-introduction-to-geoblacklight---geoblacklight-workshop/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2015/04/deploying-geoblacklight/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2015/11/open-in-cartodb-now-in-geoblacklight/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2016/01/using-packer-to-create-a-development-virtual-machine-for-geoblacklight/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2016/01/geoblacklight-at-nyu-a-blog-series-by-andrew-battista/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2016/02/vecnet-digital-library-redesign-and-geospatial-search-using-geoblacklight/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2016/08/geoblacklight-version-10-released/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2016/09/big-ten-academic-alliance-geoportal-launches/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2017/08/summer-code-concludes-and-geoblacklight-version-160-released/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2018/03/geoblacklight-winter-code-2018--new-release-18/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2018/08/2018-summer-code-for-geoblacklight/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2019/02/geoblacklight-20-is-here/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2020/09/geoblacklight-30-is-here/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2021/02/announcing-the-geoblacklight-community-winter-2021-sprint/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2021/09/new-releases-after-the-geoblacklight-community-summer-2021-sprint/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2022/02/announcing-the-geoblacklight-community-winter-2022-sprint/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2022/03/geoblacklight-community-sprint-recap-winter-2022/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2022/08/announcing-the-geoblacklight-community-summer-2022-sprint/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2022/09/the-wait-is-over---geoblacklight-40-is-here/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2023/03/winter-sprint-2023/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2023/06/geoblacklight-v41-released-along-with-new-documentation-pages/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2024/02/winter-2024-sprint-wrap-up---geoblacklight-v42/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/2024/05/geo4libcamp-2024--geoblacklight-workshop/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/adding_mirador_viewer/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/adding_svg_icons/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/catalog_controller/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/data_relations_widget/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/developers/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/external_solr/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/framework-recommendations/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/geoblacklight_quick_start/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/geopackages/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/glossary/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/hardware_recommendations/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/implementation_recommendations/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/index_maps/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/item_images/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/json-geojson/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/leaflet/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/metadata/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/periodic_maintenance/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/releases/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/rendering_html_from_description/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/settings-yml/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/settings/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/solr_search_relevancy/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/tutorials/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/upgrade_version_2_0/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/upgrade_version_4_0/ + 2024-08-16 + daily + + + https://geoblacklight.org/docs/user_authentication/ + 2024-08-16 + daily + + + https://geoblacklight.org/showcase/ + 2024-08-16 + daily + + + https://geoblacklight.org/showcase/submit/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/archive/2024/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/archive/2023/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/archive/2022/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/archive/2021/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/archive/2020/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/archive/2019/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/archive/2018/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/archive/2017/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/archive/2016/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/archive/2015/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/category/tutorials/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/category/sprints/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/category/releases/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/page/2/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/page/3/ + 2024-08-16 + daily + + + https://geoblacklight.org/blog/category/sprints/page/2/ + 2024-08-16 + daily + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 00000000..702a4937 Binary files /dev/null and b/sitemap.xml.gz differ diff --git a/stylesheets/extra.css b/stylesheets/extra.css new file mode 100644 index 00000000..b8cc9eee --- /dev/null +++ b/stylesheets/extra.css @@ -0,0 +1,156 @@ +/* +This is custom CSS for geoblacklight.org. The default text in the Material Theme is a little small, so many of these values increase the size slightly. +- created: @karenmajewicz, 2023-06-14 +- updated: + */ + + +/* TOP MENU */ + .md-tabs__link { + font-size: 1.7em; + } + +/* SIDE MENU */ +.md-nav--primary { + border-right: .1em solid #24574F; + } + +.md-nav__item--section > .md-nav__link[for] { + color: inherit; +} + +.md-nav__item{ + font-size: 1em; + } + +/* FONT SIZES */ +* { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + } + +.md-typeset h1{ + color: #3fad58; + font-weight: 500; + font-size: 2.1em; +} + +.md-typeset h2{ + color: #09483e; + font-size: 1.75em; +} + +.md-typeset h3{ + color: inherit; + font-size: 1.3em; +} + +.md-footer-nav { + display: none; +} + +/* BLOG TITLES */ +a.toclink{ + font-size: .85em; + font-weight: 500; + } + +/* CENTER IMAGES */ +.center { + display: block; + margin-left: auto; + margin-right: auto; + width: 25%; +} + +/* OTHER GRIDS */ + +/* HOMEPAGE */ +/* RESPONSIVE - 1 COLUMN ON SMALL SCREENS */ +@media screen and (max-width: 640px) { + #grid-col { grid-template-columns: 100%; } +} + +/* GRID CONTAINER */ +#grid-line { + display: grid; + grid-template-columns: auto auto ; + grid-gap: 20px; +} + +/* GRID CELL */ +div.cell { +/* border: .5px solid #e0e0e0; */ + padding: 5px; +} + +/* SPAN MULTIPLE COLUMNS */ +.spancol { + grid-column-start: 1; + grid-column-end: 2; +} + + +/* Built-in grid cards */ + +/* Style for the grid cell */ +.md-typeset .grid.cards li { + overflow: hidden; +} + +/* Style for images within each grid cell */ +.md-typeset .grid.cards li img { + width: 100%; + height: 175px; + object-fit: cover; + object-position: top; +} + +.md-typeset .grid.cards > ol > li, .md-typeset .grid.cards > ul > li, .md-typeset .grid > .card { + border: .05rem solid #3fad58; + border-radius: .2rem; + display: block; + margin: 0; + padding: .8rem; + transition:border .25s, box-shadow .25s +} + +/* wraps text for embedded long code lines */ +code { + white-space : pre-wrap !important; +} + +/* SPAN MULTIPLE COLUMNS */ +.spancol { + grid-column-start: 1; + grid-column-end: 2; +} + +th, td { + border: 1px solid var(--md-typeset-table-color); + border-spacing: 0; + border-bottom: none; + border-left: none; + border-top: none; +} + +.md-typeset__table { + line-height: 1.5; +} + +.md-typeset__table table:not([class]) { + font-size: .74rem; + border-right: none; + font-family: monospace; +} + +.md-typeset__table table:not([class]) td, +.md-typeset__table table:not([class]) th { + padding: 9px; +} + +/* light mode alternating table bg colors */ +.md-typeset__table tr:nth-child(2n) { + background-color: #f8f8f8; +} + +

Designated Code Mergers: see GitHub Teams