Node to wrap h3-js from https://github.com/uber/h3-js

I'd like to add Uber H3 cells to my node-red-contrib-web-worldmap display. But, I'm not quite sure how to take a JavaScript lib and then wrap it with a flow node. Ideally, what I like to figure out is how to take the lib and then make a few nodes from it. In my mind, I see these nodes are needed.

Wrapper for

  • polygonToCells (c api: polygonToCells) (e.g. take a bounding box and return all of the h3 cells in that box)
    • msg.polygon, msg.level to => msg.payload = [{cell}, {cell}, ...]
    • where polygon = [[lat, lon], ...] or [{lat:?, lon:?}, ...] or both
  • latLngToCell (geoToH3) (e.g. take coordinates and return the h3 cells that contains that point)
    • msg.lat, msg.lon, msg.level to => msg.payload = {cell}
  • H3Index (e.g. take h3 index (as string or 64-bit int and return the h3 cells that contains that point)
    • msg.index => msg.payload = {cell} (index = 64-bit int)
    • msg.string => msg.payload = {cell} (stringToH3 string = hexadecimal string e.g. 8a2aaaa2e747fff)
  • cellToParent (c lib:h3ToParent)
    • msg.payload(cell object), msg.level(default = +1) => msg.payload = {cell}
  • cellToChildren (c lib:h3ToChildren)
    • msg.payload(cell object), msg.level(default = -1) => msg.payload = [{cell},{cell},...]

Where the h3 cell object would have

  • .index = 64-bit Integer
  • .string = String formatted index (c api: h3ToString)
  • .boundary = [point, point, point, point, point, point] (c api: h3ToGeoBoundary)
  • .center = {lat:11, lon:22} (c api: h3ToGeo)
  • .resolution = 7 (c api: h3GetResolution)
  • others?

Where the point object would be

  • {lat:? lon:?} but any points as inputs would accept both [lat,lon] as well as {lat:?, lon:?}

What do you think? Where would I even start?

How do you see this integrating with worldmap for example ? (I don't know anything about h3-js) . Do you have something that gives you h3 "co-ordinates" you want to plot on the map ? or are you trying to draw the hexagons on the map ? or is it the other way round ? you want to take things from the map (like clicks) and convert them into h3 cells ?

I need to show h3 level 6,7,8,9 hex grids when the zoom is appropriate. I plan to use the bounding box to pull the appropriate hex cells.

  • on boundary change
  • when zoom between x and y set level z
  • if new level clear layer
  • get cells for level from boundary
  • diff list (+new -old)
  • format
  • plot

To start with rather than writing a load of nodes I would use the worldmap in node can report the bounds and zoom on every change of view. Feed that to a function node that called the h3-js library (add the library to the setup tab in the function) - and create whatever polylines are required... and then feed those back to the map.

Something like this

[{"id":"15f8b79cf5fd3f8a","type":"worldmap","z":"27d4cd61f8c2d5ed","name":"","lat":"","lon":"","zoom":"","layer":"","cluster":"","maxage":"","usermenu":"show","layers":"show","panit":"false","panlock":"false","zoomlock":"false","hiderightclick":"false","coords":"false","showgrid":"false","showruler":"false","allowFileDrop":"false","path":"/worldmap","overlist":"DR,CO,RA,DN,HM","maplist":"OSMG,OSMC,EsriC,EsriS,EsriT,EsriDG,UKOS","mapname":"","mapurl":"","mapopt":"","mapwms":false,"x":570,"y":60,"wires":[]},{"id":"48fa1be596e9db93","type":"worldmap in","z":"27d4cd61f8c2d5ed","name":"","path":"/worldmap","events":"bounds","x":195,"y":135,"wires":[["c7b326e1ad1e914b","13bdc2149aee0ab3"]]},{"id":"c7b326e1ad1e914b","type":"debug","z":"27d4cd61f8c2d5ed","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":570,"y":135,"wires":[]},{"id":"13bdc2149aee0ab3","type":"function","z":"27d4cd61f8c2d5ed","name":"function 2","func":"\nnode.send({ payload: { \"command\": { \"clear\": \"HEX\" } }})\n\nvar p = msg.payload;\nif (p.zoom < 5) { return; }\n\n// Get the set of hexagons within a polygon\nconst polygon = [\n    [p.south, p.west],\n    [p.north, p.west],\n    [p.north, p.east],\n    [p.south, p.east]\n];\nconst hexagons = h3.polygonToCells(polygon, parseInt(p.zoom - 5));\n// -> ['872830828ffffff', '87283082effffff', ...]\nnode.warn(hexagons)\n\n\nvar allhex = hexagons.map(x => {\n    const hex = h3.cellToBoundary(x);\n    hex.push(hex[0]);\n    return hex;\n})\n\nnode.warn(allhex);\n\nallhex.forEach((x,i) => {\n    const p =  {\n        name: \"Hex\"+i,\n        layer: \"HEX\",\n        line: x\n    };\n    node.send({payload:p});\n})\nreturn null;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"h3","module":"h3-js"}],"x":375,"y":105,"wires":[["15f8b79cf5fd3f8a"]]}]

Feed that to a function node that called the h3-js library (add the library to the setup tab in the function)

I don't understand how you "add the library to the setup tab in the function". Whatever you did it let you use h3 in the main scope of your function. I'm not a programmer but an engineer with a lot or Perl experience so all of this JavaScript is new to me.

I initially got this error

"ReferenceError: h3 is not defined (line 14, col 18)"

After googling a bunch all I could ever figure out was to add the h3 to the global variables not to the main function scope like you did.

What I did was to add h3:require('h3-js') to the setting.js file under the functionGlobalContext section and then let h3 = global.get("h3"); in the function. But, your solution (whatever it was) is so much more elegant. I wish I could figure that out. But mine solution works too!

./settings.js    functionGlobalContext: {
./settings.js        h3:require('h3-js')

Recap:

  1. Install the h3-js lib under node-red root installation
[root@server .node-red]# pwd
/root/.node-red
[root@server .node-red]# npm install h3-js
added 1 package, and audited 411 packages in 11s
  1. Edited the setings.js file to add the h3:require('h3-js')
[root@server .node-red]# vi ./settings.js
[root@server .node-red]# grep -B 4 -A 1 h3 ./settings.js
    functionGlobalContext: {
        // os:require('os'),
        // jfive:require("johnny-five"),
        // j5board:require("johnny-five").Board({repl:false})
        h3:require('h3-js')
    },
  1. In the function node to pull h3 into my namespace (I needed to but you did not).
let h3 = global.get("h3");
  1. The results before any tuning

After a bit of refactoring and to be more my style, this is what I have come up with for h3.polygonToCells and h3.cellToChildren

image

Flow

[{"id":"79fb6c16.c8d374","type":"change","z":"e02f29e1.67d3d8","name":"polygon/resolution","rules":[{"t":"set","p":"payload.polygon","pt":"msg","to":"[\t  [payload.south, payload.west],\t  [payload.north, payload.west],\t  [payload.north, payload.east],\t  [payload.south, payload.east],\t  [payload.south, payload.west]\t]","tot":"jsonata"},{"t":"set","p":"payload.resolution","pt":"msg","to":"8","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":1030,"y":380,"wires":[["3c794efe.320c22","1289562b.53082a"]]},{"id":"3c794efe.320c22","type":"function","z":"e02f29e1.67d3d8","name":"h3.polygonToCells","func":"let h3         = global.get(\"h3\");\nvar resolution = msg.payload.resolution;\nvar polygon    = msg.payload.polygon\nvar strings    = h3.polygonToCells(polygon, resolution);\nmsg.payload    = []; //isa [{string:str, boundry:[[lat,lon], ...]}, ...]\n\nstrings.forEach((string) => {\n  var boundry = h3.cellToBoundary(string); //isa [[], [], ...]\n  boundry.push(boundry[0]); // OGC closed linear ring\n  var cell    = {\n                 \"string\":     string, \n                 \"resolution\": resolution,\n                 \"boundry\":    boundry,\n                };\n  msg.payload.push(cell);\n});\n\nreturn msg;","outputs":1,"noerr":0,"x":1250,"y":380,"wires":[["d9370a1d.c94488"]]},{"id":"d9370a1d.c94488","type":"split","z":"e02f29e1.67d3d8","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":1430,"y":380,"wires":[["5c806c3d.f9f4c4","2e52a07d.6a9c5"]]},{"id":"2e52a07d.6a9c5","type":"function","z":"e02f29e1.67d3d8","name":"h3.cellToChildren","func":"let h3         = global.get(\"h3\");\nvar cell       = msg.payload;\nvar resolution = cell.resolution + 1; //TODO: allow override\n//node.warn(cell.string);\n//node.warn(resolution);\n\nvar strings    = h3.cellToChildren(cell.string, resolution);\n//node.warn(strings);\n\nmsg.payload    = []; //isa [{string:str, boundry:[[lat,lon], ...]}, ...]\n\nstrings.forEach((string) => {\n  var boundry = h3.cellToBoundary(string); //isa [[], [], ...]\n  boundry.push(boundry[0]); // OGC closed linear ring\n  var cell    = {\n                 \"string\":     string, \n                 \"resolution\": resolution,\n                 \"boundry\":    boundry,\n                };\n  msg.payload.push(cell);\n});\n\nreturn msg;","outputs":1,"noerr":0,"x":1610,"y":340,"wires":[["f904cb0d.2228a8"]]},{"id":"f904cb0d.2228a8","type":"split","z":"e02f29e1.67d3d8","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":1790,"y":340,"wires":[["90e56f86.a1f5"]]},{"id":"90e56f86.a1f5","type":"change","z":"e02f29e1.67d3d8","name":"color","rules":[{"t":"set","p":"cell","pt":"msg","to":"payload","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"{}","tot":"json"},{"t":"set","p":"payload.weight","pt":"msg","to":"1","tot":"num"},{"t":"set","p":"payload.color","pt":"msg","to":"#CCCCCC","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1930,"y":340,"wires":[["580b1557.f6291c"]]},{"id":"5c806c3d.f9f4c4","type":"change","z":"e02f29e1.67d3d8","name":"color","rules":[{"t":"set","p":"cell","pt":"msg","to":"payload","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"{}","tot":"json"},{"t":"set","p":"payload.weight","pt":"msg","to":"2","tot":"num"},{"t":"set","p":"payload.color","pt":"msg","to":"#BBBBBB","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1930,"y":380,"wires":[["580b1557.f6291c"]]},{"id":"580b1557.f6291c","type":"change","z":"e02f29e1.67d3d8","name":"format","rules":[{"t":"set","p":"payload.layer","pt":"msg","to":"hex","tot":"str"},{"t":"set","p":"payload.name","pt":"msg","to":"cell.string","tot":"msg"},{"t":"set","p":"payload.line","pt":"msg","to":"cell.boundry","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":2070,"y":360,"wires":[["ac5a14e6.2859f8"]]},{"id":"ac5a14e6.2859f8","type":"change","z":"e02f29e1.67d3d8","name":"","rules":[],"action":"","property":"","from":"","to":"","reg":false,"x":2195,"y":440,"wires":[["4c9dc44a.908fdc"]],"l":false},{"id":"4c9dc44a.908fdc","type":"ui_worldmap","z":"e02f29e1.67d3d8","group":"b216f4f.7d69c08","order":1,"width":"20","height":"10","name":"","lat":"38.2","lon":"-97.2","zoom":"4.5","layer":"","cluster":"","maxage":"","usermenu":"hide","layers":"show","panit":"false","panlock":"false","zoomlock":"false","hiderightclick":"true","coords":"deg","showgrid":"false","allowFileDrop":"false","path":"/worldmap","overlist":"","maplist":"OSMG,OSMC,EsriC,EsriS,EsriT,EsriDG","mapname":"","mapurl":"","mapopt":"","mapwms":false,"x":2340,"y":260,"wires":[]},{"id":"9b2015b6.65a058","type":"worldmap in","z":"e02f29e1.67d3d8","name":"worldmap boundary","path":"/worldmap","events":"bounds","x":110,"y":260,"wires":[["9470a517.f4e368","f53a4b1b.56d988"]]},{"id":"f53a4b1b.56d988","type":"trigger","z":"e02f29e1.67d3d8","op1":"","op2":"","op1type":"nul","op2type":"payl","duration":"500","extend":true,"units":"ms","reset":"","bytopic":"all","name":"","x":420,"y":260,"wires":[["3a7daa20.4a5456"]]},{"id":"3a7daa20.4a5456","type":"change","z":"e02f29e1.67d3d8","name":"set center","rules":[{"t":"set","p":"payload.center","pt":"msg","to":"{}","tot":"json"},{"t":"set","p":"payload.center.lat","pt":"msg","to":"$average([payload.north, payload.south])\t","tot":"jsonata"},{"t":"set","p":"payload.center.lon","pt":"msg","to":"$average([payload.east, payload.west])\t","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":600,"y":260,"wires":[["495f29b2.e5e508","9ed54ec.ce141b"]]},{"id":"495f29b2.e5e508","type":"switch","z":"e02f29e1.67d3d8","name":"zoom >= 15.5","property":"payload.zoom","propertyType":"msg","rules":[{"t":"gte","v":"15.5","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":780,"y":260,"wires":[["d306486c.fc1f58","28ef495f.224f26","800fae11.27ece","79fb6c16.c8d374"],["65916287.60cedc","fe6930f5.e980f"]]},{"id":"b216f4f.7d69c08","type":"ui_group","z":"","name":"World Map","tab":"a4e26d94.4446","order":1,"disp":true,"width":"20","collapse":false},{"id":"a4e26d94.4446","type":"ui_tab","z":"","name":"World Map","icon":"dashboard","order":6,"disabled":false,"hidden":false}]

Event Summary:

  1. on map change of bounding box
  2. format bbox as polygon (OGC closed ring)
  3. call polygonToCells at level 8 which returns the cell string and boundary
  4. split return to process each cell
  5. get all children for each cell
  6. format objects for worldmap
1 Like