├── .gitignore ├── .npmignore ├── README.md ├── bin └── presenter-cli.js ├── examples ├── doc.xml ├── docs │ └── images │ │ ├── charts.png │ │ └── map.png ├── images │ ├── charts.png │ └── map.png ├── rich.xml └── sample.xml ├── lib ├── document-viewer.js └── ter-u12n.json ├── package.json └── server ├── package.json ├── presenter.js ├── server.js └── test └── test.txt /.gitignore: -------------------------------------------------------------------------------- 1 | p.js 2 | node_modules 3 | p.txt 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /server 2 | /test 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WOPR 2 | 3 | WOPR is a simple markup language for creating [rich terminal reports](https://github.com/yaronn/blessed-contrib), presentations and infographics. 4 | 5 | Put [a report](https://raw.githubusercontent.com/yaronn/wopr/master/examples/sample.xml) on the web (e.g. gist) and view it via curl: 6 | 7 | `````bash 8 | $> curl -N tty.zone/\[0-2\]\?auto\&cols=$((COLUMNS)) 9 | ````` 10 | (If you experience firewall issues replace tty.zone with ec2-23-21-64-152.compute-1.amazonaws.com or use a [local viewer](https://github.com/yaronn/wopr#viewing-reports)) 11 | 12 | Created by Yaron Naveh ([@YaronNaveh](https://twitter.com/YaronNaveh)) 13 | 14 | ![](./examples/images/charts.png "term") 15 | ![](./examples/images/map.png "term") 16 | 17 | ##Writing your first terminal report## 18 | 19 | Here is a simple report with a bar chart: 20 | 21 | `````xml 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ````` 30 | 31 | You have 3 options to view this report: 32 | 33 | **Option 1: POST it to the wopr online viewer** 34 | 35 | `````bash 36 | $> curl --data '' tty.zone\?cols=$((COLUMNS)) 37 | ````` 38 | 39 | If you experience firewall issues replace tty.zone with ec2-23-21-64-152.compute-1.amazonaws.com. 40 | 41 | **Note:** The online viewer is a reference implementation. Do not send it secret data but rather create [your own](https://github.com/yaronn/wopr/tree/master/server). 42 | 43 | **Option 2: POST it from external url** 44 | 45 | Save the report content in some url (e.g. gist) and then: 46 | 47 | `````bash 48 | $> a=$(curl -s https://gist.githubusercontent.com/yaronn/e6eec6d0e7adac63c83f/raw/50aca544d26a32aa189e790635c8679067017948/gistfile1.xml); curl --data "$a" tty.zone\?cols=$((COLUMNS)) 49 | ````` 50 | 51 | (note you need the gist raw url) 52 | 53 | If you experience firewall issues replace tty.zone with ec2-23-21-64-152.compute-1.amazonaws.com. 54 | 55 | **Note:** The online viewer is a reference implementation. Do not send it secret data but rather create [your own](https://github.com/yaronn/wopr/tree/master/server). 56 | 57 | 58 | **Option 3: Via the local viewer** 59 | 60 | Save the report xml to report.xml and then: 61 | 62 | `````bash 63 | $> npm install -g wopr 64 | $> wopr report.xml 65 | ````` 66 | 67 | Note the local viewer does not send anything online and does not require network. 68 | 69 | ![](./examples/images/charts.png "term") 70 | 71 | ##Markup Basics# 72 | 73 | **Pages** 74 | 75 | A document is a set of pages: 76 | 77 | `````xml 78 | 79 | 80 | ... 81 | 82 | 83 | ... 84 | 85 | 86 | ````` 87 | 88 | **Layout** 89 | 90 | A page is a 12x12 grid in which you can position different widgets: 91 | 92 | `````xml 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | ````` 104 | 105 | Here, the bar widget is in the first column and row (0-based indexing) and spans three columns and rows. 106 | The box element is in the same page but in a different position. 107 | 108 | 109 | **Widgets** 110 | 111 | The available widgets are the ones that exist in the [blessed](https://github.com/chjj/blessed) and [blessed-contrib](https://github.com/yaronn/blessed-contrib) projects. 112 | You can infer the xml representation of a javascript widget using a simple convention. Assume that you would instantiate some blessed widget with this javascript: 113 | 114 | `````javascript 115 | blessed.widget({ string: "5" 116 | , int: 1 117 | , intArray: [1,2,3] 118 | , stringArray: ["a", "b", "c"] 119 | , multiArray: [ [1,2,3], [4,5,6] ] 120 | , complexArray: [ {a: 1, b: [1,2] }, {a: 3, b: [3,4]} ] 121 | , object: { innerProp: 1, multiArray: [ [1,2], [3,4] ] } 122 | }) 123 | ````` 124 | 125 | Then here is how you would represent it in xml: 126 | 127 | `````xml 128 | 129 | 130 | 1,2,3 131 | 4,5,6 132 | 133 | 134 | 1,2 135 | 3,4 136 | 137 | 138 | 139 | 140 | 141 | 142 | ````` 143 | 144 | You can also look at the [demo xml](https://raw.githubusercontent.com/yaronn/wopr/master/examples/sample.xml) to get more samples. 145 | 146 | ![](./examples/images/map.png "term") 147 | 148 | ##Viewing Reports## 149 | 150 | 151 | Depending on how you use a report, you have a few ways to view it. On Windows you will probably only be able to use the third option and need to [install the fonts](http://webservices20.blogspot.com/2015/04/running-terminal-dashboards-on-windows.html) for best view. 152 | 153 | **Option 1: POST it to the wopr online viewer** 154 | 155 | `````bash 156 | $> curl --data '' tty.zone\?cols=$((COLUMNS)) 157 | ````` 158 | 159 | If you experience firewall issues replace tty.zone with ec2-23-21-64-152.compute-1.amazonaws.com. 160 | 161 | **Option 2: POST it from external url** 162 | 163 | Save the report content in some url (e.g. gist) and then: 164 | 165 | `````bash 166 | $> a=$(curl -s https://gist.githubusercontent.com/yaronn/e6eec6d0e7adac63c83f/raw/50aca544d26a32aa189e790635c8679067017948/gistfile1.xml); curl --data "$a" tty.zone\?cols=$((COLUMNS)) 167 | ````` 168 | 169 | (note you need the gist raw url) 170 | 171 | If you experience firewall issues replace tty.zone with ec2-23-21-64-152.compute-1.amazonaws.com. 172 | 173 | Tip: If you use a url shortener (e.g. bit.ly) add the -L flag to curl to follow redirects. 174 | 175 | **Option 3: via the local viewer** 176 | 177 | Save the report xml to report.xml and then: 178 | 179 | `````bash 180 | $> npm install -g wopr 181 | $> wopr report.xml 182 | ````` 183 | 184 | Note the local viewer does not send anything online and does not require network. 185 | 186 | **Tip:** Maximize the terminal before viewing the report for best viewing experience 187 | **Tip:** If you CTRL+C in the middle or rendering your cursoe might disappear. Restore it by running again and letting the render complete or with `$> echo '\033[?25h'` 188 | 189 | **View customization** 190 | When using the online reports, you might need to adjust the slides size based on your font / resolution or use non-xterm terminal. tty.zone supports the following query params: 191 | 192 | `````bash 193 | curl -N tty.zone\?\&cols=200\&rows=50\&terminal=xterm 194 | ````` 195 | 196 | You can infer them automatically from your environment: 197 | 198 | `````bash 199 | curl -N tty.zone\?\&cols=$((COLUMNS))\&rows=$((LINES-5))\&terminal=${TERM} 200 | ````` 201 | 202 | It is best to escape all special characters (e.g. ? &) as seen in the above samples, since some shells will require this (zsh). 203 | 204 | 205 | **Pages** 206 | 207 | When viewing a report with the local viewer you can advance slides with the Return or Space keys. 208 | When using the online viewer you have 2 options: 209 | 210 | **Option 1:** Manually advance slides with Return or Space: 211 | 212 | `````bash 213 | p=0; while true; do curl tty.zone/$((p++))\?cols=$((COLUMNS)); read; done 214 | ````` 215 | 216 | **Option 2:** Slides advance automatically every 5 seconds: 217 | 218 | `````bash 219 | curl -N tty.zone/\[0-2\]\?auto\&cols=$((COLUMNS)) 220 | ````` 221 | 222 | Where 0 is the index of the first slide and 2 of the last slide. Keep the brackets in the url (they are not to express optional argument) and escape them as in the above sample. 223 | 224 | Tip: disable curl buffering with the -N flag 225 | 226 | You can also view a specific slide (#4 in this case): 227 | 228 | `````bash 229 | curl --data '...' tty.zone/4\?cols=$((COLUMNS)) 230 | ````` 231 | 232 | ##License## 233 | MIT 234 | 235 | 236 | ## More Information 237 | Created by Yaron Naveh ([twitter](http://twitter.com/YaronNaveh), [blog](http://webservices20.blogspot.com/)) 238 | -------------------------------------------------------------------------------- /bin/presenter-cli.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var file = process.argv[2] 4 | if (file=="-h" | file=="--help") { 5 | showHelp() 6 | process.exit() 7 | } 8 | 9 | var fs = require('fs') 10 | , parse = require('xml2js').parseString 11 | , Viewer = require('../lib/document-viewer.js') 12 | , blessed = require('blessed') 13 | , screen = blessed.screen() 14 | 15 | var page = -1 16 | var viewer = null 17 | 18 | if (!file) throw "please specify the presentation to render: \r\n\r\n $> doc-viewer doc.xml \r\n\r\n " 19 | 20 | var xml = fs.readFileSync(file) 21 | 22 | parse(xml, function (err, doc) { 23 | if (err) throw err 24 | 25 | if (!doc || !doc.document) throw "invalid document was provided" 26 | 27 | 28 | viewer = new Viewer(doc.document, screen) 29 | next() 30 | 31 | screen.key(['space', 'return'], function(ch, key) { 32 | next() 33 | }); 34 | 35 | screen.key(['backspace'], function(ch, key) { 36 | prev() 37 | }); 38 | 39 | screen.key(['q', 'escape'], function(ch, key) { 40 | process.exit() 41 | }); 42 | 43 | }); 44 | 45 | function prev() { 46 | if (page==0) return 47 | page-- 48 | show() 49 | 50 | } 51 | 52 | function next() { 53 | if (page>=viewer.document.page.length-1) process.exit() 54 | page++ 55 | show() 56 | } 57 | 58 | function show() { 59 | var err = viewer.renderPage(page) 60 | if (err!==null) console.log(err) 61 | } 62 | 63 | function showHelp() { 64 | console.log("\r\nusage: $> wopr [file.xml] \r\n\r\nuse spacebar / return to go forward in the presentation, backspace to go back, and ESC or q or CTRL+C to exit.\r\n\r\nhttps://github.com/yaronn/wopr\r\n\r\n") 65 | } -------------------------------------------------------------------------------- /examples/doc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/docs/images/charts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaronn/wopr/4cac0914baa82a8b7beb39a4be3292d6b5be6756/examples/docs/images/charts.png -------------------------------------------------------------------------------- /examples/docs/images/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaronn/wopr/4cac0914baa82a8b7beb39a4be3292d6b5be6756/examples/docs/images/map.png -------------------------------------------------------------------------------- /examples/images/charts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaronn/wopr/4cac0914baa82a8b7beb39a4be3292d6b5be6756/examples/images/charts.png -------------------------------------------------------------------------------- /examples/images/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaronn/wopr/4cac0914baa82a8b7beb39a4be3292d6b5be6756/examples/images/map.png -------------------------------------------------------------------------------- /examples/rich.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 20 | 21 | 29 | 30 | 38 | 39 | 48 | 49 | 57 | 58 | 63 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /examples/sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | *wopr* is a markup format for rich terminal `reports`, `presentations` and `infographics`. Build the report from any language and view it locally or remotely via curl. More information: [https://github.com/yaronn/wopr](https://github.com/yaronn/wopr) [https://twitter.com/YaronNaveh](https://twitter.com/YaronNaveh) 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ,1,02:17,21,13,20%,{red-fg}fail{/red-fg} 55 | ,2,02:15,33,11,25%,{blue-fg}pass{/blue-fg} 56 | ,3,03:22,15,12,80%,{red-fg}fail{/red-fg} 57 | ,4,05:00,11,18,12%,{red-fg}fail{/red-fg} 58 | ,5,02:17,17,12,0%,{blue-fg}pass{/blue-fg} 59 | ,6,01:01,23,29,80%,{red-fg}fail{/red-fg} 60 | 61 |
62 | 63 | 64 | 65 |
66 | 67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | *wopr* is a markup format for rich terminal `reports`, `presentations` and `inforgraphics`. Build the report from any language and view it locally or remotely via curl. More information: [https://github.com/yaronn/wopr](https://github.com/yaronn/wopr) [https://twitter.com/YaronNaveh](https://twitter.com/YaronNaveh) 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | *Thank you!* More information: [https://github.com/yaronn/wopr](https://github.com/yaronn/wopr) [https://twitter.com/YaronNaveh](https://twitter.com/YaronNaveh) created by @YaronNaveh 151 | 152 | 153 | 154 | 155 | 156 | 157 |
158 | 159 | -------------------------------------------------------------------------------- /lib/document-viewer.js: -------------------------------------------------------------------------------- 1 | var blessed = require('blessed') 2 | , contrib = require('blessed-contrib') 3 | 4 | function DocumentViewer(document, screen) { 5 | this.document = document 6 | this.screen = screen || blessed.screen() 7 | //console.log(JSON.stringify(document, null, 2)) 8 | } 9 | 10 | DocumentViewer.prototype.renderPage = function(pageId, msg) { 11 | 12 | var self = this 13 | 14 | if (typeof(msg)=="undefined") msg = "press Return to continue" 15 | 16 | this.clear() 17 | 18 | var page = this.document.page[pageId] 19 | 20 | var grid = new contrib.grid({rows: 12, cols: 12, hideBorder: true, screen: this.screen, dashboardMargin: 5}) 21 | 22 | for (var i in page.item) { 23 | var item = page.item[i] 24 | var className = Object.keys(item)[1] 25 | var ctor = contrib[className] || blessed[className] 26 | if (!ctor) { 27 | return "no such widget: " + className 28 | } 29 | var opts = this.readOptions(item[className][0], ctor) 30 | 31 | //workaround 32 | if (className=="picture") { 33 | opts.onReady = function() { self.screen.render() } 34 | } 35 | 36 | //workaround 37 | if (classname="bigtext" && !opts.font) { 38 | opts.font = __dirname + "/../lib/ter-u12n.json" 39 | } 40 | 41 | //console.log(JSON.stringify(opts, null, 2)) 42 | grid.set(item.$.row, item.$.col, item.$.rowSpan, item.$.colSpan, ctor, opts) 43 | } 44 | 45 | var next = blessed.box({top: "90%", content: msg}) 46 | this.screen.append(next) 47 | 48 | this.screen.render() 49 | return null 50 | } 51 | 52 | DocumentViewer.prototype.clear = function(node) { 53 | var i = this.screen.children.length 54 | while (i--) this.screen.children[i].detach() 55 | } 56 | 57 | DocumentViewer.prototype.readOptions = function(node, ctor) { 58 | 59 | var optionsMethod = ctor.prototype["getOptionsPrototype"] 60 | var optionsMainProto = optionsMethod==null ? null : optionsMethod() 61 | //console.log(optionsMainProto) 62 | return this.readOptionsInner(node, optionsMainProto) 63 | } 64 | 65 | DocumentViewer.prototype.readOptionsInner = function(node, optionsMainProto) { 66 | var res = {} 67 | 68 | var items = [] 69 | for (var attr in node.$) { 70 | items.push({name: attr, value: node.$[attr]}) 71 | } 72 | 73 | for (var n in node) { 74 | if (n!='$') items.push( { name: n, value: node[n] } ) 75 | } 76 | 77 | for (var i=0; i0) res.push(self.convertArray(s, type[0][0])) 116 | } 117 | return res 118 | } 119 | else if (typeof(type[0])=="object") { 120 | var res = [] 121 | for (var i=0; i1) page=parseInt(u.pathname.substr(1)) || 0 17 | var auto = u.query["auto"]==='' || u.query["auto/"]==='' //second can happen in zsh when quoting the string 18 | var msg = auto?'next slide will appear within a few seconds':undefined 19 | 20 | if (!body || body=="") { 21 | return cba("You must upload the document to present as the POST body") 22 | } 23 | 24 | 25 | parse(body, function (err, doc) { 26 | try { 27 | 28 | if (err) { 29 | return cba("Document xml is not valid: " + err) 30 | } 31 | 32 | if (!doc || !doc.document) return cba("document not valid or has no pages") 33 | if (!doc.document.page || doc.document.page.length==0) return cba("document must have at least one page") 34 | 35 | if (page>=doc.document.page.length) { 36 | return cba('\r\n\r\nPresentation has ended (total '+doc.document.page.length+' pages). Press CTRL+C to exit.\r\n\r\n') 37 | } 38 | 39 | req.connection.on('close',function(){ 40 | screen = null 41 | }); 42 | 43 | var screen = contrib.createScreen(req, res) 44 | if (screen==null) return 45 | 46 | viewer = new Viewer(doc.document, screen) 47 | var err = viewer.renderPage(page, msg) 48 | if (err!==null) { 49 | clean(screen) 50 | return cba(err) 51 | } 52 | 53 | //note the setTimeout is necessary even if delay is 0 54 | setTimeout(function() { 55 | //restore cursor 56 | res.end('\033[?25h') 57 | clean(screen) 58 | return cba() 59 | }, auto?5000:0) 60 | 61 | } 62 | 63 | catch (e) { 64 | return cba(e) 65 | } 66 | 67 | 68 | }) 69 | } 70 | 71 | function clean(screen) { 72 | //TODO this code is very sensitive to blessed versions, need to check right version/usage 73 | //screen.program.destroy() 74 | //screen.destroy() 75 | } 76 | 77 | module.exports = present 78 | 79 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var blessed = require('blessed') 4 | 5 | patchBlessed() 6 | 7 | var http = require('http') 8 | , url = require('url') 9 | , fs = require('fs') 10 | , present = require('./presenter') 11 | , contrib = require('blessed-contrib') 12 | 13 | var port = process.env.PORT || 1337 14 | 15 | http.createServer(function (req, res) { 16 | 17 | if (req.method == 'POST') { 18 | var body = ''; 19 | req.on('data', function (data) { 20 | body += data; 21 | 22 | // Too much POST data, kill the connection! 23 | if (body.length > 1e6) 24 | req.connection.destroy(); 25 | }); 26 | req.on('end', function () { 27 | present(req, res, body, function(err) { 28 | if (err) console.log(new Error().stack) 29 | if (err) return contrib.serverError(req, res, err) 30 | }) 31 | }); 32 | } 33 | else { 34 | if (req.headers["user-agent"] && req.headers["user-agent"].indexOf('curl')!=-1) { 35 | 36 | var content = fs.readFileSync(__dirname+'/../examples/sample.xml') 37 | present(req, res, content, function(err) { 38 | if (err) console.log(new Error().stack) 39 | if (err) return contrib.serverError(req, res, err) 40 | }) 41 | return 42 | } 43 | else { 44 | res.writeHead(301, {'Location': 'https://github.com/yaronn/wopr'}); 45 | res.end() 46 | return 47 | } 48 | } 49 | 50 | 51 | }).listen(port); 52 | 53 | /*in the context of web server the following code will leak: 54 | 55 | http.createServer(function (req, res) { 56 | var s = contrib.createScreen(req, res) 57 | //alternatively: 58 | //var program = new blessed.Program() 59 | 60 | setTimeout(function() {res.end(Date.now() + "")}, 0) 61 | 62 | }).listen(8080); 63 | 64 | this comes down to event registrations on program (the latest commit here points them https://github.com/yaronn/blessed-patch-temp) 65 | */ 66 | function patchBlessed() { 67 | blessed.Program.prototype.listem = function() {} 68 | process.on = function() {} 69 | } 70 | 71 | console.log('Server running at http://127.0.0.1:'+port+'/'); 72 | -------------------------------------------------------------------------------- /server/test/test.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | p=0; while true; do curl -NL --data '' http://localhost:1337/?$((p++)); read; done 6 | 7 | 8 | 9 | 10 | a=$(curl -s http:// www.netscape.com/); echo "$a" 11 | 12 | 13 | 14 | a=$(curl -s https://gist.githubusercontent.com/yaronn/6a8f6947fedd88f783d3/raw/12a9f3216769834f356a458f0b8ee6049e59206d/gistfile1.txt); p=0; while true; do curl -NL --data "$a" http://localhost:1337/$((p++)); read; done 15 | 16 | 17 | 18 | p=0; while true; do curl tty.zone/$((p++)); read; done 19 | 20 | 21 | sudo PORT=8080 FORCE_COLOR=1 forever start server.js 22 | sudo PORT=8081 FORCE_COLOR=1 forever start server.js 23 | 24 | 25 | curl -N tty.zone/[0-2]?auto 26 | 27 | curl -N ec2-23-21-64-152.compute-1.amazonaws.com/[0-2]?auto 28 | 29 | 30 | 31 | curl -N --data '' tty.zone 32 | 33 | 34 | nginx: 35 | 36 | http { 37 | upstream my { 38 | server localhost:8080; 39 | server localhost:8081; 40 | } 41 | server { 42 | listen 80; 43 | server_name tty.zone www.tty.zone; 44 | location / { 45 | proxy_pass http://my; 46 | proxy_buffering off; 47 | } 48 | } 49 | 50 | ... 51 | } 52 | 53 | top + CTRL+I to get process cpu percent per all cores 54 | 55 | cat /var/log/nginx/error.log | tail -10 56 | cat /var/log/nginx/access.log | tail -10 57 | 58 | 59 | 60 | 61 | //weather parse 62 | 63 | var str="http://www.w.com/?markerCount=37&lon0=-122.33&lat0=47.61&char0=61&lon1=-119.77&lat1=36.75&char1=78&lon2=-115.14&lat2=36.17&char2=71&lon3=-116.2&lat3=43.61&char3=61&lon4=-111.89&lat4=40.76&char4=55&lon5=-112.07&lat5=33.45&char5=81&lon6=-108.5&lat6=45.78&char6=61&lon7=-104.82&lat7=41.14&char7=50&lon8=-104.98&lat8=39.74&char8=48&lon9=-106.65&lat9=35.08&char9=52&lon10=-100.78&lat10=46.81&char10=56&lon11=-103.23&lat11=44.08&char11=59&lon12=-95.94&lat12=41.26&char12=67&lon13=-94.58&lat13=39.1&char13=69&lon14=-97.52&lat14=35.47&char14=64&lon15=-96.81&lat15=32.78&char15=60&lon16=-95.36&lat16=29.76&char16=67&lon17=-93.26&lat17=44.98&char17=55&lon18=-93.61&lat18=41.6&char18=66&lon19=-92.29&lat19=34.75&char19=63&lon20=-90.23&lat20=39.73&char20=57&lon21=-90.08&lat21=29.95&char21=64&lon22=-87.91&lat22=43.04&char22=43&lon23=-87.64&lat23=41.51&char23=52&lon24=-83.05&lat24=42.33&char24=40&lon25=-86.16&lat25=39.77&char25=54&lon26=-85.76&lat26=38.25&char26=56&lon27=-86.78&lat27=36.17&char27=57&lon28=-90.05&lat28=35.15&char28=64&lon29=-84.39&lat29=33.75&char29=52&lon30=-81.38&lat30=28.54&char30=78&lon31=-80.19&lat31=25.77&char31=75&lon32=-94.7&lat32=37.41&char32=57&lon33=-84.29&lat33=37.75&char33=54&lon34=-77.04&lat34=38.9&char34=50&lon35=-75.5&lat35=43&char35=38&lon36=-71.06&lat36=42.36&char36=32" 64 | 65 | 66 | var url = require('url') 67 | 68 | var q = url.parse(str, true).query 69 | 70 | 71 | var res = "" 72 | 73 | for (var i=0; i