Distance between repeatable points

Since Fulcrum calculations are written in JavaScript, we can incorporate 3rd party scripts to provide helper functions. One excellent open source library for working with basic geospatial operations in JavaScript is Geolib. The example below demonstrates how to incorporate the Geolib library into a calculation field and use the getPathLength function to calculate the total distance between repeatable points.

/*! geolib 2.0.21 by Manuel Bieh
* Library to provide geo functions like distance calculation,
* conversion of decimal coordinates to sexagesimal and vice versa, etc.
* WGS 84 (World Geodetic System 1984)
*
* @author Manuel Bieh
* @url http://www.manuelbieh.com/
* @version 2.0.21
* @license MIT
**/
!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.geolib=t()}}(function(){var t;return function t(e,i,a){function n(s,r){if(!i[s]){if(!e[s]){var u="function"==typeof require&&require;if(!r&&u)return u(s,!0);if(o)return o(s,!0);var h=new Error("Cannot find module '"+s+"'");throw h.code="MODULE_NOT_FOUND",h}var l=i[s]={exports:{}};e[s][0].call(l.exports,function(t){var i=e[s][1][t];return n(i?i:t)},l,l.exports,t,e,i,a)}return i[s].exports}for(var o="function"==typeof require&&require,s=0;s<a.length;s++)n(a[s]);return n}({geolib:[function(e,i,a){!function(e,a){"use strict";function n(){}n.TO_RAD=Math.PI/180,n.TO_DEG=180/Math.PI,n.PI_X2=2*Math.PI,n.PI_DIV4=Math.PI/4;var o=Object.create(n.prototype,{version:{value:"2.0.21"},radius:{value:6378137},minLat:{value:-90},maxLat:{value:90},minLon:{value:-180},maxLon:{value:180},sexagesimalPattern:{value:/^([0-9]{1,3})°\s*([0-9]{1,3}(?:\.(?:[0-9]{1,2}))?)'\s*(([0-9]{1,3}(\.([0-9]{1,4}))?)"\s*)?([NEOSW]?)$/},measures:{value:Object.create(Object.prototype,{m:{value:1},km:{value:.001},cm:{value:100},mm:{value:1e3},mi:{value:1/1609.344},sm:{value:1/1852.216},ft:{value:100/30.48},in:{value:100/2.54},yd:{value:1/.9144}})},prototype:{value:n.prototype},extend:{value:function(t,e){for(var i in t)"undefined"!=typeof o.prototype[i]&&e!==!0||("function"==typeof t[i]&&"function"==typeof t[i].bind?o.prototype[i]=t[i].bind(o):o.prototype[i]=t[i])}}});"undefined"==typeof Number.prototype.toRad&&(Number.prototype.toRad=function(){return this*n.TO_RAD}),"undefined"==typeof Number.prototype.toDeg&&(Number.prototype.toDeg=function(){return this*n.TO_DEG}),o.extend({decimal:{},sexagesimal:{},distance:null,getKeys:function(t){if("[object Array]"==Object.prototype.toString.call(t))return{longitude:t.length>=1?0:a,latitude:t.length>=2?1:a,elevation:t.length>=3?2:a};var e=function(e){var i;return e.every(function(e){return"object"!=typeof t||(!t.hasOwnProperty(e)||function(){return i=e,!1}())}),i},i=e(["lng","lon","longitude"]),n=e(["lat","latitude"]),o=e(["alt","altitude","elevation","elev"]);return"undefined"==typeof n&&"undefined"==typeof i&&"undefined"==typeof o?a:{latitude:n,longitude:i,elevation:o}},getLat:function(t,e){return e===!0?t[this.getKeys(t).latitude]:this.useDecimal(t[this.getKeys(t).latitude])},latitude:function(t){return this.getLat.call(this,t)},getLon:function(t,e){return e===!0?t[this.getKeys(t).longitude]:this.useDecimal(t[this.getKeys(t).longitude])},longitude:function(t){return this.getLon.call(this,t)},getElev:function(t){return t[this.getKeys(t).elevation]},elevation:function(t){return this.getElev.call(this,t)},coords:function(t,e){var i={latitude:e===!0?t[this.getKeys(t).latitude]:this.useDecimal(t[this.getKeys(t).latitude]),longitude:e===!0?t[this.getKeys(t).longitude]:this.useDecimal(t[this.getKeys(t).longitude])},a=t[this.getKeys(t).elevation];return"undefined"!=typeof a&&(i.elevation=a),i},ll:function(t,e){return this.coords.call(this,t,e)},validate:function(t){var e=this.getKeys(t);if("undefined"==typeof e||"undefined"==typeof e.latitude||"undefined"===e.longitude)return!1;var i=t[e.latitude],a=t[e.longitude];return!("undefined"==typeof i||!this.isDecimal(i)&&!this.isSexagesimal(i))&&(!("undefined"==typeof a||!this.isDecimal(a)&&!this.isSexagesimal(a))&&(i=this.useDecimal(i),a=this.useDecimal(a),!(i<this.minLat||i>this.maxLat||a<this.minLon||a>this.maxLon)))},getDistance:function(t,e,i,a){i=Math.floor(i)||1,a=Math.floor(a)||0;var n,s,r,u,h,l,d,c=this.coords(t),g=this.coords(e),f=6378137,m=6356752.314245,p=1/298.257223563,v=(g.longitude-c.longitude).toRad(),M=Math.atan((1-p)*Math.tan(parseFloat(c.latitude).toRad())),y=Math.atan((1-p)*Math.tan(parseFloat(g.latitude).toRad())),x=Math.sin(M),D=Math.cos(M),b=Math.sin(y),L=Math.cos(y),R=v,P=100;do{var N=Math.sin(R),E=Math.cos(R);if(l=Math.sqrt(L*N*(L*N)+(D*b-x*L*E)*(D*b-x*L*E)),0===l)return o.distance=0;n=x*b+D*L*E,s=Math.atan2(l,n),r=D*L*N/l,u=1-r*r,h=n-2*x*b/u,isNaN(h)&&(h=0);var I=p/16*u*(4+p*(4-3*u));d=R,R=v+(1-I)*p*r*(s+I*l*(h+I*n*(-1+2*h*h)))}while(Math.abs(R-d)>1e-12&&--P>0);if(0===P)return NaN;var S=u*(f*f-m*m)/(m*m),F=1+S/16384*(4096+S*(-768+S*(320-175*S))),w=S/1024*(256+S*(-128+S*(74-47*S))),k=w*l*(h+w/4*(n*(-1+2*h*h)-w/6*h*(-3+4*l*l)*(-3+4*h*h))),O=m*F*(s-k);if(O=O.toFixed(a),"undefined"!=typeof this.elevation(t)&&"undefined"!=typeof this.elevation(e)){var W=Math.abs(this.elevation(t)-this.elevation(e));O=Math.sqrt(O*O+W*W)}return this.distance=Math.round(O*Math.pow(10,a)/i)*i/Math.pow(10,a)},getDistanceSimple:function(t,e,i){i=Math.floor(i)||1;var a=Math.round(Math.acos(Math.sin(this.latitude(e).toRad())*Math.sin(this.latitude(t).toRad())+Math.cos(this.latitude(e).toRad())*Math.cos(this.latitude(t).toRad())*Math.cos(this.longitude(t).toRad()-this.longitude(e).toRad()))*this.radius);return o.distance=Math.floor(Math.round(a/i)*i)},getCenter:function(t){var e=t;if("object"==typeof t&&!(t instanceof Array)){e=[];for(var i in t)e.push(this.coords(t[i]))}if(!e.length)return!1;var a,o,s,r=0,u=0,h=0;e.forEach(function(t){a=this.latitude(t).toRad(),o=this.longitude(t).toRad(),r+=Math.cos(a)*Math.cos(o),u+=Math.cos(a)*Math.sin(o),h+=Math.sin(a)},this);var l=e.length;return r/=l,u/=l,h/=l,o=Math.atan2(u,r),s=Math.sqrt(r*r+u*u),a=Math.atan2(h,s),{latitude:(a*n.TO_DEG).toFixed(6),longitude:(o*n.TO_DEG).toFixed(6)}},getBounds:function(t){if(!t.length)return!1;var e=this.elevation(t[0]),i={maxLat:-(1/0),minLat:1/0,maxLng:-(1/0),minLng:1/0};"undefined"!=typeof e&&(i.maxElev=0,i.minElev=1/0);for(var a=0,n=t.length;a<n;++a)i.maxLat=Math.max(this.latitude(t[a]),i.maxLat),i.minLat=Math.min(this.latitude(t[a]),i.minLat),i.maxLng=Math.max(this.longitude(t[a]),i.maxLng),i.minLng=Math.min(this.longitude(t[a]),i.minLng),e&&(i.maxElev=Math.max(this.elevation(t[a]),i.maxElev),i.minElev=Math.min(this.elevation(t[a]),i.minElev));return i},getCenterOfBounds:function(t){var e=this.getBounds(t),i=e.minLat+(e.maxLat-e.minLat)/2,a=e.minLng+(e.maxLng-e.minLng)/2;return{latitude:parseFloat(i.toFixed(6)),longitude:parseFloat(a.toFixed(6))}},getBoundsOfDistance:function(t,e){var i,a,o=this.latitude(t),s=this.longitude(t),r=o.toRad(),u=s.toRad(),h=e/this.radius,l=r-h,d=r+h,c=this.maxLat.toRad(),g=this.minLat.toRad(),f=this.maxLon.toRad(),m=this.minLon.toRad();if(l>g&&d<c){var p=Math.asin(Math.sin(h)/Math.cos(r));i=u-p,i<m&&(i+=n.PI_X2),a=u+p,a>f&&(a-=n.PI_X2)}else l=Math.max(l,g),d=Math.min(d,c),i=m,a=f;return[{latitude:l.toDeg(),longitude:i.toDeg()},{latitude:d.toDeg(),longitude:a.toDeg()}]},isPointInside:function(t,e){for(var i=!1,a=-1,n=e.length,o=n-1;++a<n;o=a)(this.longitude(e[a])<=this.longitude(t)&&this.longitude(t)<this.longitude(e[o])||this.longitude(e[o])<=this.longitude(t)&&this.longitude(t)<this.longitude(e[a]))&&this.latitude(t)<(this.latitude(e[o])-this.latitude(e[a]))*(this.longitude(t)-this.longitude(e[a]))/(this.longitude(e[o])-this.longitude(e[a]))+this.latitude(e[a])&&(i=!i);return i},preparePolygonForIsPointInsideOptimized:function(t){for(var e=0,i=t.length-1;e<t.length;e++)this.longitude(t[i])===this.longitude(t[e])?(t[e].constant=this.latitude(t[e]),t[e].multiple=0):(t[e].constant=this.latitude(t[e])-this.longitude(t[e])*this.latitude(t[i])/(this.longitude(t[i])-this.longitude(t[e]))+this.longitude(t[e])*this.latitude(t[e])/(this.longitude(t[i])-this.longitude(t[e])),t[e].multiple=(this.latitude(t[i])-this.latitude(t[e]))/(this.longitude(t[i])-this.longitude(t[e]))),i=e},isPointInsideWithPreparedPolygon:function(t,e){for(var i=!1,a=this.longitude(t),n=this.latitude(t),o=0,s=e.length-1;o<e.length;o++)(this.longitude(e[o])<a&&this.longitude(e[s])>=a||this.longitude(e[s])<a&&this.longitude(e[o])>=a)&&(i^=a*e[o].multiple+e[o].constant<n),s=o;return i},isInside:function(){return this.isPointInside.apply(this,arguments)},isPointInCircle:function(t,e,i){return this.getDistance(t,e)<i},withinRadius:function(){return this.isPointInCircle.apply(this,arguments)},getRhumbLineBearing:function(t,e){var i=this.longitude(e).toRad()-this.longitude(t).toRad(),a=Math.log(Math.tan(this.latitude(e).toRad()/2+n.PI_DIV4)/Math.tan(this.latitude(t).toRad()/2+n.PI_DIV4));return Math.abs(i)>Math.PI&&(i=i>0?(n.PI_X2-i)*-1:n.PI_X2+i),(Math.atan2(i,a).toDeg()+360)%360},getBearing:function(t,e){e.latitude=this.latitude(e),e.longitude=this.longitude(e),t.latitude=this.latitude(t),t.longitude=this.longitude(t);var i=(Math.atan2(Math.sin(e.longitude.toRad()-t.longitude.toRad())*Math.cos(e.latitude.toRad()),Math.cos(t.latitude.toRad())*Math.sin(e.latitude.toRad())-Math.sin(t.latitude.toRad())*Math.cos(e.latitude.toRad())*Math.cos(e.longitude.toRad()-t.longitude.toRad())).toDeg()+360)%360;return i},getCompassDirection:function(t,e,i){var a,n;switch(n="circle"==i?this.getBearing(t,e):this.getRhumbLineBearing(t,e),Math.round(n/22.5)){case 1:a={exact:"NNE",rough:"N"};break;case 2:a={exact:"NE",rough:"N"};break;case 3:a={exact:"ENE",rough:"E"};break;case 4:a={exact:"E",rough:"E"};break;case 5:a={exact:"ESE",rough:"E"};break;case 6:a={exact:"SE",rough:"E"};break;case 7:a={exact:"SSE",rough:"S"};break;case 8:a={exact:"S",rough:"S"};break;case 9:a={exact:"SSW",rough:"S"};break;case 10:a={exact:"SW",rough:"S"};break;case 11:a={exact:"WSW",rough:"W"};break;case 12:a={exact:"W",rough:"W"};break;case 13:a={exact:"WNW",rough:"W"};break;case 14:a={exact:"NW",rough:"W"};break;case 15:a={exact:"NNW",rough:"N"};break;default:a={exact:"N",rough:"N"}}return a.bearing=n,a},getDirection:function(t,e,i){return this.getCompassDirection.apply(this,arguments)},orderByDistance:function(t,e){var i=[];for(var a in e){var n=this.getDistance(t,e[a]),o=Object.create(e[a]);o.distance=n,o.key=a,i.push(o)}return i.sort(function(t,e){return t.distance-e.distance})},isPointInLine:function(t,e,i){return(this.getDistance(e,t,1,3)+this.getDistance(t,i,1,3)).toFixed(3)==this.getDistance(e,i,1,3)},isPointNearLine:function(t,e,i,a){return this.getDistanceFromLine(t,e,i)<a},getDistanceFromLine:function(t,e,i){var a=this.getDistance(e,t,1,3),n=this.getDistance(t,i,1,3),o=this.getDistance(e,i,1,3),s=0,r=Math.acos((a*a+o*o-n*n)/(2*a*o)),u=Math.acos((n*n+o*o-a*a)/(2*n*o));return s=r>Math.PI/2?a:u>Math.PI/2?n:Math.sin(r)*a},findNearest:function(t,e,i,a){i=i||0,a=a||1;var n=this.orderByDistance(t,e);return 1===a?n[i]:n.splice(i,a)},getPathLength:function(t){for(var e,i=0,a=0,n=t.length;a<n;++a)e&&(i+=this.getDistance(this.coords(t[a]),e)),e=this.coords(t[a]);return i},getSpeed:function(t,e,i){var a=i&&i.unit||"km";"mph"==a?a="mi":"kmh"==a&&(a="km");var n=o.getDistance(t,e),s=1*e.time/1e3-1*t.time/1e3,r=n/s*3600,u=Math.round(r*this.measures[a]*1e4)/1e4;return u},computeDestinationPoint:function(t,e,i,a){var n=this.latitude(t),o=this.longitude(t);a="undefined"==typeof a?this.radius:Number(a);var s=Number(e)/a,r=Number(i).toRad(),u=Number(n).toRad(),h=Number(o).toRad(),l=Math.asin(Math.sin(u)*Math.cos(s)+Math.cos(u)*Math.sin(s)*Math.cos(r)),d=h+Math.atan2(Math.sin(r)*Math.sin(s)*Math.cos(u),Math.cos(s)-Math.sin(u)*Math.sin(l));return d=(d+3*Math.PI)%(2*Math.PI)-Math.PI,{latitude:l.toDeg(),longitude:d.toDeg()}},convertUnit:function(t,e,i){if(0===e)return 0;if("undefined"==typeof e){if(null===this.distance)throw new Error("No distance was given");if(0===this.distance)return 0;e=this.distance}if(t=t||"m",i=null==i?4:i,"undefined"!=typeof this.measures[t])return this.round(e*this.measures[t],i);throw new Error("Unknown unit for conversion.")},useDecimal:function(t){if("[object Array]"===Object.prototype.toString.call(t)){var e=this;return t=t.map(function(t){if(e.isDecimal(t))return e.useDecimal(t);if("object"==typeof t){if(e.validate(t))return e.coords(t);for(var i in t)t[i]=e.useDecimal(t[i]);return t}return e.isSexagesimal(t)?e.sexagesimal2decimal(t):t})}if("object"==typeof t&&this.validate(t))return this.coords(t);if("object"==typeof t){for(var i in t)t[i]=this.useDecimal(t[i]);return t}if(this.isDecimal(t))return parseFloat(t);if(this.isSexagesimal(t)===!0)return parseFloat(this.sexagesimal2decimal(t));throw new Error("Unknown format.")},decimal2sexagesimal:function(t){if(t in this.sexagesimal)return this.sexagesimal[t];var e=t.toString().split("."),i=Math.abs(e[0]),a=60*("0."+(e[1]||0)),n=a.toString().split(".");return a=Math.floor(a),n=(60*("0."+(n[1]||0))).toFixed(2),this.sexagesimal[t]=i+"° "+a+"' "+n+'"',this.sexagesimal[t]},sexagesimal2decimal:function(t){if(t in this.decimal)return this.decimal[t];var e=new RegExp(this.sexagesimalPattern),i=e.exec(t),a=0,n=0;i&&(a=parseFloat(i[2]/60),n=parseFloat(i[4]/3600)||0);var o=(parseFloat(i[1])+a+n).toFixed(8);return o="S"==i[7]||"W"==i[7]?parseFloat(-o):parseFloat(o),this.decimal[t]=o,o},isDecimal:function(t){return t=t.toString().replace(/\s*/,""),!isNaN(parseFloat(t))&&parseFloat(t)==t},isSexagesimal:function(t){return t=t.toString().replace(/\s*/,""),this.sexagesimalPattern.test(t)},round:function(t,e){var i=Math.pow(10,e);return Math.round(t*i)/i}}),"undefined"!=typeof i&&"undefined"!=typeof i.exports?(i.exports=o,"object"==typeof e&&(e.geolib=o)):"function"==typeof t&&t.amd?t("geolib",[],function(){return o}):e.geolib=o}(this)},{}]},{},[])("geolib")});
// end of Geolib code

// set geolib library as a variable
var geolib = module.exports;

// empty array to hold coordinate objects
var pathCoordinates = [];

// if there is more than 1 item in the repeatable field named "sites", loop through the repeatable objects and push the coordinates into the pathCoordinates array
if ($sites && $sites.length > 1) {
  for (var i = 0; i < $sites.length; ++i) {
    pathCoordinates.push({
      latitude: $sites[i].geometry.coordinates[1],
      longitude: $sites[i].geometry.coordinates[0]
    });
  }

  // get the distance between points in meters
  var distance = geolib.getPathLength(pathCoordinates);

  // set the result of the calc field with a label
  SETRESULT(distance + " meters");
} else {
  SETRESULT(null);
}

Copy and paste the entire code block above into the expression section of your calculation field, making sure to replace $sites with the data name of your repeatable field.