text:
_ = require("underscore");

//var ConsensusColors = require("./consensusColors.js");
var canvasSupport = require("./canvasSupport.js");
var render = require("./render/render.js");
var Letter = require("./model/letter.js");
var view = require("backbone-viewj");
var axis = require("./axis");
var eventListener = require("./eventListener.js");
var settings = require("./info/settings.js");

var jbone = require("jbone");

module.exports = view.extend({

  options: {
    xaxis: true,
    yaxis: true,
    height: 300,
    column_width: 34,
    debug: true,
    scale_height_enabled: true,
    scaled_max: true,
    zoom_buttons: true,
    colorscheme: 'default',
    data: undefined,
    start: 1,
    end: undefined,
    zoom: 0.4,
    colors: undefined,
    divider: false,
    show_probs: false,
    divider_step: 5,
    show_divider: false,
    border: false,
    settings: false,
    scroller: true,
    positionMarker: true
  },

  loadDefault: function(options){
    this.data = options.data;

    // never show the alignment coordinates by default as that would get
    // really confusing.
    this.display_ali_map = 0;

    this.alphabet = options.data.alphabet || 'dna';

    this.start = options.start;
    //this.end = options.end || this.data.heightArr.length;
    this.zoom = parseFloat(options.zoom) || 0.4;
    this.default_zoom = this.zoom;

    this.column_width = options.column_width;
    this.height = options.height;
    this.canvas_width = 5000;
    this.scale_height_enabled = options.scale_height_enabled;

    // this needs to be set to null here so that we can initialise it after
    // the render function has fired and the width determined.
    this.scrollme = null;

    this.previous_target = 0;
    // keeps track of which canvas elements have been drawn and which ones haven't.
    this.rendered = [];
    this.previous_zoom = 0;

    if(this.data.max_height == undefined){
      this.data.max_height = this.calcMaxHeight(this.data.heightArr);
    }

    // only show insert when we actually have the data
    if(!this.data.insert_probs || !this.data.delete_probs){
      this.options.show_probs = false;
    }

    if (options.scaled_max) {
      this.data.max_height = options.data.max_height_obs || this.data.max_height || 2;
    } else {
      this.data.max_height = options.data.max_height_theory || this.data.max_height || 2;
    }

    if(options.colors){
      this.changeColors(options.colors);
    }else{
      if (this.alphabet === 'aa') {
        this.aa_colors = require("./colors/aa.js");
        this.changeColors(this.aa_colors);
      }else{
        this.dna_colors = require("./colors/dna.js");
        this.changeColors(this.dna_colors);
      }
    }
  },
  initialize: function(options) {
    if (!canvasSupport()) {
      this.el.textContent = "Your browser doesn't support canvas.";
      return;
    }
    if(options.data == undefined){
      this.el.textContent = "No data added.";
    }

    // load default settings
    _.extend(this.options,options);
    var opt = this.options;
    this.loadDefault(opt);

    if(!this.options.show_probs){
      this.info_content_height = this.height;
    }else{
      // turn off the insert rows if the hmm used the observed or weighted processing flags.
      if (this.data.processing && /^observed|weighted/.test(this.data.processing)) {
        this.show_inserts = 0;
        this.info_content_height = this.height - 14;
      } else {
        this.show_inserts = 1;
        this.info_content_height = this.height - 44;
      }
    }
    this.$el = jbone(this.el);

    this.initDivs();

    if(this.options.settings){
      var form = settings(this,opt);
      this.$el.append(form);
    }

    eventListener(this.$el,this, this.logo_graphic);
    /*
       if (opt.columnInfo) {
       var columnInfo = require("./info/column_info.js");
       columnInfo(this);
       }
       */

  },
  initDivs: function(){
    var logo_graphic = mk("div");
    logo_graphic.className = "logo_graphic";
    this.logo_graphic = jbone(logo_graphic);

    var container = mk("div");
    container.className = "logo_container";
    container.style.height = this.height;
    this.container = jbone(container);

    this.container.append(logo_graphic);

    // add some internal divs for scrolling etc.
    this.$el.append(container);

    if(this.options.divider){
      var divider = mk("div");
      divider.className = "logo_divider";
      this.$el.append(divider);
    }

    this.dom_element = jbone(logo_graphic);
    this.called_on = this.$el;

    if(this.options.xaxis){
      axis.render_x_axis_label.call(this);
    }
    if(this.options.yaxis){
      axis.render_y_axis_label.call(this);
    }else{
      this.container[0].style.marginLeft = "0px";
    }

  },

  render: function(){
    render.call(this);
    return this;
  },

  changeColors: function(colors){
    this.colors = colors;
    var bUseColorObject = (colors != undefined && colors.type != undefined);
    if(bUseColorObject){
      this.colorscheme = "dynamic";
    }
    this.buildAlphabet();
  },

  buildAlphabet: function(){
    /*
       if (this.alphabet === 'aa') {
       var probs_arr = this.data.probs_arr;
       if (probs_arr) {
       var cc = new ConsensusColors();
       this.cmap = cc.color_map(probs_arr);
       }
       }
       */

    //build the letter canvases
    this.letters = {};
    var colors = this.colors;
    if(this.colorscheme == "dynamic"){
      var tColors = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split('');
      colors = {};
      tColors.forEach(function(e){
        colors[e] = "";
      });
    }
    for (var letter in colors) {
      if (colors.hasOwnProperty(letter)) {
        var loptions = {color: colors[letter]};
        this.letters[letter] = new Letter(letter, loptions);
      }
    }
  },

  toggleColorscheme: function (scheme) {
    // work out the current column we are on so we can return there
    var col_total = this.currentColumn();

    if (scheme) {
      if (scheme === 'default') {
        this.colorscheme = 'default';
      } else {
        this.colorscheme = 'consensus';
      }
    } else {
      if (this.colorscheme === 'default') {
        this.colorscheme = 'consensus';
      } else {
        this.colorscheme = 'default';
      }
    }

    // reset the rendered counter so that each section will re-render
    // with the new heights
    this.rendered = [];

    // re-flow and re-render the content
    this.scrollme.reflow();
    //scroll off by one to force a render of the canvas.
    this.scrollToColumn(col_total + 1);
    //scroll back to the location we started at.
    this.scrollToColumn(col_total);
  },

  toggleScale: function (scale) {
    // work out the current column we are on so we can return there
    var col_total = this.currentColumn();

    if (scale) {
      if (scale === 'obs') {
        this.data.max_height = this.data.max_height_obs;
      } else {
        this.data.max_height = this.data.max_height_theory;
      }
    } else {
      // toggle the max height
      if (this.data.max_height === this.data.max_height_obs) {
        this.data.max_height = this.data.max_height_theory;
      } else {
        this.data.max_height = this.data.max_height_obs;
      }
    }
    // reset the rendered counter so that each section will re-render
    // with the new heights
    this.rendered = [];
    //update the y-axis
    if(this.logoYAxis){
      this.logoYAxis.remove();
      //this.called_on.find('.logo_yaxis').remove();
    }
    axis.render_y_axis_label.call(this);

    // re-flow and re-render the content
    this.scrollme.reflow();
    //scroll off by one to force a render of the canvas.
    this.scrollToColumn(col_total + 1);
    //scroll back to the location we started at.
    this.scrollToColumn(col_total);
  },
  toggleAliMap: function (coords) {
    // work out the current column we are on so we can return there
    var col_total = this.currentColumn();

    if (coords) {
      if (coords === 'model') {
        this.display_ali_map = 0;
      } else {
        this.display_ali_map = 1;
      }
    } else {
      // toggle the max height
      if (this.display_ali_map === 1) {
        this.display_ali_map = 0;
      } else {
        this.display_ali_map = 1;
      }
    }
    axis.render_x_axis_label(this);

    // reset the rendered counter so that each section will re-render
    // with the new heights
    this.rendered = [];

    // re-flow and re-render the content
    this.scrollme.reflow();
    //scroll off by one to force a render of the canvas.
    this.scrollToColumn(col_total + 1);
    //scroll back to the location we started at.
    this.scrollToColumn(col_total);
  },

  currentColumn: function () {
    var before_left = this.scrollme.scroller.getValues().left,
    col_width = (this.column_width * this.zoom),
    col_count = before_left / col_width,
    half_visible_columns = (this.container.width() / col_width) / 2,
    col_total = Math.ceil(col_count + half_visible_columns);
    return col_total;
  },

  changeZoom: function (options) {
    var zoom_level = 0.3,
    expected_width = null;
    if (options.target) {
      zoom_level = options.target;
    } else if (options.distance) {
      zoom_level = (parseFloat(this.zoom) - parseFloat(options.distance)).toFixed(1);
      if (options.direction === '+') {
        zoom_level = (parseFloat(this.zoom) + parseFloat(options.distance)).toFixed(1);
      }
    }

    if (zoom_level > 1) {
      zoom_level = 1;
    } else if (zoom_level < 0.1) {
      zoom_level = 0.1;
    }

    // see if we need to zoom or not
    expected_width = (this.logo_graphic.width() * zoom_level) / this.zoom;
    if (expected_width > this.container.width()) {
      // if a center is not specified, then use the current center of the view
      if (!options.column) {
        //work out my current position
        var col_total = this.currentColumn();

        this.zoom = zoom_level;
        this.render({zoom: this.zoom});
        this.scrollme.reflow();

        //scroll to previous position
        this.scrollToColumn(col_total);
      } else { // center around the mouse click position.
        this.zoom = zoom_level;
        this.render({zoom: this.zoom});
        this.scrollme.reflow();

        var coords = this.coordinatesFromColumn(options.column);
        this.scrollme.scroller.scrollTo(coords - options.offset);
      }
    }
    return this.zoom;

  },

  columnFromCoordinates: function (x) {
    var column = Math.ceil(x / (this.column_width * this.zoom));
    return column;
  },

  coordinatesFromColumn: function (col) {
    var new_column = col - 1,
    x = (new_column  * (this.column_width * this.zoom)) + ((this.column_width * this.zoom) / 2);
    return x;
  },

  scrollToColumn: function (num, animate) {
    var half_view = (this.logo_container.width() / 2),
    new_left = this.coordinatesFromColumn(num);
    this.scrollme.scroller.scrollTo(new_left - half_view, 0, animate);
  },
  calcMaxHeight: function(columns){
    // loops over all columns and return the max height seen
    return columns.reduce(function(m,c){
      var col = 0;
      for(var k in c){
        col += c[k];
      }
      return col > m ? col : m;
    },0);
  }

});

var mk = function(name){
  return document.createElement(name);
}


text:
'use strict';

var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

var CommentsSchema = new Schema({
  author: String,
  content: {
		type: String,
		required: true
  },
  date: {
  	type: Date,
  	default: Date.now
  },
  email: {
  	type: String
  },
  ip: {
  	type: String
  },
  gravatar: {
  	type: String
  },
  approved: {
  	type: Boolean,
  	default: false,
  	required: true
  },
  likes: Number,
  meta: Object
});

module.exports = mongoose.model('Comments', CommentsSchema);

redpajama_set_name: RedPajamaGithub


text:
title: Nim vs Crystal - Part 3 - Crypto, DApps & P2P
summary: "Crystal and Nim go head-to-head to figure out the best modern, low-level programming language!  In part 3; Crypto, P2P & DApps are explored."
author: robin_percy
categories:
  - tutorials
layout: blog-post
image: '/assets/images/nim-crystal-header_blank.jpg'
---

![crystal vs nim](/assets/images/nim-crystal-header-img_NEW.jpg)

Welcome back to my series comparing the two sweethearts of the modern low-level programming world.  Just to quickly recap: in [article #1](/news/2019/11/18/nim-vs-crystal-part-1-performance-interoperability/) I noted my thoughts on the interoperability capabilities of the two languages, alongside briefly reviewing the performance metrics for each (albeit with relatively simple tests).  Whether simple or not, the tests ***did*** throw up some unexpected twists in the plot.  Crystal used *very-nearly* half of the memory amount executing the tests when compared to Nim, and also took *very nearly* half of the execution time in doing so.  **This seriously took me by surprise!**

In [article #2](/news/2019/11/21/nim-vs-crystal-part-2-threading-tooling/); I looked at the Concurrency primitives of each language, and explored both the in-built tooling, and external package ecosystems surrounding each language.  As I said in that article, one of the biggest factors I look at when considering adopting a new language; is its tooling ecosystem.  This includes, but is not limited to:  A comprehensive package manager, an intuitive testing suite, a good project scaffolder, and an in-built formatter/linter to ensure my code stays semantically correct – especially if I know I will be working in Open Source repos that others will contribute to.  But they're just the high-level tools that I look for...

From a low-level standpoint; I look for efficient use of technology in features such as in-memory storage, caching, garbage collection, and concurrency primitives that not just *markedly* improve our application performance, but that are also relatively simple, and intuitive to use.  I see *this* as particularly important as I have, in my past, seen some truly shocking examples of trying to handle multi-threading, from languages that I love \*cough* ***Ruby*** \*cough*. I also like to see a fully-featured standard library that takes influence from previous successful languages.  However, I digress...

I regret to say that this is the final article in this series!  It's been good fun for me; getting to the know the ins-and-outs of Nim, and to re-grow a fresh appreciation of Crystal, having put it on the back-burner for quite some time.  However,  whether the final article in the series or not, it's going to be a good one!  We're going to be covering the benefits to the Cryptocurrency / DApp industries from both Crystal and Nim.  So without further ado:

***Let's dive on in!***

## Cryptocurrency

Firstly, I'd like to talk about the possibility of using either Crystal or Nim,  (or both!) in the development of crypto apps. Hypothetically; if we had the inclination to build out our own Cryptocurrency:   Crystal and Nim have ***proven to be two of the strongest languages*** to consider for the undertaking..  (That being the *next* blog series I'm going to write – in the near future, so deciding which language to use will be heavily influenced by ***this*** blog series!)

For our Cryptocurrency, we would need to be able to use an intelligent key manager, utilise smart hashing algorithms, maintain strong performance, and all of this atop of a distributed, decentralised virtual machine or blockchain.  Now, all of this sounds like a ***very*** tall order!  For all of these feature requirements to be met by a single programming language, it would mean that this language is going to have to be **ONE HELL** of an impressive piece of technology.

Happily, both Crystal *and* Nim allow us ***all*** of the above functionality.  In our hypothetical usecase, if we were to build out a fully-featured blockchain; mining *and* hashing functions would need to be continually made, both of which entail relatively heavy computations.  As shown over the last 2 articles in the series, we can at least be sure that both langs can handle the performance stresses, no problemo.

As I'd like to write this topic out into a further detailed article series, I will show off just 2 of the above pieces of functionality we'd require for our Crypto app:

### Calculating our Block Hashes

When building our Blockchain; we need to consider how we're going to identify and chain our transaction blocks together (blockchain).  Without going into details in *this* article on how blockchains function, we'll stick with the existing, and proven, SHA256 algorithm.

### In Crystal:

``` crystal
require "json"
require "openssl"

module OurCryptoApp::Model
  struct Transaction
    include JSON::Serializable

    alias TxnHash = String

    property from : String
    property to : String
    property amount : Float32
    getter hash : TxnHash
    getter timestamp : Int64

    def initialize(@from, @to, @amount)
      @timestamp = Time.utc_now.to_unix
      @hash = calc_hash
    end

    private def calc_hash : TxnHash
      sha = OpenSSL::Digest.new("SHA256")
      sha.update("#{@from}#{@to}#{@amount}#{@timestamp}")
      sha.hexdigest
    end
  end
end
```

### In Nim:

If we want to generate a similar hash in Nim, we could run the following:

``` nim
import strutils

const SHA256Len = 32

proc SHA256(d: cstring, n: culong, md: cstring = nil): cstring {.cdecl, dynlib: "libssl.so", importc.}

proc SHA256(s: string): string =
  result = ""
  let s = SHA256(s.cstring, s.len.culong)
  for i in 0 .. < SHA256Len:
    result.add s[i].BiggestInt.toHex(2).toLower

echo SHA256("Hash this block, yo")
```

## Releasing our Crypto App

Another serious factor we have to consider, is the ability to distribute our crypto app, once built, with great ease.  Remembering that both Crystal and Nim are *compiled* languages, we're already off to a promising start.  (A single executable binary is always going to be easier to distribute than something requiring its own specialist environment!)

It pays rather large dividends being able to write our Crypto app just once, and having the ability to maintain one singular code repo for that app. To this end – I think it is definitely worth considering a multi-platform app framework.  I already know that in my next article series, I will be exploring building a Crypto app using [React Native](https://facebook.github.io/react-native/).

However, if you wish to build the frontend of your cross-platform crypto app in something else, there are a variety of technologies available - all of which seem to work well with both Crystal and Nim:

 - [Ionic Framework](http://ionicframework.com/)
 - [Flutter](https://flutter.io/)
 - [NativeScript](https://www.nativescript.org/)

And if you come from a Windows background:

 - [Xamarin](https://dotnet.microsoft.com/apps/xamarin)

### Building & Releasing In Nim:

If we wanted to build out and release our app for Android, we can run:

```
nim c -c --cpu:arm --os:android -d:androidNDK --noMain:on
```

To generate the C source files we need to include in our Android Studio project.  We then simply add the generated C files to our CMake build script in our Android project.

Similarly, we could run:

```
nim c -c --os:ios --noMain:on
```

To generate C files to include in our XCode project. Then, we can use XCode to compile, link, package and sign everything.

### Building & Releasing In Crystal:

Crystal also allows for cross-compilation, and makes it just as easy.  For example, to build our app for Linux distributions from our Mac, we can run:

```
crystal build your_program.cr --cross-compile --target "x86_64-unknown-linux-gnu"
```

***Worth noting:*** *Crystal doesn't offer the out-of-the-box iPhone / Android cross-compilation functionality that Nim does, so building our app in Nim gets a definite thumbs-up from a distribution point-of-view!*

## Ethereum - Building, Signing & Sending a  Transaction

For the sake of this article, in Crystal, I didn't see the need to write out a more low-level example of the below action, as it *is* so similar to the Nim demo that follows.  This actually worked out in my favour, as it means I get to further show off the native HTTP library for Crystal.

### In Crystal:

``` crystal
require "http/client"

module Ethereum
  class Transaction

    # /ethereum/create/ Create - Ethereum::Transaction.create(args)
    def self.create(to : String, from : String, amount : UInt64, gas_price : UInt64? = nil, gas_limit : UInt64? = nil) : EthereumToSign | ErrorMessage

      headers = HTTP::Headers.new
      if ENV["ONCHAIN_API_KEY"]? != nil
        headers.add("X-API-KEY", ENV["ONCHAIN_API_KEY"])
      end

      response = HTTP::Client.post "https://onchain.io/api/ethereum/create//?to=#{to}&from=#{from}&amount=#{amount}&gas_price=#{gas_price}&gas_limit=#{gas_limit}", headers: headers

      return ErrorMessage.from_json response.body if response.status_code != 200

      ethereumtosign = EthereumToSign.from_json response.body

      return ethereumtosign
    end

    # /ethereum/sign_and_send/ Sign and send - Ethereum::Transaction.sign_and_send(args)
    def self.sign_and_send(to : String, from : String, amount : UInt64, r : String, s : String, v : String, gas_price : UInt64? = nil, gas_limit : UInt64? = nil) : SendStatus | ErrorMessage

      headers = HTTP::Headers.new
      if ENV["ONCHAIN_API_KEY"]? != nil
        headers.add("X-API-KEY", ENV["ONCHAIN_API_KEY"])
      end

      response = HTTP::Client.post "https://onchain.io/api/ethereum/sign_and_send//?to=#{to}&from=#{from}&amount=#{amount}&r=#{r}&s=#{s}&v=#{v}&gas_price=#{gas_price}&gas_limit=#{gas_limit}", headers: headers

      return ErrorMessage.from_json response.body if response.status_code != 200

      sendstatus = SendStatus.from_json response.body

      return sendstatus
    end

  end
end
```

Then, in our application we could simply call:

``` crystal
Ethereum::Transaction.create("0xA02378cA1c24767eCD776aAFeC02158a30dc01ac", "0xA02378cA1c24767eCD776aAFeC02158a30dc01ac", 80000)
```

And we would get a response similar to the following, ready to be signed and sent to the Ethereum network:

``` json
{
  "tx": "02000000011cd5d7621e2a7c9403e54e089cb0b5430b83ed13f1b897d3e319b100ba1b059b01000000db00483045022100d7534c80bc0a42addc3d955f74e31610aa78bf15d79ec4df4c36dc98e802f5200220369cab1bccb2dbca0921444ce3fafb15129fa0494d041998be104df39b8895ec01483045022100fe48c4c1d46e163acaff6b0d2e702812d20",
  "hash_to_sign": "955f74e31610aa78bf15d79ec4df4c36dc98e802f52002"
}
```

## In Nim:

From a deeper, more low-level perspective; instead of using an HTTP library as in the Crystal example above, we can use Status' very own Nim-Ethereum library to build our Ethereum transaction.  Assuming we have imported `nim-eth` into our Nimble project, our Ethereum transaction can be built atop of the following protocol:

``` nim
import
  nim-eth/[common, rlp, keys], nimcrypto

proc initTransaction*(nonce: AccountNonce, gasPrice, gasLimit: GasInt, to: EthAddress,
  value: UInt256, payload: Blob, V: byte, R, S: UInt256, isContractCreation = false): Transaction =
  result.accountNonce = nonce
  result.gasPrice = gasPrice
  result.gasLimit = gasLimit
  result.to = to
  result.value = value
  result.payload = payload
  result.V = V
  result.R = R
  result.S = S
  result.isContractCreation = isContractCreation

type
  TransHashObj = object
    accountNonce:  AccountNonce
    gasPrice:      GasInt
    gasLimit:      GasInt
    to {.rlpCustomSerialization.}: EthAddress
    value:         UInt256
    payload:       Blob
    mIsContractCreation {.rlpIgnore.}: bool

proc read(rlp: var Rlp, t: var TransHashObj, _: type EthAddress): EthAddress {.inline.} =
  if rlp.blobLen != 0:
    result = rlp.read(EthAddress)
  else:
    t.mIsContractCreation = true

proc append(rlpWriter: var RlpWriter, t: TransHashObj, a: EthAddress) {.inline.} =
  if t.mIsContractCreation:
    rlpWriter.append("")
  else:
    rlpWriter.append(a)

const
  EIP155_CHAIN_ID_OFFSET* = 35

func rlpEncode*(transaction: Transaction): auto =
  # Encode transaction without signature
  return rlp.encode(TransHashObj(
    accountNonce: transaction.accountNonce,
    gasPrice: transaction.gasPrice,
    gasLimit: transaction.gasLimit,
    to: transaction.to,
    value: transaction.value,
    payload: transaction.payload,
    mIsContractCreation: transaction.isContractCreation
    ))

func rlpEncodeEIP155*(tx: Transaction): auto =
  let V = (tx.V.int - EIP155_CHAIN_ID_OFFSET) div 2
  # Encode transaction without signature
  return rlp.encode(Transaction(
    accountNonce: tx.accountNonce,
    gasPrice: tx.gasPrice,
    gasLimit: tx.gasLimit,
    to: tx.to,
    value: tx.value,
    payload: tx.payload,
    isContractCreation: tx.isContractCreation,
    V: V.byte,
    R: 0.u256,
    S: 0.u256
    ))

func txHashNoSignature*(tx: Transaction): Hash256 =
  # Hash transaction without signature
  return keccak256.digest(if tx.V.int >= EIP155_CHAIN_ID_OFFSET: tx.rlpEncodeEIP155 else: tx.rlpEncode)
```

*Note* - I do realise the above Nim code example and the Crystal examples are different - I fully intended them to be.  The Crystal example allowed me to further show off the HTTP library I touched on in the last article, and the Nim example allowed me to go to a lower-level; something I think brings the article relevancy full circle.

[Status' Eth Common Library](https://github.com/status-im/nim-eth/) contains a whole bunch of useful Nim libraries for interacting with the Ethereum Network, including:

 - [Recursive Length Prefix encoding (RLP)](https://github.com/status-im/nim-eth/blob/master/doc/rlp.md),
 - [P2P](https://github.com/status-im/nim-eth/blob/master/doc/p2p.md),
 - [Eth-keys](https://github.com/status-im/nim-eth/blob/master/doc/keys.md),
 - [Eth-keyfile](https://github.com/status-im/nim-eth/blob/master/doc/keyfile.md),
 - [Ethereum Trie structure](https://github.com/status-im/nim-eth/blob/master/doc/trie.md), and
 - [Ethereum Bloom Filter](https://github.com/status-im/nim-eth/blob/master/doc/bloom.md).

If you are going to be working in the Ethereum ecosystem using Nim, it goes without saying that these utilities are absolutely essential.  With Status & the [Nimbus](https://nimbus.team) team being such early adopters and major contributors to the Nim/Crypto universe, you are more than likely to stumble across this code sooner or later!

## Conclusion

Our hypothetical Crypto app has taken shape throughout this article, and I think both languages have shown off great promise, and have proven their respective abilities to power the Cryptocurrency universe.

Realistically, if you were a brand-new developer looking to learn a language to break into the Crypto scene, the choice would almost definitely be **Crystal**.  This is simply because of the *much* larger ecosystem and resources surrounding it.

However, if you were an already-established developer, looking to build out a crypto app that you could develop and multi-platform release with greater ease, you'd inevitably choose **Nim**.  Crystal not only lacks the ability to be developed properly on Windows, but also lacks the interoperability and multi-release functionality, as we have seen, with Nim.

Alas, this brings me on to my final points...

## Series Conclusion

It's funny – each article in this series, I've started by saying to myself "Right, Nim is going to win." And then half way through; changing my story to "Crystal is my choice, actually."

But then I went and spoiled it all, by saying something stupid like "Cryptocurrency".

Prior to this article, I *was swaying* towards settling on Crystal.  Not only did it impress in performance, but also seemed to have an enthusiastic ecosystem building around it.  Nim, however, refused to go down without a fight –  offering up *extremely* impressive interoperability, awesome inbuilt tooling, and great efficiency overall.

I hate to do this, but I'm just going to have to say it:  for your usecase – **pick the best tool for the job**. Please ensure that you research properly into both languages, and weigh-up the pro's/con's that pertain to your specific usecase.

***Cliches aside*** – if I had to pick a favourite overall language, it would have to be **Crystal**.  Frankly, this opinion is formed from my extensive use of Crystal over Nim, the fact I **much** prefer the Crystal syntax, and the fact that I am simply more comfortable coding in Crystal than I am in Nim!

So, to answer the epic question – Crystal vs Nim?

Personally, I choose Crystal.  But I think **you** should choose ***Nim.*** 😅

[ **- @rbin**](https://twitter.com/rbin)


text:
package us.corenetwork.limbo;

import org.bukkit.Bukkit;
import org.bukkit.ChatColor;

public class Logs
{
	public static void debug(String text)
	{
		if (Settings.DEBUG.bool())
			sendLog("&f[&3Limbo&f]&f "+text);
	}

	public static void info(String text)
	{
		sendLog("&f[&fLimbo&f]&f "+text);
	}

	public static void warning(String text)
	{
		sendLog("&f[&eLimbo&f]&f " + text);
	}

	public static void severe(String text)
	{
		sendLog("&f[&cLimbo&f]&f " + text);
	}

	public static void sendLog(String text)
	{
		Bukkit.getConsoleSender().sendMessage(ChatColor.translateAlternateColorCodes('&', text));
	}

}


text:
def main(request, response):
    url = request.GET[b"url"].decode("utf-8")
    uuid = request.GET[b"uuid"].decode("utf-8")
    page = request.GET[b"page"].decode("utf-8")
    valid_json = request.GET[b"valid_json"].decode("utf-8")
    empty_json = request.GET[b"empty_json"].decode("utf-8")
    fail_cors = request.GET[b"fail_cors"].decode("utf-8")
    valid_encoding = request.GET[b"valid_encoding"].decode("utf-8")
    redirect = request.GET[b"redirect"].decode("utf-8")
    sec_fetch_dest = request.headers[b"Sec-Fetch-Dest"].decode(
        "utf-8").lower() if b"Sec-Fetch-Dest" in request.headers else None
    content_type = b"application/speculationrules+json" if request.GET[
        b"valid_mime"].decode("utf-8") == "true" else b"application/json"
    status = int(request.GET[b"status"])

    if redirect == "true":
        new_url = request.url.replace("redirect=true",
                                      "redirect=false").encode("utf-8")
        return 301, [(b"Location", new_url),
                     (b'Access-Control-Allow-Origin', b'*')], b""

    encoding = "utf-8" if valid_encoding == "true" else "windows-1250"
    content_type += f'; charset={encoding}'.encode('utf-8')
    strparam = b'\xc3\xb7'.decode('utf-8')

    content = f'''
      {{
        "prefetch": [
          {{
            "source":"list",
            "urls":["{url}?uuid={uuid}&page={page}&str={strparam}"],
            "requires":["anonymous-client-ip-when-cross-origin"]
          }}
        ]
      }}
  '''
    if empty_json == "true":
        content = ""
    elif valid_json != "true":
        content = "invalid json"
    elif sec_fetch_dest is None or sec_fetch_dest != "script":
        content = "normal document"

    headers = [(b"Content-Type", content_type)]
    if fail_cors != "true":
        origin = request.headers[
            b"Origin"] if b"Origin" in request.headers else b'*'
        headers.append((b'Access-Control-Allow-Origin', origin))
    return status, headers, content.encode(encoding)


text:
package plugins;

import java.util.Collection;
import java.util.Date;
import java.util.LinkedList;
import whitebox.geospatialfiles.WhiteboxRaster;
import whitebox.interfaces.WhiteboxPlugin;
import whitebox.interfaces.WhiteboxPluginHost;
import whitebox.utilities.Parallel;

/**
 * This tool can be used to calculate the visibility of all locations within an input digital elevation model (DEM).
 *
 * @author Dr. John Lindsay email: jlindsay@uoguelph.ca
 */
public class VisibilityIndex implements WhiteboxPlugin {

    private WhiteboxPluginHost myHost = null;
    private String[] args;

    /**
     * Used to retrieve the plugin tool's name. This is a short, unique name
     * containing no spaces.
     *
     * @return String containing plugin name.
     */
    @Override
    public String getName() {
        return "VisibilityIndex";
    }

    /**
     * Used to retrieve the plugin tool's descriptive name. This can be a longer
     * name (containing spaces) and is used in the interface to list the tool.
     *
     * @return String containing the plugin descriptive name.
     */
    @Override
    public String getDescriptiveName() {
        return "Visibility Index";
    }

    /**
     * Used to retrieve a short description of what the plugin tool does.
     *
     * @return String containing the plugin's description.
     */
    @Override
    public String getToolDescription() {
        return "Estimates the relative visibility of sites in a DEM.";
    }

    /**
     * Used to identify which toolboxes this plugin tool should be listed in.
     *
     * @return Array of Strings.
     */
    @Override
    public String[] getToolbox() {
        String[] ret = {"TerrainAnalysis"};
        return ret;
    }

    /**
     * Sets the WhiteboxPluginHost to which the plugin tool is tied. This is the
     * class that the plugin will send all feedback messages, progress updates,
     * and return objects.
     *
     * @param host The WhiteboxPluginHost that called the plugin tool.
     */
    @Override
    public void setPluginHost(WhiteboxPluginHost host) {
        myHost = host;
    }

    /**
     * Used to communicate feedback pop-up messages between a plugin tool and
     * the main Whitebox user-interface.
     *
     * @param feedback String containing the text to display.
     */
    private void showFeedback(String message) {
        if (myHost != null) {
            myHost.showFeedback(message);
        } else {
            System.out.println(message);
        }
    }

    /**
     * Used to communicate a return object from a plugin tool to the main
     * Whitebox user-interface.
     *
     * @return Object, such as an output WhiteboxRaster.
     */
    private void returnData(Object ret) {
        if (myHost != null) {
            myHost.returnData(ret);
        }
    }
    private int previousProgress = 0;
    private String previousProgressLabel = "";

    /**
     * Used to communicate a progress update between a plugin tool and the main
     * Whitebox user interface.
     *
     * @param progressLabel A String to use for the progress label.
     * @param progress Float containing the progress value (between 0 and 100).
     */
    private void updateProgress(String progressLabel, int progress) {
        if (myHost != null && ((progress != previousProgress)
                || (!progressLabel.equals(previousProgressLabel)))) {
            myHost.updateProgress(progressLabel, progress);
        }
        previousProgress = progress;
        previousProgressLabel = progressLabel;
    }

    /**
     * Used to communicate a progress update between a plugin tool and the main
     * Whitebox user interface.
     *
     * @param progress Float containing the progress value (between 0 and 100).
     */
    private void updateProgress(int progress) {
        if (myHost != null && progress != previousProgress) {
            myHost.updateProgress(progress);
        }
        previousProgress = progress;
    }

    /**
     * Sets the arguments (parameters) used by the plugin.
     *
     * @param args An array of string arguments.
     */
    @Override
    public void setArgs(String[] args) {
        this.args = args.clone();
    }
    private boolean cancelOp = false;

    /**
     * Used to communicate a cancel operation from the Whitebox GUI.
     *
     * @param cancel Set to true if the plugin should be canceled.
     */
    @Override
    public void setCancelOp(boolean cancel) {
        cancelOp = cancel;
    }

    private void cancelOperation() {
        showFeedback("Operation cancelled.");
        updateProgress("Progress: ", 0);
    }
    private boolean amIActive = false;

    /**
     * Used by the Whitebox GUI to tell if this plugin is still running.
     *
     * @return a boolean describing whether or not the plugin is actively being
     * used.
     */
    @Override
    public boolean isActive() {
        return amIActive;
    }
    private double[][] zValues;
    private double[][] outputData;
    private double numViewsheds = 0;
    private double noData;
    private int outputNoData;
    private int numSolvedRows = 0;
    private double stationHeight = 0;

    /**
     * Used to execute this plugin tool.
     */
    @Override
    public void run() {
        amIActive = true;

        //startTime = System.currentTimeMillis();
        String inputHeader;
        String outputHeader;
        int row, col;
        final int rows, cols;
        int progress = 0;
        double z;
        double[] data;
        double vertCount = 1;
        double horizCount;
        double t1, t2, tva;
        int stationRow;
        int stationCol;
        double stationX;
        double stationY;
        double stationZ;
        double x, y, dist, dZ;
        double va;
        boolean processConcurrently = true;
        //double numViewsheds = 0;

        if (args.length <= 0) {
            showFeedback("Plugin parameters have not been set.");
            return;
        }

        inputHeader = args[0];
        outputHeader = args[1];
        final int step = Integer.parseInt(args[2]);
        processConcurrently = Boolean.parseBoolean(args[3]);
        stationHeight = Double.parseDouble(args[4]);

        // check to see that the inputHeader and outputHeader are not null.
        if ((inputHeader == null) || (outputHeader == null)) {
            showFeedback("One or more of the input parameters have not been set properly.");
            return;
        }

        try {

            WhiteboxRaster DEM = new WhiteboxRaster(inputHeader, "r");
            rows = DEM.getNumberRows();
            cols = DEM.getNumberColumns();
            noData = DEM.getNoDataValue();
            outputNoData = -32768;
            double north = DEM.getNorth();
            double cellSizeY = DEM.getCellSizeY();
            double halfCellSizeY = cellSizeY / 2.0;
            final double[] yCoordsByRow = new double[rows];
            for (int i = 0; i < rows; i++) {
                yCoordsByRow[i] = north - halfCellSizeY - i * cellSizeY;
            }

            double west = DEM.getWest();
            double cellSizeX = DEM.getCellSizeX();
            double halfCellSizeX = cellSizeX / 2.0;
            final double[] xCoordsByColumn = new double[cols];
            for (int i = 0; i < cols; i++) {
                xCoordsByColumn[i] = west + halfCellSizeX + i * cellSizeX;
            }

            zValues = new double[rows][cols];
            for (row = 0; row < rows; row++) {
                data = DEM.getRowValues(row);
                zValues[row] = data;
            }

            DEM.close();

//            double[][] viewAngle = new double[rows][cols];
//            double[][] maxViewAngle = new double[rows][cols];
            outputData = new double[rows][cols];

            if (processConcurrently) {
                Parallel.For(0, rows, step, new Parallel.LoopBody<Integer>() {

                    @Override
                    public void run(Integer stationRow) {
                        double[][] viewAngle = new double[rows][cols];
                        double[][] maxViewAngle = new double[rows][cols];
                        int row, col;
                        double x, y, z, dZ, dist;
                        double stationX, stationY, stationZ;
                        double horizCount, vertCount = 1;
                        double t1, t2, tva;
                        double va;

                        if (!cancelOp) {

                        for (int stationCol = 0; stationCol < cols; stationCol += step) {
                            numViewsheds++;
                            stationX = xCoordsByColumn[stationCol];
                            stationY = yCoordsByRow[stationRow];
                            stationZ = zValues[stationRow][stationCol] + stationHeight;

                            // calculate the view angle to each cell from this station.
                            for (row = 0; row < rows; row++) {
                                for (col = 0; col < cols; col++) {
                                    z = zValues[row][col];
                                    if (z != noData) {
                                        x = xCoordsByColumn[col];
                                        y = yCoordsByRow[row];
                                        dZ = z - stationZ;
                                        dist = Math.sqrt((x - stationX) * (x - stationX) + (y - stationY) * (y - stationY));
                                        if (dist != 0.0) {
                                            viewAngle[row][col] = dZ / dist * 1000.0;
                                        }
                                    } else {
                                        viewAngle[row][col] = noData;
                                    }
                                }
                                if (cancelOp) {
                                    cancelOperation();
                                    return;
                                }
                            }

                            // perform the simple scan lines.
                            for (row = stationRow - 1; row <= stationRow + 1; row++) {
                                for (col = stationCol - 1; col <= stationCol + 1; col++) {
                                    if (row >= 0 && row < rows && col >= 0 && col < cols) {
                                        maxViewAngle[row][col] = viewAngle[row][col];
                                    }
                                }
                            }

                            double maxVA;
                            if (stationRow - 1 >= 0) {
                                maxVA = viewAngle[stationRow - 1][stationCol]; //viewAngle.getValue(stationRow - 1, stationCol);
                                for (row = stationRow - 2; row >= 0; row--) {
                                    z = viewAngle[row][stationCol]; //viewAngle.getValue(row, stationCol);
                                    if (z > maxVA) {
                                        maxVA = z;
                                    }
                                    //maxViewAngle.setValue(row, stationCol, maxVA);
                                    maxViewAngle[row][stationCol] = maxVA;
                                }
                            }
                            if (stationRow + 1 < rows) {
                                maxVA = viewAngle[stationRow + 1][stationCol]; //viewAngle.getValue(stationRow + 1, stationCol);
                                for (row = stationRow + 2; row < rows; row++) {
                                    z = viewAngle[row][stationCol]; //viewAngle.getValue(row, stationCol);
                                    if (z > maxVA) {
                                        maxVA = z;
                                    }
                                    //maxViewAngle.setValue(row, stationCol, maxVA);
                                    maxViewAngle[row][stationCol] = maxVA;
                                }
                            }
                            if (stationCol + 1 < cols) {
                                maxVA = viewAngle[stationRow][stationCol + 1]; //viewAngle.getValue(stationRow, stationCol + 1);
                                for (col = stationCol + 2; col < cols - 1; col++) {
                                    z = viewAngle[stationRow][col]; //viewAngle.getValue(stationRow, col);
                                    if (z > maxVA) {
                                        maxVA = z;
                                    }
                                    //maxViewAngle.setValue(stationRow, col, maxVA);
                                    maxViewAngle[stationRow][col] = maxVA;
                                }
                            }
                            if (stationCol - 1 >= 0) {
                                maxVA = viewAngle[stationRow][stationCol - 1]; //viewAngle.getValue(stationRow, stationCol - 1);
                                for (col = stationCol - 2; col >= 0; col--) {
                                    z = viewAngle[stationRow][col]; //viewAngle.getValue(stationRow, col);
                                    if (z > maxVA) {
                                        maxVA = z;
                                    }
                                    //maxViewAngle.setValue(stationRow, col, maxVA);
                                    maxViewAngle[stationRow][col] = maxVA;
                                }
                            }

                            //solve the first triangular facet
                            vertCount = 1;
                            for (row = stationRow - 2; row >= 0; row--) {
                                vertCount++;
                                horizCount = 0;
                                for (col = stationCol + 1; col <= stationCol + vertCount; col++) {
                                    if (col < cols) {
                                        va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                        horizCount++;
                                        if (horizCount != vertCount) {
                                            t1 = maxViewAngle[row + 1][col - 1]; //maxViewAngle.getValue(row + 1, col - 1);
                                            t2 = maxViewAngle[row + 1][col]; //maxViewAngle.getValue(row + 1, col);
                                            tva = t2 + horizCount / vertCount * (t1 - t2);
                                        } else {
                                            tva = maxViewAngle[row + 1][col - 1]; //maxViewAngle.getValue(row + 1, col - 1);
                                        }
                                        if (tva > va) {
                                            //maxViewAngle.setValue(row, col, tva);
                                            maxViewAngle[row][col] = tva;
                                        } else {
                                            //maxViewAngle.setValue(row, col, va);
                                            maxViewAngle[row][col] = va;
                                        }
                                    } else {
                                        break;
                                    }
                                }
                            }

                            //solve the second triangular facet
                            vertCount = 1;
                            for (row = stationRow - 2; row >= 0; row--) {
                                vertCount++;
                                horizCount = 0;
                                for (col = stationCol - 1; col >= stationCol - vertCount; col--) {
                                    if (col >= 0) {
                                        va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                        horizCount++;
                                        if (horizCount != vertCount) {
                                            t1 = maxViewAngle[row + 1][col + 1]; //maxViewAngle.getValue(row + 1, col + 1);
                                            t2 = maxViewAngle[row + 1][col]; //maxViewAngle.getValue(row + 1, col);
                                            tva = t2 + horizCount / vertCount * (t1 - t2);
                                        } else {
                                            tva = maxViewAngle[row + 1][col + 1]; //maxViewAngle.getValue(row + 1, col + 1);
                                        }
                                        if (tva > va) {
                                            //maxViewAngle.setValue(row, col, tva);
                                            maxViewAngle[row][col] = tva;
                                        } else {
                                            //maxViewAngle.setValue(row, col, va);
                                            maxViewAngle[row][col] = va;
                                        }
                                    } else {
                                        break;
                                    }
                                }
                            }

                            // solve the third triangular facet
                            vertCount = 1;
                            for (row = stationRow + 2; row < rows; row++) {
                                vertCount++;
                                horizCount = 0;
                                for (col = stationCol - 1; col >= stationCol - vertCount; col--) {
                                    if (col >= 0) {
                                        va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                        horizCount++;
                                        if (horizCount != vertCount) {
                                            t1 = maxViewAngle[row - 1][col + 1]; //maxViewAngle.getValue(row - 1, col + 1);
                                            t2 = maxViewAngle[row - 1][col]; //maxViewAngle.getValue(row - 1, col);
                                            tva = t2 + horizCount / vertCount * (t1 - t2);
                                        } else {
                                            tva = maxViewAngle[row - 1][col + 1]; //maxViewAngle.getValue(row - 1, col + 1);
                                        }
                                        if (tva > va) {
                                            //maxViewAngle.setValue(row, col, tva);
                                            maxViewAngle[row][col] = tva;
                                        } else {
                                            //maxViewAngle.setValue(row, col, va);
                                            maxViewAngle[row][col] = va;
                                        }
                                    } else {
                                        break;
                                    }
                                }
                            }

                            // solve the fourth triangular facet
                            vertCount = 1;
                            for (row = stationRow + 2; row < rows; row++) {
                                vertCount++;
                                horizCount = 0;
                                for (col = stationCol + 1; col <= stationCol + vertCount; col++) {
                                    if (col < cols) {
                                        va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                        horizCount++;
                                        if (horizCount != vertCount) {
                                            t1 = maxViewAngle[row - 1][col - 1]; //maxViewAngle.getValue(row - 1, col - 1);
                                            t2 = maxViewAngle[row - 1][col]; //maxViewAngle.getValue(row - 1, col);
                                            tva = t2 + horizCount / vertCount * (t1 - t2);
                                        } else {
                                            tva = maxViewAngle[row - 1][col - 1]; //maxViewAngle.getValue(row - 1, col - 1);
                                        }
                                        if (tva > va) {
                                            //maxViewAngle.setValue(row, col, tva);
                                            maxViewAngle[row][col] = tva;
                                        } else {
                                            //maxViewAngle.setValue(row, col, va);
                                            maxViewAngle[row][col] = va;
                                        }
                                    } else {
                                        break;
                                    }
                                }
                            }

                            // solve the fifth triangular facet
                            vertCount = 1;
                            for (col = stationCol + 2; col < cols; col++) {
                                vertCount++;
                                horizCount = 0;
                                for (row = stationRow - 1; row >= stationRow - vertCount; row--) {
                                    if (row >= 0) {
                                        va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                        horizCount++;
                                        if (horizCount != vertCount) {
                                            t1 = maxViewAngle[row + 1][col - 1]; //maxViewAngle.getValue(row + 1, col - 1);
                                            t2 = maxViewAngle[row][col - 1]; //maxViewAngle.getValue(row, col - 1);
                                            tva = t2 + horizCount / vertCount * (t1 - t2);
                                        } else {
                                            tva = maxViewAngle[row + 1][col - 1]; //maxViewAngle.getValue(row + 1, col - 1);
                                        }
                                        if (tva > va) {
                                            //maxViewAngle.setValue(row, col, tva);
                                            maxViewAngle[row][col] = tva;
                                        } else {
                                            //maxViewAngle.setValue(row, col, va);
                                            maxViewAngle[row][col] = va;
                                        }
                                    } else {
                                        break;
                                    }
                                }
                            }

                            // solve the sixth triangular facet
                            vertCount = 1;
                            for (col = stationCol + 2; col < cols; col++) {
                                vertCount++;
                                horizCount = 0;
                                for (row = stationRow + 1; row <= stationRow + vertCount; row++) {
                                    if (row < rows) {
                                        va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                        horizCount++;
                                        if (horizCount != vertCount) {
                                            t1 = maxViewAngle[row - 1][col - 1]; //maxViewAngle.getValue(row - 1, col - 1);
                                            t2 = maxViewAngle[row][col - 1]; //maxViewAngle.getValue(row, col - 1);
                                            tva = t2 + horizCount / vertCount * (t1 - t2);
                                        } else {
                                            tva = maxViewAngle[row - 1][col - 1]; //maxViewAngle.getValue(row - 1, col - 1);
                                        }
                                        if (tva > va) {
                                            //maxViewAngle.setValue(row, col, tva);
                                            maxViewAngle[row][col] = tva;
                                        } else {
                                            //maxViewAngle.setValue(row, col, va);
                                            maxViewAngle[row][col] = va;
                                        }
                                    } else {
                                        break;
                                    }
                                }
                            }

                            // solve the seventh triangular facet
                            vertCount = 1;
                            for (col = stationCol - 2; col >= 0; col--) {
                                vertCount++;
                                horizCount = 0;
                                for (row = stationRow + 1; row <= stationRow + vertCount; row++) {
                                    if (row < rows) {
                                        va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                        horizCount++;
                                        if (horizCount != vertCount) {
                                            t1 = maxViewAngle[row - 1][col + 1]; //maxViewAngle.getValue(row - 1, col + 1);
                                            t2 = maxViewAngle[row][col + 1]; //maxViewAngle.getValue(row, col + 1);
                                            tva = t2 + horizCount / vertCount * (t1 - t2);
                                        } else {
                                            tva = maxViewAngle[row - 1][col + 1]; //maxViewAngle.getValue(row - 1, col + 1);
                                        }
                                        if (tva > va) {
                                            //maxViewAngle.setValue(row, col, tva);
                                            maxViewAngle[row][col] = tva;
                                        } else {
                                            //maxViewAngle.setValue(row, col, va);
                                            maxViewAngle[row][col] = va;
                                        }
                                    } else {
                                        break;
                                    }
                                }
                            }

                            // solve the eigth triangular facet
                            vertCount = 1;
                            for (col = stationCol - 2; col >= 0; col--) {
                                vertCount++;
                                horizCount = 0;
                                for (row = stationRow - 1; row >= stationRow - vertCount; row--) {
                                    if (row >= 0) {
                                        va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                        horizCount++;
                                        if (horizCount != vertCount) {
                                            t1 = maxViewAngle[row + 1][col + 1]; //maxViewAngle.getValue(row + 1, col + 1);
                                            t2 = maxViewAngle[row][col + 1]; //maxViewAngle.getValue(row, col + 1);
                                            tva = t2 + horizCount / vertCount * (t1 - t2);
                                        } else {
                                            tva = maxViewAngle[row + 1][col + 1]; //maxViewAngle.getValue(row + 1, col + 1);
                                        }
                                        if (tva > va) {
                                            maxViewAngle[row][col] = tva;
                                        } else {
                                            maxViewAngle[row][col] = va;
                                        }
                                    } else {
                                        break;
                                    }
                                }
                            }

                            for (row = 0; row < rows; row++) {
                                for (col = 0; col < cols; col++) {
                                    if (maxViewAngle[row][col] <= viewAngle[row][col] && viewAngle[row][col] != noData) {
                                        outputData[row][col]++;
                                    } else if (viewAngle[row][col] == noData) {
                                        outputData[row][col] = outputNoData;
                                    }
                                }
                                if (cancelOp) {
                                    cancelOperation();
                                    return;
                                }
                            }
                        }
                        if (cancelOp) {
                            cancelOperation();
                            return;
                        }
                        numSolvedRows++;
                        int progress = (int) (100f * numSolvedRows / (rows - 1));
                        updateProgress(progress);
                        }
                    }
                });

            } else {
                for (stationRow = 0; stationRow < rows; stationRow += step) {
                    for (stationCol = 0; stationCol < cols; stationCol += step) {
                        double[][] viewAngle = new double[rows][cols];
                        double[][] maxViewAngle = new double[rows][cols];
                        numViewsheds++;
                        stationX = xCoordsByColumn[stationCol];
                        stationY = yCoordsByRow[stationRow];
                        stationZ = zValues[stationRow][stationCol] + stationHeight;

                        // calculate the view angle to each cell from this station.
                        for (row = 0; row < rows; row++) {
                            for (col = 0; col < cols; col++) {
                                z = zValues[row][col]; //data[col];
                                if (z != noData) {
                                    x = xCoordsByColumn[col];
                                    y = yCoordsByRow[row];
                                    dZ = z - stationZ;
                                    dist = Math.sqrt((x - stationX) * (x - stationX) + (y - stationY) * (y - stationY));
                                    if (dist != 0.0) {
                                        viewAngle[row][col] = dZ / dist * 1000.0;
                                    }
                                } else {
                                    viewAngle[row][col] = noData;
                                }
                            }
                            if (cancelOp) {
                                cancelOperation();
                                return;
                            }
                        }

                        // perform the simple scan lines.
                        for (row = stationRow - 1; row <= stationRow + 1; row++) {
                            for (col = stationCol - 1; col <= stationCol + 1; col++) {
                                //maxViewAngle.setValue(row, col, viewAngle.getValue(row, col));
                                if (row >= 0 && row < rows && col >= 0 && col < cols) {
                                    maxViewAngle[row][col] = viewAngle[row][col];
                                }
                            }
                        }

                        double maxVA;
                        if (stationRow - 1 >= 0) {
                            maxVA = viewAngle[stationRow - 1][stationCol]; //viewAngle.getValue(stationRow - 1, stationCol);
                            for (row = stationRow - 2; row >= 0; row--) {
                                z = viewAngle[row][stationCol]; //viewAngle.getValue(row, stationCol);
                                if (z > maxVA) {
                                    maxVA = z;
                                }
                                //maxViewAngle.setValue(row, stationCol, maxVA);
                                maxViewAngle[row][stationCol] = maxVA;
                            }
                        }
                        if (stationRow + 1 < rows) {
                            maxVA = viewAngle[stationRow + 1][stationCol]; //viewAngle.getValue(stationRow + 1, stationCol);
                            for (row = stationRow + 2; row < rows; row++) {
                                z = viewAngle[row][stationCol]; //viewAngle.getValue(row, stationCol);
                                if (z > maxVA) {
                                    maxVA = z;
                                }
                                //maxViewAngle.setValue(row, stationCol, maxVA);
                                maxViewAngle[row][stationCol] = maxVA;
                            }
                        }
                        if (stationCol + 1 < cols) {
                            maxVA = viewAngle[stationRow][stationCol + 1]; //viewAngle.getValue(stationRow, stationCol + 1);
                            for (col = stationCol + 2; col < cols - 1; col++) {
                                z = viewAngle[stationRow][col]; //viewAngle.getValue(stationRow, col);
                                if (z > maxVA) {
                                    maxVA = z;
                                }
                                //maxViewAngle.setValue(stationRow, col, maxVA);
                                maxViewAngle[stationRow][col] = maxVA;
                            }
                        }
                        if (stationCol - 1 >= 0) {
                            maxVA = viewAngle[stationRow][stationCol - 1]; //viewAngle.getValue(stationRow, stationCol - 1);
                            for (col = stationCol - 2; col >= 0; col--) {
                                z = viewAngle[stationRow][col]; //viewAngle.getValue(stationRow, col);
                                if (z > maxVA) {
                                    maxVA = z;
                                }
                                //maxViewAngle.setValue(stationRow, col, maxVA);
                                maxViewAngle[stationRow][col] = maxVA;
                            }
                        }

                        //solve the first triangular facet
                        vertCount = 1;
                        for (row = stationRow - 2; row >= 0; row--) {
                            vertCount++;
                            horizCount = 0;
                            for (col = stationCol + 1; col <= stationCol + vertCount; col++) {
                                if (col < cols) {
                                    va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                    horizCount++;
                                    if (horizCount != vertCount) {
                                        t1 = maxViewAngle[row + 1][col - 1]; //maxViewAngle.getValue(row + 1, col - 1);
                                        t2 = maxViewAngle[row + 1][col]; //maxViewAngle.getValue(row + 1, col);
                                        tva = t2 + horizCount / vertCount * (t1 - t2);
                                    } else {
                                        tva = maxViewAngle[row + 1][col - 1]; //maxViewAngle.getValue(row + 1, col - 1);
                                    }
                                    if (tva > va) {
                                        //maxViewAngle.setValue(row, col, tva);
                                        maxViewAngle[row][col] = tva;
                                    } else {
                                        //maxViewAngle.setValue(row, col, va);
                                        maxViewAngle[row][col] = va;
                                    }
                                } else {
                                    break;
                                }
                            }
                        }

                        //solve the second triangular facet
                        vertCount = 1;
                        for (row = stationRow - 2; row >= 0; row--) {
                            vertCount++;
                            horizCount = 0;
                            for (col = stationCol - 1; col >= stationCol - vertCount; col--) {
                                if (col >= 0) {
                                    va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                    horizCount++;
                                    if (horizCount != vertCount) {
                                        t1 = maxViewAngle[row + 1][col + 1]; //maxViewAngle.getValue(row + 1, col + 1);
                                        t2 = maxViewAngle[row + 1][col]; //maxViewAngle.getValue(row + 1, col);
                                        tva = t2 + horizCount / vertCount * (t1 - t2);
                                    } else {
                                        tva = maxViewAngle[row + 1][col + 1]; //maxViewAngle.getValue(row + 1, col + 1);
                                    }
                                    if (tva > va) {
                                        //maxViewAngle.setValue(row, col, tva);
                                        maxViewAngle[row][col] = tva;
                                    } else {
                                        //maxViewAngle.setValue(row, col, va);
                                        maxViewAngle[row][col] = va;
                                    }
                                } else {
                                    break;
                                }
                            }
                        }

                        // solve the third triangular facet
                        vertCount = 1;
                        for (row = stationRow + 2; row < rows; row++) {
                            vertCount++;
                            horizCount = 0;
                            for (col = stationCol - 1; col >= stationCol - vertCount; col--) {
                                if (col >= 0) {
                                    va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                    horizCount++;
                                    if (horizCount != vertCount) {
                                        t1 = maxViewAngle[row - 1][col + 1]; //maxViewAngle.getValue(row - 1, col + 1);
                                        t2 = maxViewAngle[row - 1][col]; //maxViewAngle.getValue(row - 1, col);
                                        tva = t2 + horizCount / vertCount * (t1 - t2);
                                    } else {
                                        tva = maxViewAngle[row - 1][col + 1]; //maxViewAngle.getValue(row - 1, col + 1);
                                    }
                                    if (tva > va) {
                                        //maxViewAngle.setValue(row, col, tva);
                                        maxViewAngle[row][col] = tva;
                                    } else {
                                        //maxViewAngle.setValue(row, col, va);
                                        maxViewAngle[row][col] = va;
                                    }
                                } else {
                                    break;
                                }
                            }
                        }

                        // solve the fourth triangular facet
                        vertCount = 1;
                        for (row = stationRow + 2; row < rows; row++) {
                            vertCount++;
                            horizCount = 0;
                            for (col = stationCol + 1; col <= stationCol + vertCount; col++) {
                                if (col < cols) {
                                    va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                    horizCount++;
                                    if (horizCount != vertCount) {
                                        t1 = maxViewAngle[row - 1][col - 1]; //maxViewAngle.getValue(row - 1, col - 1);
                                        t2 = maxViewAngle[row - 1][col]; //maxViewAngle.getValue(row - 1, col);
                                        tva = t2 + horizCount / vertCount * (t1 - t2);
                                    } else {
                                        tva = maxViewAngle[row - 1][col - 1]; //maxViewAngle.getValue(row - 1, col - 1);
                                    }
                                    if (tva > va) {
                                        //maxViewAngle.setValue(row, col, tva);
                                        maxViewAngle[row][col] = tva;
                                    } else {
                                        //maxViewAngle.setValue(row, col, va);
                                        maxViewAngle[row][col] = va;
                                    }
                                } else {
                                    break;
                                }
                            }
                        }

                        // solve the fifth triangular facet
                        vertCount = 1;
                        for (col = stationCol + 2; col < cols; col++) {
                            vertCount++;
                            horizCount = 0;
                            for (row = stationRow - 1; row >= stationRow - vertCount; row--) {
                                if (row >= 0) {
                                    va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                    horizCount++;
                                    if (horizCount != vertCount) {
                                        t1 = maxViewAngle[row + 1][col - 1]; //maxViewAngle.getValue(row + 1, col - 1);
                                        t2 = maxViewAngle[row][col - 1]; //maxViewAngle.getValue(row, col - 1);
                                        tva = t2 + horizCount / vertCount * (t1 - t2);
                                    } else {
                                        tva = maxViewAngle[row + 1][col - 1]; //maxViewAngle.getValue(row + 1, col - 1);
                                    }
                                    if (tva > va) {
                                        //maxViewAngle.setValue(row, col, tva);
                                        maxViewAngle[row][col] = tva;
                                    } else {
                                        //maxViewAngle.setValue(row, col, va);
                                        maxViewAngle[row][col] = va;
                                    }
                                } else {
                                    break;
                                }
                            }
                        }

                        // solve the sixth triangular facet
                        vertCount = 1;
                        for (col = stationCol + 2; col < cols; col++) {
                            vertCount++;
                            horizCount = 0;
                            for (row = stationRow + 1; row <= stationRow + vertCount; row++) {
                                if (row < rows) {
                                    va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                    horizCount++;
                                    if (horizCount != vertCount) {
                                        t1 = maxViewAngle[row - 1][col - 1]; //maxViewAngle.getValue(row - 1, col - 1);
                                        t2 = maxViewAngle[row][col - 1]; //maxViewAngle.getValue(row, col - 1);
                                        tva = t2 + horizCount / vertCount * (t1 - t2);
                                    } else {
                                        tva = maxViewAngle[row - 1][col - 1]; //maxViewAngle.getValue(row - 1, col - 1);
                                    }
                                    if (tva > va) {
                                        //maxViewAngle.setValue(row, col, tva);
                                        maxViewAngle[row][col] = tva;
                                    } else {
                                        //maxViewAngle.setValue(row, col, va);
                                        maxViewAngle[row][col] = va;
                                    }
                                } else {
                                    break;
                                }
                            }
                        }

                        // solve the seventh triangular facet
                        vertCount = 1;
                        for (col = stationCol - 2; col >= 0; col--) {
                            vertCount++;
                            horizCount = 0;
                            for (row = stationRow + 1; row <= stationRow + vertCount; row++) {
                                if (row < rows) {
                                    va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                    horizCount++;
                                    if (horizCount != vertCount) {
                                        t1 = maxViewAngle[row - 1][col + 1]; //maxViewAngle.getValue(row - 1, col + 1);
                                        t2 = maxViewAngle[row][col + 1]; //maxViewAngle.getValue(row, col + 1);
                                        tva = t2 + horizCount / vertCount * (t1 - t2);
                                    } else {
                                        tva = maxViewAngle[row - 1][col + 1]; //maxViewAngle.getValue(row - 1, col + 1);
                                    }
                                    if (tva > va) {
                                        //maxViewAngle.setValue(row, col, tva);
                                        maxViewAngle[row][col] = tva;
                                    } else {
                                        //maxViewAngle.setValue(row, col, va);
                                        maxViewAngle[row][col] = va;
                                    }
                                } else {
                                    break;
                                }
                            }
                        }

                        // solve the eigth triangular facet
                        vertCount = 1;
                        for (col = stationCol - 2; col >= 0; col--) {
                            vertCount++;
                            horizCount = 0;
                            for (row = stationRow - 1; row >= stationRow - vertCount; row--) {
                                if (row >= 0) {
                                    va = viewAngle[row][col]; //viewAngle.getValue(row, col);
                                    horizCount++;
                                    if (horizCount != vertCount) {
                                        t1 = maxViewAngle[row + 1][col + 1]; //maxViewAngle.getValue(row + 1, col + 1);
                                        t2 = maxViewAngle[row][col + 1]; //maxViewAngle.getValue(row, col + 1);
                                        tva = t2 + horizCount / vertCount * (t1 - t2);
                                    } else {
                                        tva = maxViewAngle[row + 1][col + 1]; //maxViewAngle.getValue(row + 1, col + 1);
                                    }
                                    if (tva > va) {
                                        maxViewAngle[row][col] = tva;
                                    } else {
                                        maxViewAngle[row][col] = va;
                                    }
                                } else {
                                    break;
                                }
                            }
                        }

                        for (row = 0; row < rows; row++) {
                            for (col = 0; col < cols; col++) {
                                if (maxViewAngle[row][col] <= viewAngle[row][col] && viewAngle[row][col] != noData) {
                                    outputData[row][col]++;
                                } else if (viewAngle[row][col] == noData) {
                                    outputData[row][col] = outputNoData;
                                }
                            }
                            if (cancelOp) {
                                cancelOperation();
                                return;
                            }
                        }
                    }
                    if (cancelOp) {
                        cancelOperation();
                        return;
                    }
                    progress = (int) (100f * stationRow / (rows - 1));
                    updateProgress(progress);
                }

            }
            //viewAngle = new double[0][0];
            //maxViewAngle = new double[0][0];
            zValues = new double[0][0];

            WhiteboxRaster output = new WhiteboxRaster(outputHeader, "rw",
                    inputHeader, WhiteboxRaster.DataType.FLOAT, outputNoData);
            output.setNoDataValue(outputNoData);
            output.setPreferredPalette("spectrum.pal");
            output.setDataScale(WhiteboxRaster.DataScale.CONTINUOUS);

            for (row = 0; row < rows; row++) {
                for (col = 0; col < cols; col++) {
                    if (outputData[row][col] != outputNoData) {
                        output.setValue(row, col, outputData[row][col] / numViewsheds);
                    }
                }
                if (cancelOp) {
                    cancelOperation();
                    return;
                }
                progress = (int) (100f * row / (rows - 1));
                updateProgress(progress);
            }

            outputData = new double[0][0];

            output.addMetadataEntry("Created by the "
                    + getDescriptiveName() + " tool.");
            output.addMetadataEntry("Created on " + new Date());

            output.close();

            //System.out.println("VisibilityIndex took " + (System.currentTimeMillis() - startTime) + " milliseconds.");

            // returning a header file string displays the image.
            returnData(outputHeader);

        } catch (OutOfMemoryError oe) {
            myHost.showFeedback("An out-of-memory error has occurred during operation.");
        } catch (Exception e) {
            myHost.showFeedback("An error has occurred during operation. See log file for details.");
            myHost.logException("Error in " + getDescriptiveName(), e);
        } finally {
            updateProgress("Progress: ", 0);
            // tells the main application that this process is completed.
            amIActive = false;
            myHost.pluginComplete();
        }
    }

//    //long startTime;
//    // this is only used for testing the tool
//    public static void main(String[] args) {
//        VisibilityIndex vi = new VisibilityIndex();
//        args = new String[5];
//        args[0] = "/Users/johnlindsay/Documents/Data/Vermont DEM/Vermont DEM.dep";
//        args[1] = "/Users/johnlindsay/Documents/Data/Vermont DEM/temp2.dep";
//        args[2] = "2";
//        args[3] = "true";
//        args[4] = "2.0";
//        vi.setArgs(args);
//        vi.run();
//
//    }
}


text:
'''
Various helper functions that tockloader uses. Mostly for interacting with
users in a nice way.
'''

import argparse
import sys

import colorama

def set_terminal_title (title):
	if sys.stdout.isatty():
		sys.stdout.write(colorama.ansi.set_title(title))
		sys.stdout.flush()

def set_terminal_title_from_port_info (info):
	'''
	Set a terminal title from a `pyserial` object.
	'''
	extras = ['Tockloader']
	if info.manufacturer and info.manufacturer != 'n/a':
		extras.append(info.manufacturer)
	if info.name and info.name != 'n/a':
		extras.append(info.name)
	if info.description and info.description != 'n/a':
		extras.append(info.description)
	#if info.hwid and info.hwid != 'n/a':
	#	extras.append(info.hwid)
	if info.product and info.product != 'n/a':
		if info.product != info.description:
			extras.append(info.product)
	title = ' : '.join(extras)

	set_terminal_title(title)

def set_terminal_title_from_port (port):
	'''
	Set the title of the user's terminal for Tockloader.
	'''
	set_terminal_title('Tockloader : {}'.format(port))

def menu (options, *, return_type, default_index=0, prompt='Which option? ', title=''):
	'''
	Present a menu of choices to a user

	`options` should be a like-list object whose iterated objects can be coerced
	into strings.

	`return_type` must be set to one of
	  - "index" - for the index into the options array
	  - "value" - for the option value chosen

	`default_index` is the index to present as the default value (what happens
	if the user simply presses enter). Passing `None` disables default
	selection.
	'''
	prompt_to_show = prompt
	print(title)
	for i,opt in enumerate(options):
		print('[{}]\t{}'.format(i, opt))
	if default_index is not None:
		prompt_to_show += '[{}] '.format(default_index)
	print()

	resp = input(prompt_to_show)
	if resp == '':
		resp = default_index
	else:
		try:
			resp = int(resp)
			if resp < 0 or resp >= len(options):
				raise ValueError
		except:
			return menu(options,
				        return_type=return_type,
				        default_index=default_index,
				        prompt=prompt,
				        title=title)

	if return_type == 'index':
		return resp
	elif return_type == 'value':
		return options[resp]
	else:
		raise NotImplementedError('Menu caller asked for bad return_type')

def plural (value):
	'''
	Return '' or 's' based on whether the `value` means a string should have
	a plural word.

	`value` can be a list or a number. If the number or the length of the list
	is 1, then '' will be returned. Otherwise 's'.
	'''
	try:
		value = len(value)
	except:
		pass
	if value == 1:
		return ''
	else:
		return 's'

def text_in_box (string, box_width):
	'''
	Return a string like:
	```
	┌───────────────┐
	│ str           │
	└───────────────┘
	```
	'''
	string_len = box_width - 4
	truncated_str = (string[:string_len-3] + '...') if len(string) > string_len else string
	out = '┌{}┐\n'.format('─'*(box_width-2))
	out += '│ {} |\n'.format(truncated_str.ljust(string_len))
	out += '└{}┘'.format('─'*(box_width-2))
	return out

class ListToDictAction(argparse.Action):
	'''
	`argparse` action to convert `[['key', 'val'], ['key2', 'val2']]` to
	`{'key': 'val', 'key2': 'val2'}`.

	This will also do the following conversions:
	- `[[]]` -> `{}`
	- `[['k': 'v'], []]` -> `{'k': 'v'}`
	- `[['k': 'v'], ['']]` -> `{'k': 'v'}`
	- `[['k': 'v'], ['a']]` -> `{'k': 'v', 'a': ''}`
	'''
	def __call__(self, parser, namespace, values, option_string=None):
		# Remove any empty values.
		values = list(filter(None, values))
		values = list(filter(lambda x: len(x[0]), values))

		# Correct any bad values.
		for item in values:
			print(item)
			if len(item) == 1:
				item.append('')
			elif len(item) > 2:
				item = item[0:2]

		# Convert to dict and set as argument attribute.
		setattr(namespace, self.dest, dict(values))


text:
package org.gradle.api.internal.artifacts.transform;

import com.google.common.collect.ImmutableList;
import org.gradle.api.Action;
import org.gradle.api.internal.artifacts.ivyservice.ArtifactCacheMetadata;
import org.gradle.api.internal.changedetection.state.InMemoryCacheDecoratorFactory;
import org.gradle.cache.CacheBuilder;
import org.gradle.cache.CacheRepository;
import org.gradle.cache.CleanupAction;
import org.gradle.cache.FileLockManager;
import org.gradle.cache.PersistentCache;
import org.gradle.cache.PersistentIndexedCache;
import org.gradle.cache.PersistentIndexedCacheParameters;
import org.gradle.cache.internal.CompositeCleanupAction;
import org.gradle.cache.internal.LeastRecentlyUsedCacheCleanup;
import org.gradle.cache.internal.ProducerGuard;
import org.gradle.cache.internal.SingleDepthFilesFinder;
import org.gradle.initialization.RootBuildLifecycleListener;
import org.gradle.internal.Factory;
import org.gradle.internal.UncheckedException;
import org.gradle.internal.concurrent.Stoppable;
import org.gradle.internal.hash.HashCode;
import org.gradle.internal.hash.Hasher;
import org.gradle.internal.hash.Hashing;
import org.gradle.internal.resource.local.DefaultPathKeyFileStore;
import org.gradle.internal.resource.local.FileAccessTimeJournal;
import org.gradle.internal.resource.local.FileAccessTracker;
import org.gradle.internal.resource.local.FileStore;
import org.gradle.internal.resource.local.FileStoreAddActionException;
import org.gradle.internal.resource.local.SingleDepthFileAccessTracker;
import org.gradle.internal.serialize.BaseSerializerFactory;
import org.gradle.internal.serialize.HashCodeSerializer;
import org.gradle.internal.serialize.ListSerializer;
import org.gradle.internal.snapshot.FileSystemLocationSnapshot;
import org.gradle.internal.snapshot.FileSystemSnapshotter;
import org.gradle.internal.util.BiFunction;

import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static org.gradle.api.internal.artifacts.ivyservice.CacheLayout.TRANSFORMS_META_DATA;
import static org.gradle.api.internal.artifacts.ivyservice.CacheLayout.TRANSFORMS_STORE;
import static org.gradle.cache.internal.LeastRecentlyUsedCacheCleanup.DEFAULT_MAX_AGE_IN_DAYS_FOR_RECREATABLE_CACHE_ENTRIES;
import static org.gradle.cache.internal.filelock.LockOptionsBuilder.mode;

public class DefaultCachingTransformerExecutor implements CachingTransformerExecutor, Stoppable, RootBuildLifecycleListener {

    private static final int FILE_TREE_DEPTH_TO_TRACK_AND_CLEANUP = 2;
    private static final String CACHE_PREFIX = TRANSFORMS_META_DATA.getKey() + "/";

    private final PersistentCache cache;
    private final PersistentIndexedCache<HashCode, List<File>> indexedCache;
    private final FileStore<String> fileStore;
    private final ProducerGuard<CacheKey> producing = ProducerGuard.adaptive();
    private final Map<CacheKey, List<File>> resultHashToResult = new ConcurrentHashMap<CacheKey, List<File>>();
    private final FileSystemSnapshotter fileSystemSnapshotter;
    private final FileAccessTracker fileAccessTracker;

    public DefaultCachingTransformerExecutor(ArtifactCacheMetadata artifactCacheMetadata, CacheRepository cacheRepository, InMemoryCacheDecoratorFactory cacheDecoratorFactory,
                                             FileSystemSnapshotter fileSystemSnapshotter, FileAccessTimeJournal fileAccessTimeJournal) {
        this.fileSystemSnapshotter = fileSystemSnapshotter;
        File transformsStoreDirectory = artifactCacheMetadata.getTransformsStoreDirectory();
        File filesOutputDirectory = new File(transformsStoreDirectory, TRANSFORMS_STORE.getKey());
        fileStore = new DefaultPathKeyFileStore(filesOutputDirectory);
        cache = cacheRepository
            .cache(transformsStoreDirectory)
            .withCleanup(createCleanupAction(filesOutputDirectory, fileAccessTimeJournal))
            .withCrossVersionCache(CacheBuilder.LockTarget.DefaultTarget)
            .withDisplayName("Artifact transforms cache")
            .withLockOptions(mode(FileLockManager.LockMode.None)) // Lock on demand
            .open();
        indexedCache = cache.createCache(PersistentIndexedCacheParameters.of(CACHE_PREFIX + "results", new HashCodeSerializer(), new ListSerializer<File>(BaseSerializerFactory.FILE_SERIALIZER))
            .cacheDecorator(cacheDecoratorFactory.decorator(1000, true)));
        fileAccessTracker = new SingleDepthFileAccessTracker(fileAccessTimeJournal, filesOutputDirectory, FILE_TREE_DEPTH_TO_TRACK_AND_CLEANUP);
    }

    private CleanupAction createCleanupAction(File filesOutputDirectory, FileAccessTimeJournal fileAccessTimeJournal) {
        return CompositeCleanupAction.builder()
            .add(filesOutputDirectory, new LeastRecentlyUsedCacheCleanup(new SingleDepthFilesFinder(FILE_TREE_DEPTH_TO_TRACK_AND_CLEANUP), fileAccessTimeJournal, DEFAULT_MAX_AGE_IN_DAYS_FOR_RECREATABLE_CACHE_ENTRIES))
            .build();
    }

    @Override
    public void stop() {
        cache.close();
    }

    @Override
    public void afterStart() {
    }

    @Override
    public void beforeComplete() {
        // Discard cached results between builds
        resultHashToResult.clear();
    }

    @Override
    public boolean contains(File absoluteFile, HashCode inputsHash) {
        return resultHashToResult.containsKey(getCacheKey(absoluteFile, inputsHash));
    }

    @Override
    public List<File> getResult(File primaryInput, Transformer transformer) {
        File absolutePrimaryInput = primaryInput.getAbsoluteFile();
        CacheKey cacheKey = getCacheKey(absolutePrimaryInput, transformer);
        List<File> transformedFiles = resultHashToResult.get(cacheKey);
        if (transformedFiles != null) {
            return transformedFiles;
        }
        return loadIntoCache(absolutePrimaryInput, cacheKey, transformer);
    }

    /**
     * Loads the transformed files from the file system cache into memory. Creates them if they are not present yet.
     * This makes sure that only one thread tries to load a result for a given key.
     */
    private List<File> loadIntoCache(final File inputFile, final CacheKey cacheKey, final BiFunction<List<File>, File, File> transformer) {
        return producing.guardByKey(cacheKey, new Factory<List<File>>() {
            @Override
            public List<File> create() {
                List<File> files = resultHashToResult.get(cacheKey);
                if (files != null) {
                    return files;
                }
                files = cache.withFileLock(new Factory<List<File>>() {
                    @Override
                    public List<File> create() {
                        HashCode persistentCacheKey = cacheKey.getPersistentCacheKey();
                        List<File> files = indexedCache.get(persistentCacheKey);
                        if (files != null) {
                            boolean allExist = true;
                            for (File file : files) {
                                if (!file.exists()) {
                                    allExist = false;
                                    break;
                                }
                            }
                            if (allExist) {
                                return files;
                            }
                        }

                        String key = inputFile.getName() + "/" + persistentCacheKey;
                        TransformAction action = new TransformAction(transformer, inputFile);
                        try {
                            fileStore.add(key, action);
                        } catch (FileStoreAddActionException e) {
                            throw UncheckedException.throwAsUncheckedException(e.getCause());
                        }

                        indexedCache.put(persistentCacheKey, action.result);
                        return action.result;
                    }
                });

                fileAccessTracker.markAccessed(files);
                resultHashToResult.put(cacheKey, files);
                return files;
            }
        });
    }

    private CacheKey getCacheKey(File primaryInput, Transformer transformer) {
        return getCacheKey(primaryInput, transformer.getSecondaryInputHash());
    }

    private CacheKey getCacheKey(File inputFile, HashCode inputsHash) {
        FileSystemLocationSnapshot snapshot = fileSystemSnapshotter.snapshot(inputFile);
        return new CacheKey(inputsHash, snapshot.getAbsolutePath(), snapshot.getHash());
    }

    /**
     * A lightweight key for in-memory caching of transformation results.
     * Computing the hash key for the persistent cache is a rather expensive
     * operation, so we only calculate it when we have a cache miss in memory.
     */
    private static class CacheKey {
        private final String absolutePath;
        private final HashCode fileContentHash;
        private final HashCode inputHash;

        public CacheKey(HashCode inputHash, String absolutePath, HashCode fileContentHash) {
            this.absolutePath = absolutePath;
            this.fileContentHash = fileContentHash;
            this.inputHash = inputHash;
        }

        public HashCode getPersistentCacheKey() {
            Hasher hasher = Hashing.newHasher();
            hasher.putHash(inputHash);
            hasher.putString(absolutePath);
            hasher.putHash(fileContentHash);
            return hasher.hash();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            CacheKey cacheKey = (CacheKey) o;

            if (!fileContentHash.equals(cacheKey.fileContentHash)) {
                return false;
            }
            if (!inputHash.equals(cacheKey.inputHash)) {
                return false;
            }
            return absolutePath.equals(cacheKey.absolutePath);
        }

        @Override
        public int hashCode() {
            int result = fileContentHash.hashCode();
            result = 31 * result + absolutePath.hashCode();
            result = 31 * result + inputHash.hashCode();
            return result;
        }
    }

    private static class TransformAction implements Action<File> {
        private final BiFunction<List<File>, File, File> transformer;
        private final File inputFile;
        private ImmutableList<File> result;

        TransformAction(BiFunction<List<File>, File, File> transformer, File inputFile) {
            this.transformer = transformer;
            this.inputFile = inputFile;
        }

        @Override
        public void execute(File outputDir) {
            outputDir.mkdirs();
            result = ImmutableList.copyOf(transformer.apply(inputFile, outputDir));
        }
    }
}


text:
package org.kuali.kra.institutionalproposal.notification;

import org.kuali.coeus.common.notification.impl.service.KcNotificationRoleQualifierService;
import org.kuali.kra.institutionalproposal.home.InstitutionalProposal;

/**
 * Defines the service to fill in module role qualifier information for Institutional Proposal.
 */
public interface InstitutionalProposalNotificationRoleQualifierService extends KcNotificationRoleQualifierService {

    /**
     * Returns the Institutional Proposal.
     *
     * @return the Institutional Proposal
     */
    InstitutionalProposal getInstitutionalProposal();

    /**
     * Sets the Institutional Proposal.
     *
     * @param institutionalProposal the Institutional Proposal to set
     */
    void setInstitutionalProposal(InstitutionalProposal institutionalProposal);

}