@Tico
instead of 3 separate change nodes, use one node to set lat
, lon
and elv
from your iDevice - that way, instead of 3 separate messages with 3 separate values, you have all values in one place (then you can build the to
coords). Look in the "prepare to/from coords" for how I put them together.

demo flow
[{"id":"8035c186.ff4e3","type":"inject","z":"dd03fa8d.2f4be8","name":"{\"from\":{\"lat\":51,\"lon\":0.1,\"elv\":0},\"to\":{\"lat\":50.99,\"lon\":0.11,\"elv\":12}}","props":[{"p":"payload"},{"p":"topic","vt":"str"},{"p":"to","v":"","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"from\":{\"lat\":51,\"lon\":0.1,\"elv\":0},\"to\":{\"lat\":50.99,\"lon\":0.11,\"elv\":12}}","payloadType":"json","x":500,"y":900,"wires":[["a3a3aa25.e2e358"]]},{"id":"a3a3aa25.e2e358","type":"function","z":"dd03fa8d.2f4be8","name":"calculate distance between 2 coords","func":"//Credit: https://javascript.plainenglish.io/calculating-azimuth-distance-and-altitude-from-a-pair-of-gps-locations-36b4325d8ab0\n\nfunction ParseAngle(ang, limit) {\n var angle = parseFloat(ang);\n if (isNaN(angle) || (angle < -limit) || (angle > limit)) {\n node.error(\"Invalid angle value.\", msg);\n return null;\n } else {\n return angle;\n }\n}\n\nfunction ParseElevation(el) {\n var angle = parseFloat(el);\n if (isNaN(angle)) {\n node.error(\"Invalid elevation value.\", msg);\n return null;\n } else {\n return angle;\n }\n}\n\nfunction ParseLocation(pos) {\n var lat = ParseAngle(pos.lat, 90.0);\n var location = null;\n if (lat != null) {\n var lon = ParseAngle(pos.lon, 180.0);\n if (lon != null) {\n var elv = ParseElevation(pos.elv);\n if (elv != null) {\n location = { lat: lat, lon: lon, elv: elv };\n }\n }\n }\n return location;\n}\n\nfunction EarthRadiusInMeters(latitudeRadians) {\n // latitudeRadians is geodetic, i.e. that reported by GPS.\n // http://en.wikipedia.org/wiki/Earth_radius\n var a = 6378137.0; // equatorial radius in meters\n var b = 6356752.3; // polar radius in meters\n var cos = Math.cos(latitudeRadians);\n var sin = Math.sin(latitudeRadians);\n var t1 = a * a * cos;\n var t2 = b * b * sin;\n var t3 = a * cos;\n var t4 = b * sin;\n return Math.sqrt((t1 * t1 + t2 * t2) / (t3 * t3 + t4 * t4));\n}\n\nfunction GeocentricLatitude(lat) {\n // Convert geodetic latitude 'lat' to a geocentric latitude 'clat'.\n // Geodetic latitude is the latitude as given by GPS.\n // Geocentric latitude is the angle measured from center of Earth between a point and the equator.\n // https://en.wikipedia.org/wiki/Latitude#Geocentric_latitude\n var e2 = 0.00669437999014;\n var clat = Math.atan((1.0 - e2) * Math.tan(lat));\n return clat;\n}\n\nfunction LocationToPoint(c) {\n // Convert (lat, lon, elv) to (x, y, z).\n var lat = c.lat * Math.PI / 180.0;\n var lon = c.lon * Math.PI / 180.0;\n var radius = EarthRadiusInMeters(lat);\n var clat = GeocentricLatitude(lat);\n\n var cosLon = Math.cos(lon);\n var sinLon = Math.sin(lon);\n var cosLat = Math.cos(clat);\n var sinLat = Math.sin(clat);\n var x = radius * cosLon * cosLat;\n var y = radius * sinLon * cosLat;\n var z = radius * sinLat;\n\n // We used geocentric latitude to calculate (x,y,z) on the Earth's ellipsoid.\n // Now we use geodetic latitude to calculate normal vector from the surface, to correct for elevation.\n var cosGlat = Math.cos(lat);\n var sinGlat = Math.sin(lat);\n\n var nx = cosGlat * cosLon;\n var ny = cosGlat * sinLon;\n var nz = sinGlat;\n\n x += c.elv * nx;\n y += c.elv * ny;\n z += c.elv * nz;\n\n return { x: x, y: y, z: z, radius: radius, nx: nx, ny: ny, nz: nz };\n}\n\nfunction Distance(ap, bp) {\n var dx = ap.x - bp.x;\n var dy = ap.y - bp.y;\n var dz = ap.z - bp.z;\n return Math.sqrt(dx * dx + dy * dy + dz * dz);\n}\n\nfunction RotateGlobe(b, a, bradius, aradius) {\n // Get modified coordinates of 'b' by rotating the globe so that 'a' is at lat=0, lon=0.\n var br = { lat: b.lat, lon: (b.lon - a.lon), elv: b.elv };\n var brp = LocationToPoint(br);\n\n // Rotate brp cartesian coordinates around the z-axis by a.lon degrees,\n // then around the y-axis by a.lat degrees.\n // Though we are decreasing by a.lat degrees, as seen above the y-axis,\n // this is a positive (counterclockwise) rotation (if B's longitude is east of A's).\n // However, from this point of view the x-axis is pointing left.\n // So we will look the other way making the x-axis pointing right, the z-axis\n // pointing up, and the rotation treated as negative.\n\n var alat = GeocentricLatitude(-a.lat * Math.PI / 180.0);\n var acos = Math.cos(alat);\n var asin = Math.sin(alat);\n\n var bx = (brp.x * acos) - (brp.z * asin);\n var by = brp.y;\n var bz = (brp.x * asin) + (brp.z * acos);\n\n return { x: bx, y: by, z: bz, radius: bradius };\n}\n\nfunction NormalizeVectorDiff(b, a) {\n // Calculate norm(b-a), where norm divides a vector by its length to produce a unit vector.\n var dx = b.x - a.x;\n var dy = b.y - a.y;\n var dz = b.z - a.z;\n var dist2 = dx * dx + dy * dy + dz * dz;\n if (dist2 == 0) {\n return null;\n }\n var dist = Math.sqrt(dist2);\n return { x: (dx / dist), y: (dy / dist), z: (dz / dist), radius: 1.0 };\n}\n\nfunction Calculate(pos1, pos2) {\n \n var result = {\n from: pos1,\n to: pos2,\n };\n\n var a = ParseLocation(pos1);\n if (a != null) {\n var b = ParseLocation(pos2);\n if (b != null) {\n var ap = LocationToPoint(a);\n var bp = LocationToPoint(b);\n var distKm = 0.001 * Distance(ap, bp);\n result.distanceKm = distKm;\n\n // Let's use a trick to calculate azimuth:\n // Rotate the globe so that point A looks like latitude 0, longitude 0.\n // We keep the actual radii calculated based on the oblate geoid,\n // but use angles based on subtraction.\n // Point A will be at x=radius, y=0, z=0.\n // Vector difference B-A will have dz = N/S component, dy = E/W component.\n var br = RotateGlobe(b, a, bp.radius, ap.radius);\n if (br.z * br.z + br.y * br.y > 1.0e-6) {\n var theta = Math.atan2(br.z, br.y) * 180.0 / Math.PI;\n var azimuth = 90.0 - theta;\n if (azimuth < 0.0) {\n azimuth += 360.0;\n }\n if (azimuth > 360.0) {\n azimuth -= 360.0;\n }\n result.azimuth = azimuth;\n }\n\n var bma = NormalizeVectorDiff(bp, ap);\n if (bma != null) {\n // Calculate altitude, which is the angle above the horizon of B as seen from A.\n // Almost always, B will actually be below the horizon, so the altitude will be negative.\n // The dot product of bma and norm = cos(zenith_angle), and zenith_angle = (90 deg) - altitude.\n // So altitude = 90 - acos(dotprod).\n var altitude = 90.0 - (180.0 / Math.PI) * Math.acos(bma.x * ap.nx + bma.y * ap.ny + bma.z * ap.nz);\n result.altitude = altitude;\n }\n }\n }\n return result;\n}\nmsg.payload = Calculate(msg.payload.from, msg.payload.to);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":930,"y":840,"wires":[["9bec75c7.82ec08"]]},{"id":"9bec75c7.82ec08","type":"debug","z":"dd03fa8d.2f4be8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":990,"y":880,"wires":[]},{"id":"8daeb3b9.878cd","type":"inject","z":"dd03fa8d.2f4be8","name":"{\"from\":{\"lat\":50,\"lon\":1,\"elv\":0},\"to\":{\"lat\":50,\"lon\":1,\"elv\":12}}","props":[{"p":"payload"},{"p":"topic","vt":"str"},{"p":"to","v":"","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"from\":{\"lat\":50,\"lon\":1,\"elv\":0},\"to\":{\"lat\":50,\"lon\":1,\"elv\":12}}","payloadType":"json","x":480,"y":780,"wires":[["a3a3aa25.e2e358"]]},{"id":"4d1fac3d.f00ec4","type":"inject","z":"dd03fa8d.2f4be8","name":"{\"from\":{\"lat\":50,\"lon\":0,\"elv\":0},\"to\":{\"lat\":50,\"lon\":1,\"elv\":0}}","props":[{"p":"payload"},{"p":"topic","vt":"str"},{"p":"to","v":"","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"from\":{\"lat\":50,\"lon\":0,\"elv\":0},\"to\":{\"lat\":50,\"lon\":1,\"elv\":0}}","payloadType":"json","x":470,"y":820,"wires":[["a3a3aa25.e2e358"]]},{"id":"e9ddbe09.e734f","type":"inject","z":"dd03fa8d.2f4be8","name":"{\"from\":{\"lat\":50,\"lon\":0,\"elv\":0},\"to\":{\"lat\":51,\"lon\":0,\"elv\":0}}","props":[{"p":"payload"},{"p":"topic","vt":"str"},{"p":"to","v":"","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"from\":{\"lat\":50,\"lon\":0,\"elv\":0},\"to\":{\"lat\":51,\"lon\":0,\"elv\":0}}","payloadType":"json","x":470,"y":860,"wires":[["a3a3aa25.e2e358"]]},{"id":"734fe766.dd6de8","type":"function","z":"dd03fa8d.2f4be8","name":"(fake) My iDevice","func":"msg.payload = {\n iPad: [-31.9, 115.8, 0, 0]\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":750,"y":700,"wires":[["f3a2052b.cca9a8"]]},{"id":"e8b119ae.f9fbb8","type":"inject","z":"dd03fa8d.2f4be8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":540,"y":700,"wires":[["734fe766.dd6de8"]]},{"id":"f3a2052b.cca9a8","type":"function","z":"dd03fa8d.2f4be8","name":"prepare to/from coords","func":"//home position\nvar from = {\n lat: -31.89,\n lon: 115.87,\n elv: 0,\n}\n\n//ipad pos\nvar iPad = msg.payload.iPad;\nvar to = {\n lat: iPad[0],\n lon: iPad[1],\n elv: iPad[2],\n}\n\n\nmsg.payload = {\n from: from,\n to: to\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1020,"y":700,"wires":[["a3a3aa25.e2e358"]]}]