Contrib-web-worldmap - offline help

Hi, first a big thank you to all that have contributed to this fantastic node.

I have built a tracker for our over adventurous rescue lurcher to give us some piece of mind when walking in remote areas in the UK where there may be no or patchy mobile coverage.

Setup
The display and node-red host is an old mobile phone (android 10) without internet access. Connected via an OTG micro USB to micro USB cable I have an Adafruit Feather M0 RFM69HCW 433MHz packet Radio.

The Feather is programmed to output the tracker data as name:"map_unique_marker_id",lat:<deg. latitude>,lon:<deg. longitude>,label:"My Dogs Name"\r\n once a second.

The flow

[{"id":"946a67e51f2c3165","type":"tab","label":"Flow 1","disabled":false,"info":"","env":[]},{"id":"2f44ef68491c2556","type":"ui_worldmap","z":"946a67e51f2c3165","group":"e386b85f061e29ad","order":0,"width":0,"height":0,"name":"","lat":"","lon":"","zoom":"18","layer":"OSMC","cluster":"","maxage":"","usermenu":"show","layers":"show","panit":"true","panlock":"false","zoomlock":"false","hiderightclick":"true","coords":"deg","showgrid":"true","allowFileDrop":"true","path":"/worldmap","overlist":"DR,CO,RA,DN,HM","maplist":"OSMG,OSMC,EsriC,EsriS,EsriT,EsriDG,UKOS,OS45,OpTop,SW","mapname":"","mapurl":"","mapopt":"","mapwms":false,"x":720,"y":120,"wires":[]},{"id":"96a9c9cc19f9c075","type":"udp in","z":"946a67e51f2c3165","name":"","iface":"","port":"2947","ipv":"udp4","multicast":"false","group":"","datatype":"utf8","x":220,"y":80,"wires":[["c8e713d5d0560962"]]},{"id":"f2ded905a2c7dbe2","type":"exec","z":"946a67e51f2c3165","command":"termux-usb","addpay":"payload","append":"","useSpawn":"false","timer":"1","winHide":false,"oldrc":false,"name":"","x":490,"y":280,"wires":[["5fd91a18ad5bcb84","ac72e9ce32272928"],["acfe7724118dd9b6"],["acfe7724118dd9b6"]]},{"id":"0febfe6cf212344e","type":"inject","z":"946a67e51f2c3165","name":"-e usbtest ","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":true,"onceDelay":"0.3","topic":"","payload":"-e ../home/usbtest /dev/bus/usb/002/002","payloadType":"str","x":130,"y":340,"wires":[["983377e2f4b521b3"]]},{"id":"c535299196141b9f","type":"inject","z":"946a67e51f2c3165","name":"Request Access","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"2","crontab":"","once":true,"onceDelay":"0.2","topic":"","payload":"-r /dev/bus/usb/002/002","payloadType":"str","x":150,"y":260,"wires":[["4e893cb8670f307a"]]},{"id":"9217822122ea6753","type":"function","z":"946a67e51f2c3165","name":"nameLatLonLabel","func":"var topic = String(msg.topic).replace(/(\\r\\n|\\n|\\n\\r|\\.)/gm, \"\");\nvar status = flow.get(node.name) || {\n    Phone:{state:undefined,\n        name:undefined,lat:undefined,lon:undefined,\n        label:undefined},\n    Tracker:{state:undefined,\n        name:undefined,lat:undefined,lon:undefined,\n        label:undefined}};\n\nswitch(topic){\n    case \"gpsd\":\n        var fields = String(msg.payload).split(\",\");\n        var latI=-1, lonI=-1,latA=-1, lonA=-1;\n        switch (fields[0]) {\n            case \"$GPGGA\":\n                latI = 2; latA = 3;\n                lonI = 4; lonA = 5;\n                break;\n            case \"$GPRMC\":\n                latI = 3; latA = 4;\n                lonI = 5; lonA = 6;\n                break;\n            default:\n                break;\n        }\n        if( latI > 0 && lonI > 0 && latA > 0 && lonA > 0 &&\n            fields[latI].length === 9 &&\n            fields[lonI].length === 10 &&\n            fields[latA].length === 1 &&\n            fields[lonA].length === 1 ) {\n            var latS = fields[latA] === \"N\" ? 1 : -1,\n                lonS = fields[lonA] === \"E\" ? 1 : -1,\n                latD = precisionRound(fields[latI]/100,0),\n                lonD = precisionRound(fields[lonI]/100,0),\n                latM = precisionRound((fields[latI]-100*latD)/60,6),\n                lonM = precisionRound((fields[lonI]-100*lonD)/60,6);        \n\n            status.Phone.state = \"GPS Lock\";\n            status.Phone.name = \"Me\";\n            status.Phone.lat = latS*(latD + latM);\n            status.Phone.lon = lonS*(lonD + lonM);\n            status.Phone.label = \"Me\";\n        }else{\n            status.Phone.state=\"GPS No Lock\";\n            status.Phone.name = \"Me\";\n            status.Phone.label = undefined;\n        }\n        break;\n    case \"initialise\":\n    case \"Access denied\":\n        status.Tracker.name = \"brock\";\n        status.Tracker.lat = undefined;\n        status.Tracker.lon = undefined;\n        status.Tracker.label = undefined;\n    case \"Access granted\":\n        status.Tracker.state = topic;\n        break;\n    default:\n        /* Tracker radio response */\n        var kv = String(msg.payload).split(\",\"); \n        //name:brock,lat:55.079239,lon:-1.469798,label:Brock\n        status.Tracker.name = String(kv[0]).split(\":\")[1];\n        status.Tracker.lat = String(kv[1]).split(\":\")[1];\n        status.Tracker.lon = String(kv[2]).split(\":\")[1];\n        status.Tracker.label = String(String(kv[3]).split(\":\")[1]).replace(/(\\r\\n|\\n|\\r)/gm, \"\");\n        if(status.name == undefined)\n            status.Tracker.state = \"Disconnected\";\n        break;\n}\nflow.set(node.name,status);\nif(status.Tracker.state !== \"Access granted\" || status.Tracker.label == undefined){\n    // show tracker \n    node.send({payload:{deleted:true,name:\"brock\"}});\n}else{\n    // delete tracker \n    node.send({payload:{name:status.Tracker.name,lat:status.Tracker.lat,lon:status.Tracker.lon,label:status.Tracker.label}});\n}\nif(status.Phone.state !== \"GPS Lock\" || status.Phone.label == undefined){\n    // delete me\n    node.send({payload:{deleted:true,name:\"Me\"}});\n}else{\n    // show me and center map on me\n    node.send({payload:{name:status.Phone.name,lat:status.Phone.lat,lon:status.Phone.lon,label:\"Me\"}});\n    node.send({payload:{command:{lat:status.Phone.lat,lon:status.Phone.lon}}});\n}\nreturn;\n\nfunction precisionRound(number, precision) {\n    var factor = Math.pow(10, precision);\n    return Math.round(number * factor) / factor;\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":120,"wires":[["2f44ef68491c2556"]]},{"id":"ca766bcc680dc53c","type":"inject","z":"946a67e51f2c3165","name":"Init","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"initialise","x":110,"y":120,"wires":[["9217822122ea6753"]]},{"id":"5fd91a18ad5bcb84","type":"change","z":"946a67e51f2c3165","name":"","rules":[{"t":"set","p":"topic","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":200,"wires":[["9217822122ea6753","f8284af1cde3779a"]]},{"id":"983377e2f4b521b3","type":"switch","z":"946a67e51f2c3165","name":"","property":"nameLatLonLabel.Tracker.state","propertyType":"flow","rules":[{"t":"eq","v":"Access granted","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":310,"y":340,"wires":[["f2ded905a2c7dbe2","582b3d0614488c55"]]},{"id":"4e893cb8670f307a","type":"switch","z":"946a67e51f2c3165","name":"","property":"nameLatLonLabel.Tracker.state","propertyType":"flow","rules":[{"t":"neq","v":"Access granted","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":310,"y":260,"wires":[["f2ded905a2c7dbe2","7e4697d6140d29ee"]]},{"id":"c8e713d5d0560962","type":"change","z":"946a67e51f2c3165","name":"t = gpsd","rules":[{"t":"set","p":"topic","pt":"msg","to":"gpsd","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":80,"wires":[["9217822122ea6753"]]},{"id":"52cd7a4d39068189","type":"comment","z":"946a67e51f2c3165","name":"Phone GPSd In","info":"","x":320,"y":40,"wires":[]},{"id":"10f44e113352a1fc","type":"comment","z":"946a67e51f2c3165","name":"termux-usb usbtest.c GPS In","info":"","x":280,"y":160,"wires":[]},{"id":"7e4697d6140d29ee","type":"debug","z":"946a67e51f2c3165","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":310,"y":300,"wires":[]},{"id":"582b3d0614488c55","type":"debug","z":"946a67e51f2c3165","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":310,"y":380,"wires":[]},{"id":"ac72e9ce32272928","type":"debug","z":"946a67e51f2c3165","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":670,"y":260,"wires":[]},{"id":"acfe7724118dd9b6","type":"debug","z":"946a67e51f2c3165","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":670,"y":300,"wires":[]},{"id":"f8284af1cde3779a","type":"debug","z":"946a67e51f2c3165","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":670,"y":200,"wires":[]},{"id":"6c88f17532727b1f","type":"inject","z":"946a67e51f2c3165","name":"Init","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"{\"name\":\"Dog\",\"lat\":\"55.200000\",\"lon\":\"-2.400000\",\"label\":\"Dog\"}","payloadType":"json","x":530,"y":40,"wires":[["2f44ef68491c2556"]]},{"id":"1058566bd72863bc","type":"inject","z":"946a67e51f2c3165","name":"Init","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.2","topic":"","payload":"{\"name\":\"Phone\",\"lat\":\"55.205000\",\"lon\":\"-2.405000\",\"label\":\"Phone\"}","payloadType":"json","x":530,"y":80,"wires":[["2f44ef68491c2556"]]},{"id":"e386b85f061e29ad","type":"ui_group","name":"Home","tab":"f80eb58ffa262d83","order":1,"disp":true,"width":"6","collapse":true,"className":""},{"id":"f80eb58ffa262d83","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]

shows a marker for the phone GPDd position and a second marker for the dog.

I have searched for but not found an opensource offline mapserver for android.

Also for our use case an offline map server seems an unnecessary complication considering we would know in advance where we would be walking and a map 10mile x 10Mile would be more than sufficient.

I have just three questions;

  1. Could anyone please tell me if a single pre-downloaded map file covering an area of 10mile x 10mile would contain the same detail when fully zoomed in as an online viewed version ?
  2. Could anyone please tell me if it is possible to just patch the node-red-config-web-worldmap node some how so it points to a single offline leaflet in local storage ?
  3. if 2 then any idea how and where to look ?

many thanks in advance,
oz

P.S. The biggest challenge so far has been that of getting data into node-red on android. I could not find a working usb/bluetooth LE node so had to resort to physical OTG cable and writing my own libusb serial driver.

A perfect solution would be to build into node-red-contrib-web-worldmap an option to save the current online map to local storage while traversing the extents of a target area. Then if internet access is unavailable switch to the saved maps in a transparent manner.

Unfortunately having looked at the source I have not been able to figure out how to do this.

Any pointers would be very appreciated.

    • probably not - but depends on your device memory and if you can find a map with high enough resolution... but a vector based map will likely always be better.
  1. The worldmap does support either geotiff encoded images or images with a set of bounds. The size/resolution of which is down to you and the amount of memory you have available - You would need to have some buttons to turn them on and off as there is no simple way to auto select which is shown at any particular zoom level. (or you could use a worldmap in node events to swap them.

  2. See the readme.

Often the browser will cache previous images - but you have no (or little) control over what remains and if you don't happen to have the zoom of the area you want you would be out of luck.

There are no plans to turn the map display into a map server. There are lots of them "out there". The usual way I do it is to use a docker container - (see worldmap readme) - however I've no idea how easy they would be run on android. All of which would be rather complicated (to me at least).

Actually I'm being an idiot... there is the ATAK app https://play.google.com/store/apps/details?id=com.atakmap.app.civ&hl=en_GB&gl=US
if your device supports it that lets you select an area and download a local cache...

However it normally talks to a server (tak server) to get information, or can accept some local inputs via plugins or direct via network. I have used the server method (eg see node-red-contrib-tak-registration - npm :slight_smile: ) but I haven't tried to insert other device info (eg from dog) locally... but it would probably be a good way forward.

Hi @dceejay thank you very much for your help.

I am aware that markers can be included in the url to open street map using &mlat=yy.yyyyyy&mlon=x.xxxxxx and they appear in the returned image.

  1. Do you know the format of returned data and is the rendering done on the phone ?

I did a test with contrib-web-worldmap (CWW) whereby after a map had been displayed I disconnected the internet and was still able to pan and zoom the map up until I sent a new marker position to the CWW node and of course the map disappeared because a request was sent to an unreachable server but the interesting thing was that the marker still rendered in the correct place.

It occurred to me that if I request a map with out markers and save it then re supply the c-w-w node with the data then the markers should show on the map and I should be able to zoom and pan.

  1. Do you think I could create a proxy server in NR and point CWW at it so that when there is internet I pass the request through to OSM and save the response and relay the response to CWW ?

I could probably get away with a 5mile x 5mile map and it would be no problem if the returned data from a CWW request was in vector format.

What do you think ?

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.