├── .gitignore ├── README.md ├── example └── index.html ├── init.js ├── package-lock.json ├── package.json ├── streamserver ├── pipelines │ ├── custom.js │ └── default.js ├── streamserver_express.js ├── streamserver_simple.js └── utils │ ├── esri_types.js │ └── filter_utils.js ├── templates └── service.json └── utils ├── test2.json ├── test3.json ├── test_json_ws.js └── websocket_utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ArcGIS WebSocket Server 2 | 3 | 4 | 5 | 6 | 7 | - [Start the app](#start-the-app) 8 | - [Using HTTPS (NGROK)](#using-https-ngrok) 9 | - [Known issues](#known-issues) 10 | - [ArcGIS API for JavaScript version <= v4.8 & v3.x](#arcgis-api-for-javascript-version--v48--v3x) 11 | - [Additional documentation](#additional-documentation) 12 | - [Talk: Geolocating tweets in real time (in Spanish)](#talk-geolocating-tweets-in-real-time-in-spanish) 13 | 14 | 15 | 16 | This node server behaves as a [GeoEvent](https://www.esri.com/en-us/arcgis/products/arcgis-geoevent-server) [StreamServer](https://developers.arcgis.com/rest/services-reference/stream-service.htm) layer, so it will emit geographic features in the [Esri JSON](https://developers.arcgis.com/documentation/common-data-types/feature-object.htm) format though a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API). This way we will be able to display a real time layer in ArcGIS without an [ArcGIS Enterprise](https://www.esri.com/en-us/arcgis/products/arcgis-enterprise/overview) stack. 17 | 18 | ![custom websocket server in arcgis](https://user-images.githubusercontent.com/826965/53808519-bc44bb80-3f52-11e9-9635-8687d5046bc4.gif) 19 | 20 | It can be used with any [ArcGIS developer technology](https://developers.arcgis.com/documentation/#sdks) or [any other product](https://esri-es.github.io/awesome-arcgis/arcgis/products/). For example add the StreamServer to a [webmap](https://esri-es.github.io/awesome-arcgis/esri/open-vision/open-specifications/web-map/) and visualize it in Operations Dashboard, ArcGIS Pro, any Storymap, etc. 21 | 22 | ## Start the app 23 | 24 | > *We are assuming you are familiar with NodeJS, if you are not please [read this first](https://nodejs.org/en/docs/guides/getting-started-guide/)* 25 | 26 | 1. Check [init.js](https://github.com/esri-es/arcgis_websocket_server/blob/feature/dynamic_service/init.js#L4) and fill in **SERVICE_CONF** object properly. 27 | 28 | ```js 29 | const SERVICE_CONF = { 30 | name : "twitter", 31 | out_sr : { 32 | wkid : 102100, 33 | latestWkid : 3857 34 | }, 35 | port : 9000, 36 | host : process.env["NGROK"] || "localhost", 37 | protocol : process.env["NGROK"] ? "https" : "http" 38 | }; 39 | ``` 40 | 41 | > Be aware of the **name : "twitter"**. It will be propagated below, in the instructions. If you want to use othe name, change it also along the instructions. 42 | 43 | 2. Start the real time server: `node init.js "ws://localhost:8888" "4.11"` 44 | 45 | > In this example **ws://localhost:8888** is the websocket connection we want to consume data from. This websocket connection it's not the same as the one which will be exposed by **StreamServer**. But don't worry, all will be wired up (luckily) 46 | 47 | > It will check the **websocket** connection first. If it's any problem, it will notice it, and it will warn you :-) 48 | 49 | > If it's able to connect, it "automagically" set the fields for your **StreamServer** according to the payload received in the websocket connection attempted before. 50 | 51 | 3. Start a web server: `cd example & http-server -p 9090` 52 | 4. Open: [http://localhost:9090/](http://localhost:9090/) 53 | 1. Stream service url: `http://localhost:9000/arcgis/rest/services/twitter/StreamServer` 54 | 55 | ### Using HTTPS (NGROK) 56 | 57 | If you want to test this from the [sandbox sample](https://developers.arcgis.com/javascript/latest/sample-code/sandbox/index.html?sample=layers-streamlayer) you can also use [ngrok](https://ngrok.com/) 58 | 59 | 1) Run: `ngrok http 9000` 60 | 2) Stop init.js and run `NGROK=yourid.ngrok.io node init.js "ws://localhost:8888" "4.11"` 61 | 3) Use: `https://yourid.ngrok.io/arcgis/rest/services/twitter/StreamServer` instead of `http://localhost:9000/arcgis/rest/services/twitter/StreamServer` 62 | 63 | ## Known issues 64 | 65 | ### ArcGIS API for JavaScript version <= v4.8 & v3.x 66 | 67 | > UPDATE : Working in a version which can handle any version! Cross your fingers! 68 | 69 | Before [this commit](https://github.com/hhkaos/arcgis_websocket_server/commit/22c48299d92e7761e6c718d2c6afa525284fc448) on May 5, 2015 this streamserver was only working with JS API <= v4.8 and v3.x. If you want to know more you can also [check this issue](https://github.com/hhkaos/arcgis_websocket_server/issues/3). 70 | 71 | ## Additional documentation 72 | 73 | * [ArcGIS Server > Stream services](http://enterprise.arcgis.com/en/server/latest/publish-services/linux/stream-services.htm) 74 | * [ArcGIS REST API > StreamServices](https://developers.arcgis.com/rest/services-reference/stream-service.htm) 75 | * [Awesome ArcGIS > GeoEvent Server](https://esri-es.github.io/awesome-arcgis/arcgis/products/arcgis-enterprise/arcgis-server/geoevent-server/) 76 | * [Awesome ArcGIS > Internet of things (IoT) & Real-time (RT)](https://esri-es.github.io/awesome-arcgis/esri/emerging-technologies/iot-rt/?) 77 | * [Public stream services in ArcGIS Online](https://esri-es.github.io/arcgis-developer-tips-and-tricks/arcgis-online/search/?q=typekeywords%3A%22stream+service%22&numResults=100&sortField=relevance&Thumbnail=generateThumbnail(elem)&Title=elem.title&Details=%27%3Ca+href%3D%22https%3A%2F%2Fwww.arcgis.com%2Fhome%2Fitem.html%3Fid%3D%27%2Belem.id%2B%27%22+target%3D%22_blank%22%3EDetails%3C%2Fa%3E%27&Owner=elem.owner&Type=elem.type&Views=elem.numViews) 78 | * [Preview several Stream Services simultaneously in a Web Map](http://www.arcgis.com/home/webmap/viewer.html?webmap=55a55a4c08934ba890f7fbd5589cffe6) 79 | * [Pubnub & Esri](https://chrome.google.com/webstore/detail/allow-control-allow-origi/nlfbmbojpeacfghkpbjhddihlkkiljbi) 80 | * [Mapping and Tracking that Your Users Crave](https://www.youtube.com/watch?v=VWoXSJWgwrU) 81 | * [Esri DevSummit 2017 Keynote: PubNub CEO Todd Greene](https://www.youtube.com/watch?v=yrbODI7cuAk) 82 | * [Search more about "Stream services"](https://esri-es.github.io/arcgis-search/?amp%3Butm_source=opensearch&search=%22Stream+services%22) 83 | 84 | ## Talk: Geolocating tweets in real time (in Spanish) 85 | 86 | En la charla [Geolocalizando tweets en tiempo real](http://slides.com/hhkaos/geolocalizando-tweets#/) del día día 24 de Julio de 2019 se explicó: 87 | 88 | * Objetivos y resultados del experimento 89 | * Cómo lanzar el proyecto 90 | * Demo: Cargar los tweets en una StreamLayer 91 | * Demo: Cargar los tweets en un Web map 92 | * Demo: Cargar los tweets con gráficar en tº real 93 | * Diagrama del comportamiento de [twitter-rt-service](https://github.com/esri-es/twitter-rt-service) 94 | * Mejoras pendientes en [twitter-rt-service](https://github.com/esri-es/twitter-rt-service) 95 | * Diagrama del comportamiento de [arcgis_websocket_server](https://github.com/esri-es/arcgis_websocket_server) 96 | * Mejoras pendientes en [arcgis_websocket_server](https://github.com/esri-es/arcgis_websocket_server) 97 | * Estructura de ficheros del proyecto: [twitter-rt-service](https://github.com/esri-es/twitter-rt-service) 98 | * Explicación del código: [twitter-rt-service](https://github.com/esri-es/twitter-rt-service) 99 | * Estructura de ficheros del proyecto: [arcgis_websocket_server](https://github.com/esri-es/arcgis_websocket_server) 100 | * Explicación del código: [arcgis_websocket_server](https://github.com/esri-es/arcgis_websocket_server) 101 | * Despedida, preguntas y agracedimientos 102 | 103 | A continuación puede encontrar el vídeo en Youtube con un índice interactivo en la descripción del vídeo: 104 | 105 | [![](http://i3.ytimg.com/vi/PeTzi-ficFo/hqdefault.jpg)](https://www.youtube.com/watch?v=PeTzi-ficFo) 106 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | StreamLayer 7 | 33 | 34 | 35 | 203 | 204 | 205 |
206 | 207 |
208 | Stream service url:
211 | 212 |
213 | 217 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /init.js: -------------------------------------------------------------------------------- 1 | const testConnection = require("./utils/websocket_utils.js"); 2 | const streamServer = require('./streamserver/streamserver_simple.js'); 3 | 4 | const SERVICE_CONF = { 5 | name : "twitter", 6 | out_sr : { 7 | wkid : 102100, 8 | latestWkid : 3857 9 | }, 10 | port : process.env["NGROK"] ? 9000 : 9000, 11 | host : process.env["NGROK"] || "localhost", 12 | protocol : process.env["NGROK"] ? "https" : "http" 13 | }; 14 | 15 | var wsClientConn = process.argv[2]; 16 | testConnection(wsClientConn) 17 | .then(conf => { 18 | let CONFIG = { 19 | client : conf.client, 20 | payload: conf.payload, 21 | service : SERVICE_CONF 22 | }; 23 | // start StreamServer 24 | streamServer.start(CONFIG); 25 | }) 26 | .catch((err) => { 27 | console.log(`ws initialization failed! reason : [${err}]`); 28 | process.exit(12); 29 | }); 30 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arcgis_websockets", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "JSV": { 8 | "version": "4.0.2", 9 | "resolved": "https://registry.npmjs.org/JSV/-/JSV-4.0.2.tgz", 10 | "integrity": "sha1-0Hf2glVx+CEy+d/67Vh7QCn+/1c=" 11 | }, 12 | "accepts": { 13 | "version": "1.3.5", 14 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 15 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 16 | "requires": { 17 | "mime-types": "~2.1.18", 18 | "negotiator": "0.6.1" 19 | } 20 | }, 21 | "ansi-styles": { 22 | "version": "1.0.0", 23 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", 24 | "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=" 25 | }, 26 | "array-flatten": { 27 | "version": "1.1.1", 28 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 29 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 30 | }, 31 | "async-limiter": { 32 | "version": "1.0.0", 33 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 34 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 35 | }, 36 | "body-parser": { 37 | "version": "1.18.3", 38 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", 39 | "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", 40 | "requires": { 41 | "bytes": "3.0.0", 42 | "content-type": "~1.0.4", 43 | "debug": "2.6.9", 44 | "depd": "~1.1.2", 45 | "http-errors": "~1.6.3", 46 | "iconv-lite": "0.4.23", 47 | "on-finished": "~2.3.0", 48 | "qs": "6.5.2", 49 | "raw-body": "2.3.3", 50 | "type-is": "~1.6.16" 51 | } 52 | }, 53 | "bytes": { 54 | "version": "3.0.0", 55 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 56 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 57 | }, 58 | "chalk": { 59 | "version": "0.4.0", 60 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", 61 | "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", 62 | "requires": { 63 | "ansi-styles": "~1.0.0", 64 | "has-color": "~0.1.0", 65 | "strip-ansi": "~0.1.0" 66 | } 67 | }, 68 | "colors": { 69 | "version": "1.3.3", 70 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", 71 | "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==" 72 | }, 73 | "content-disposition": { 74 | "version": "0.5.2", 75 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 76 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 77 | }, 78 | "content-type": { 79 | "version": "1.0.4", 80 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 81 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 82 | }, 83 | "cookie": { 84 | "version": "0.3.1", 85 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 86 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 87 | }, 88 | "cookie-signature": { 89 | "version": "1.0.6", 90 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 91 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 92 | }, 93 | "core-util-is": { 94 | "version": "1.0.2", 95 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 96 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 97 | }, 98 | "debug": { 99 | "version": "2.6.9", 100 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 101 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 102 | "requires": { 103 | "ms": "2.0.0" 104 | } 105 | }, 106 | "depd": { 107 | "version": "1.1.2", 108 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 109 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 110 | }, 111 | "destroy": { 112 | "version": "1.0.4", 113 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 114 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 115 | }, 116 | "duplexer": { 117 | "version": "0.1.1", 118 | "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", 119 | "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" 120 | }, 121 | "duplexify": { 122 | "version": "3.7.1", 123 | "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", 124 | "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", 125 | "requires": { 126 | "end-of-stream": "^1.0.0", 127 | "inherits": "^2.0.1", 128 | "readable-stream": "^2.0.0", 129 | "stream-shift": "^1.0.0" 130 | } 131 | }, 132 | "ee-first": { 133 | "version": "1.1.1", 134 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 135 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 136 | }, 137 | "encodeurl": { 138 | "version": "1.0.2", 139 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 140 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 141 | }, 142 | "end-of-stream": { 143 | "version": "1.4.1", 144 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", 145 | "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", 146 | "requires": { 147 | "once": "^1.4.0" 148 | } 149 | }, 150 | "escape-html": { 151 | "version": "1.0.3", 152 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 153 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 154 | }, 155 | "etag": { 156 | "version": "1.8.1", 157 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 158 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 159 | }, 160 | "event-stream": { 161 | "version": "4.0.1", 162 | "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", 163 | "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", 164 | "requires": { 165 | "duplexer": "^0.1.1", 166 | "from": "^0.1.7", 167 | "map-stream": "0.0.7", 168 | "pause-stream": "^0.0.11", 169 | "split": "^1.0.1", 170 | "stream-combiner": "^0.2.2", 171 | "through": "^2.3.8" 172 | } 173 | }, 174 | "finalhandler": { 175 | "version": "1.1.2", 176 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 177 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 178 | "requires": { 179 | "debug": "2.6.9", 180 | "encodeurl": "~1.0.2", 181 | "escape-html": "~1.0.3", 182 | "on-finished": "~2.3.0", 183 | "parseurl": "~1.3.3", 184 | "statuses": "~1.5.0", 185 | "unpipe": "~1.0.0" 186 | }, 187 | "dependencies": { 188 | "parseurl": { 189 | "version": "1.3.3", 190 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 191 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 192 | }, 193 | "statuses": { 194 | "version": "1.5.0", 195 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 196 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 197 | } 198 | } 199 | }, 200 | "forwarded": { 201 | "version": "0.1.2", 202 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 203 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 204 | }, 205 | "fresh": { 206 | "version": "0.5.2", 207 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 208 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 209 | }, 210 | "from": { 211 | "version": "0.1.7", 212 | "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", 213 | "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" 214 | }, 215 | "fs-extra": { 216 | "version": "8.0.1", 217 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.0.1.tgz", 218 | "integrity": "sha512-W+XLrggcDzlle47X/XnS7FXrXu9sDo+Ze9zpndeBxdgv88FHLm1HtmkhEwavruS6koanBjp098rUpHs65EmG7A==", 219 | "requires": { 220 | "graceful-fs": "^4.1.2", 221 | "jsonfile": "^4.0.0", 222 | "universalify": "^0.1.0" 223 | } 224 | }, 225 | "graceful-fs": { 226 | "version": "4.1.15", 227 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", 228 | "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" 229 | }, 230 | "has-color": { 231 | "version": "0.1.7", 232 | "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", 233 | "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=" 234 | }, 235 | "http-errors": { 236 | "version": "1.6.3", 237 | "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 238 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 239 | "requires": { 240 | "depd": "~1.1.2", 241 | "inherits": "2.0.3", 242 | "setprototypeof": "1.1.0", 243 | "statuses": ">= 1.4.0 < 2" 244 | } 245 | }, 246 | "iconv-lite": { 247 | "version": "0.4.23", 248 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", 249 | "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", 250 | "requires": { 251 | "safer-buffer": ">= 2.1.2 < 3" 252 | } 253 | }, 254 | "inherits": { 255 | "version": "2.0.3", 256 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 257 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 258 | }, 259 | "ipaddr.js": { 260 | "version": "1.8.0", 261 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", 262 | "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" 263 | }, 264 | "isarray": { 265 | "version": "1.0.0", 266 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 267 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 268 | }, 269 | "jsonfile": { 270 | "version": "4.0.0", 271 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", 272 | "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", 273 | "requires": { 274 | "graceful-fs": "^4.1.6" 275 | } 276 | }, 277 | "jsonlint": { 278 | "version": "1.6.3", 279 | "resolved": "https://registry.npmjs.org/jsonlint/-/jsonlint-1.6.3.tgz", 280 | "integrity": "sha512-jMVTMzP+7gU/IyC6hvKyWpUU8tmTkK5b3BPNuMI9U8Sit+YAWLlZwB6Y6YrdCxfg2kNz05p3XY3Bmm4m26Nv3A==", 281 | "requires": { 282 | "JSV": "^4.0.x", 283 | "nomnom": "^1.5.x" 284 | } 285 | }, 286 | "map-stream": { 287 | "version": "0.0.7", 288 | "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", 289 | "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=" 290 | }, 291 | "media-typer": { 292 | "version": "0.3.0", 293 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 294 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 295 | }, 296 | "merge-descriptors": { 297 | "version": "1.0.1", 298 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 299 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 300 | }, 301 | "methods": { 302 | "version": "1.1.2", 303 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 304 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 305 | }, 306 | "mgrs": { 307 | "version": "1.0.0", 308 | "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", 309 | "integrity": "sha1-+5FYjnjJACVnI5XLQLJffNatGCk=" 310 | }, 311 | "mime": { 312 | "version": "1.4.1", 313 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 314 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 315 | }, 316 | "mime-db": { 317 | "version": "1.36.0", 318 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", 319 | "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" 320 | }, 321 | "mime-types": { 322 | "version": "2.1.20", 323 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", 324 | "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", 325 | "requires": { 326 | "mime-db": "~1.36.0" 327 | } 328 | }, 329 | "moment": { 330 | "version": "2.24.0", 331 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", 332 | "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" 333 | }, 334 | "ms": { 335 | "version": "2.0.0", 336 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 337 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 338 | }, 339 | "negotiator": { 340 | "version": "0.6.1", 341 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 342 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 343 | }, 344 | "nomnom": { 345 | "version": "1.8.1", 346 | "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.8.1.tgz", 347 | "integrity": "sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=", 348 | "requires": { 349 | "chalk": "~0.4.0", 350 | "underscore": "~1.6.0" 351 | } 352 | }, 353 | "on-finished": { 354 | "version": "2.3.0", 355 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 356 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 357 | "requires": { 358 | "ee-first": "1.1.1" 359 | } 360 | }, 361 | "once": { 362 | "version": "1.4.0", 363 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 364 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 365 | "requires": { 366 | "wrappy": "1" 367 | } 368 | }, 369 | "parseurl": { 370 | "version": "1.3.2", 371 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 372 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 373 | }, 374 | "path-to-regexp": { 375 | "version": "0.1.7", 376 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 377 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 378 | }, 379 | "pause-stream": { 380 | "version": "0.0.11", 381 | "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", 382 | "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", 383 | "requires": { 384 | "through": "~2.3" 385 | } 386 | }, 387 | "process-nextick-args": { 388 | "version": "2.0.0", 389 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", 390 | "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" 391 | }, 392 | "proj4": { 393 | "version": "2.5.0", 394 | "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.5.0.tgz", 395 | "integrity": "sha512-XZTRT7OPdLzgvtTqL8DG2cEj8lYdovztOwiwpwRSYayOty5Ipf3H68dh/fiL+HKDEyetmQSMhkkMGiJoyziz3w==", 396 | "requires": { 397 | "mgrs": "1.0.0", 398 | "wkt-parser": "^1.2.0" 399 | } 400 | }, 401 | "proxy-addr": { 402 | "version": "2.0.4", 403 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", 404 | "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", 405 | "requires": { 406 | "forwarded": "~0.1.2", 407 | "ipaddr.js": "1.8.0" 408 | } 409 | }, 410 | "qs": { 411 | "version": "6.5.2", 412 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 413 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 414 | }, 415 | "range-parser": { 416 | "version": "1.2.0", 417 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 418 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 419 | }, 420 | "raw-body": { 421 | "version": "2.3.3", 422 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", 423 | "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", 424 | "requires": { 425 | "bytes": "3.0.0", 426 | "http-errors": "1.6.3", 427 | "iconv-lite": "0.4.23", 428 | "unpipe": "1.0.0" 429 | } 430 | }, 431 | "readable-stream": { 432 | "version": "2.3.6", 433 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 434 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 435 | "requires": { 436 | "core-util-is": "~1.0.0", 437 | "inherits": "~2.0.3", 438 | "isarray": "~1.0.0", 439 | "process-nextick-args": "~2.0.0", 440 | "safe-buffer": "~5.1.1", 441 | "string_decoder": "~1.1.1", 442 | "util-deprecate": "~1.0.1" 443 | } 444 | }, 445 | "router": { 446 | "version": "1.3.3", 447 | "resolved": "https://registry.npmjs.org/router/-/router-1.3.3.tgz", 448 | "integrity": "sha1-wUL2tepNazNZAiypW2WAvSF/ic8=", 449 | "requires": { 450 | "array-flatten": "2.1.1", 451 | "debug": "2.6.9", 452 | "methods": "~1.1.2", 453 | "parseurl": "~1.3.2", 454 | "path-to-regexp": "0.1.7", 455 | "setprototypeof": "1.1.0", 456 | "utils-merge": "1.0.1" 457 | }, 458 | "dependencies": { 459 | "array-flatten": { 460 | "version": "2.1.1", 461 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz", 462 | "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=" 463 | } 464 | } 465 | }, 466 | "safe-buffer": { 467 | "version": "5.1.2", 468 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 469 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 470 | }, 471 | "safer-buffer": { 472 | "version": "2.1.2", 473 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 474 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 475 | }, 476 | "send": { 477 | "version": "0.16.2", 478 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", 479 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", 480 | "requires": { 481 | "debug": "2.6.9", 482 | "depd": "~1.1.2", 483 | "destroy": "~1.0.4", 484 | "encodeurl": "~1.0.2", 485 | "escape-html": "~1.0.3", 486 | "etag": "~1.8.1", 487 | "fresh": "0.5.2", 488 | "http-errors": "~1.6.2", 489 | "mime": "1.4.1", 490 | "ms": "2.0.0", 491 | "on-finished": "~2.3.0", 492 | "range-parser": "~1.2.0", 493 | "statuses": "~1.4.0" 494 | } 495 | }, 496 | "serve-static": { 497 | "version": "1.13.2", 498 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", 499 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", 500 | "requires": { 501 | "encodeurl": "~1.0.2", 502 | "escape-html": "~1.0.3", 503 | "parseurl": "~1.3.2", 504 | "send": "0.16.2" 505 | } 506 | }, 507 | "setprototypeof": { 508 | "version": "1.1.0", 509 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 510 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 511 | }, 512 | "split": { 513 | "version": "1.0.1", 514 | "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", 515 | "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", 516 | "requires": { 517 | "through": "2" 518 | } 519 | }, 520 | "statuses": { 521 | "version": "1.4.0", 522 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 523 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 524 | }, 525 | "stream-chain": { 526 | "version": "2.1.0", 527 | "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.1.0.tgz", 528 | "integrity": "sha512-PAUXdRGm0G8P0+/+JEd3O9kfmB9kwmr2nKIc5zhcsHn0KdBByD5PJ2po21iDzc+TZsOSEbU8j4JbAevJsZkLyQ==" 529 | }, 530 | "stream-combiner": { 531 | "version": "0.2.2", 532 | "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", 533 | "integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=", 534 | "requires": { 535 | "duplexer": "~0.1.1", 536 | "through": "~2.3.4" 537 | } 538 | }, 539 | "stream-json": { 540 | "version": "1.2.1", 541 | "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.2.1.tgz", 542 | "integrity": "sha512-KcZkgyTENM4HXBDyIguAcyWUogXp+wwWF3wLL5QtWj8SlNESx+4xGWk7rYdFDtTvQ1FhSRkQEWv5xOu4eHU+jQ==", 543 | "requires": { 544 | "stream-chain": "^2.1.0" 545 | } 546 | }, 547 | "stream-shift": { 548 | "version": "1.0.0", 549 | "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", 550 | "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" 551 | }, 552 | "string_decoder": { 553 | "version": "1.1.1", 554 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 555 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 556 | "requires": { 557 | "safe-buffer": "~5.1.0" 558 | } 559 | }, 560 | "strip-ansi": { 561 | "version": "0.1.1", 562 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", 563 | "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=" 564 | }, 565 | "through": { 566 | "version": "2.3.8", 567 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 568 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 569 | }, 570 | "through2": { 571 | "version": "3.0.1", 572 | "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", 573 | "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", 574 | "requires": { 575 | "readable-stream": "2 || 3" 576 | } 577 | }, 578 | "type-is": { 579 | "version": "1.6.16", 580 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 581 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 582 | "requires": { 583 | "media-typer": "0.3.0", 584 | "mime-types": "~2.1.18" 585 | } 586 | }, 587 | "ultron": { 588 | "version": "1.1.1", 589 | "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", 590 | "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" 591 | }, 592 | "underscore": { 593 | "version": "1.6.0", 594 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", 595 | "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" 596 | }, 597 | "universalify": { 598 | "version": "0.1.2", 599 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", 600 | "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" 601 | }, 602 | "unpipe": { 603 | "version": "1.0.0", 604 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 605 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 606 | }, 607 | "util-deprecate": { 608 | "version": "1.0.2", 609 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 610 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 611 | }, 612 | "utils-merge": { 613 | "version": "1.0.1", 614 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 615 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 616 | }, 617 | "uuid": { 618 | "version": "3.3.2", 619 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 620 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 621 | }, 622 | "vary": { 623 | "version": "1.1.2", 624 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 625 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 626 | }, 627 | "websocket-stream": { 628 | "version": "5.5.0", 629 | "resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-5.5.0.tgz", 630 | "integrity": "sha512-EXy/zXb9kNHI07TIMz1oIUIrPZxQRA8aeJ5XYg5ihV8K4kD1DuA+FY6R96HfdIHzlSzS8HiISAfrm+vVQkZBug==", 631 | "requires": { 632 | "duplexify": "^3.5.1", 633 | "inherits": "^2.0.1", 634 | "readable-stream": "^2.3.3", 635 | "safe-buffer": "^5.1.2", 636 | "ws": "^3.2.0", 637 | "xtend": "^4.0.0" 638 | }, 639 | "dependencies": { 640 | "ws": { 641 | "version": "3.3.3", 642 | "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", 643 | "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", 644 | "requires": { 645 | "async-limiter": "~1.0.0", 646 | "safe-buffer": "~5.1.0", 647 | "ultron": "~1.1.0" 648 | } 649 | } 650 | } 651 | }, 652 | "wkt-parser": { 653 | "version": "1.2.3", 654 | "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.2.3.tgz", 655 | "integrity": "sha512-s7zrOedGuHbbzMaQOuf8HacuCYp3LmmrHjkkN//7UEAzsYz7xJ6J+j/84ZWZkQcrRqi3xXyuc4odPHj7PEB0bw==" 656 | }, 657 | "wrappy": { 658 | "version": "1.0.2", 659 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 660 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 661 | }, 662 | "ws": { 663 | "version": "5.2.2", 664 | "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", 665 | "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", 666 | "requires": { 667 | "async-limiter": "~1.0.0" 668 | } 669 | }, 670 | "xtend": { 671 | "version": "4.0.1", 672 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", 673 | "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" 674 | } 675 | } 676 | } 677 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arcgis_websockets", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "subscribe_arcgis.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "colors": "^1.3.3", 14 | "event-stream": "^4.0.1", 15 | "finalhandler": "^1.1.2", 16 | "fs-extra": "^8.0.1", 17 | "jsonlint": "^1.6.3", 18 | "moment": "^2.24.0", 19 | "proj4": "^2.5.0", 20 | "router": "^1.3.3", 21 | "stream-chain": "^2.1.0", 22 | "stream-json": "^1.2.1", 23 | "through2": "^3.0.1", 24 | "uuid": "^3.3.2", 25 | "websocket-stream": "^5.5.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /streamserver/pipelines/custom.js: -------------------------------------------------------------------------------- 1 | /* 2 | This is where you can customize the transformation of your data from websockets 3 | 4 | If you want to add more steps in the final pipeline: 5 | 6 | - implement a function like this and add it to the array on the module.exports 7 | function your_step( context ) { 8 | return data => { 9 | ...do your stuff here 10 | return result // result has to be an object like data or null to skip it 11 | } 12 | } 13 | 14 | context = 15 | { 16 | geo : { lat : fieldLat, lon : fieldLon } || null, 17 | service : 18 | } 19 | 20 | data = { 21 | key : 22 | value : 23 | } 24 | 25 | 26 | */ 27 | 28 | const proj4 = require('proj4'); 29 | 30 | module.exports = [adaptPayload]; 31 | 32 | function adaptPayload (context) { 33 | return data => { 34 | if(context.geo !== null) { 35 | let data_lat = data.value[context.geo.lat]; 36 | let data_lon = data.value[context.geo.lon]; 37 | if (data_lat !== 0 && data_lon !== 0 ) { 38 | // Reprojection according to conf. 39 | try { 40 | let [lon,lat] = proj4(proj4.defs(`EPSG:${context.service.out_sr.latestWkid}`),[data_lon,data_lat]) 41 | let fixed = { 42 | geometry : { 43 | x : lon, y : lat, 44 | spatialReference : context.service.out_sr 45 | }, 46 | attributes : data.value 47 | }; 48 | fixed.attributes.FltId = data.value.id_str; 49 | data.value = fixed; 50 | 51 | return data; 52 | } catch(err) { 53 | console.error(`Failed re-projection [${err}]`); 54 | console.log(`${data_lat} || ${data_lon}`); 55 | return null; 56 | } 57 | } else { 58 | return null 59 | } 60 | } else { 61 | return null 62 | } 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /streamserver/pipelines/default.js: -------------------------------------------------------------------------------- 1 | const {parser} = require('stream-json/Parser'); 2 | const {streamValues} = require('stream-json/streamers/StreamValues'); 3 | const {chain} = require('stream-chain'); 4 | const CUSTOM_PIPELINE = require('./custom.js'); 5 | 6 | function _injectCtx (arr,ctx) { 7 | return arr.map(fn => fn(ctx)); 8 | } 9 | 10 | function sanityCheck(arr) { 11 | let length = arr.length; 12 | return Array.isArray(arr) && arr.filter(fn => typeof(fn) === "function").length === length; 13 | } 14 | 15 | function compose(ctx) { 16 | let pipeline = [ 17 | parser({jsonStreaming: true}), 18 | streamValues() 19 | ]; 20 | if (sanityCheck(CUSTOM_PIPELINE)) { 21 | pipeline.push(..._injectCtx(CUSTOM_PIPELINE,ctx)); 22 | } else { 23 | console.log(`Default Pipeline setup...[Skipping custom pipeline]`); 24 | if (CUSTOM_PIPELINE.length > 0) { 25 | console.warn(`Something is wrong : Please review your custom pipeline`); 26 | process.exit(12); 27 | } 28 | } 29 | 30 | return pipeline; 31 | } 32 | 33 | module.exports = compose; 34 | -------------------------------------------------------------------------------- /streamserver/streamserver_express.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | const expressWebSocket = require('express-ws'); 3 | const websocket = require('websocket-stream'); 4 | const websocketStream = require('websocket-stream/stream'); 5 | const {parser} = require('stream-json/Parser'); 6 | const {streamValues} = require('stream-json/streamers/StreamValues'); 7 | const {chain} = require('stream-chain'); 8 | const uuidv4 = require('uuid/v4'); 9 | const proj4 = require('proj4'); 10 | const esriTypes = require('./utils/esri_types.js'); 11 | const streamServerFilter = require('./src/filter_utils.js'); 12 | 13 | function setup(serviceName) { 14 | var CONF; 15 | try { 16 | let { 17 | hostname, 18 | port, 19 | protocol 20 | } = new URL(process.argv[2]); 21 | CONF = { 22 | ws: { 23 | server: { 24 | port: process.env["NGROK"] ? null : 9000, 25 | protocol: "ws", 26 | host: process.env["NGROK"] || "localhost" 27 | }, 28 | client: { 29 | protocol: protocol.replace(/:/g, ""), 30 | host: hostname, 31 | port: port, 32 | geo_fields: null 33 | } 34 | }, 35 | service: { 36 | name: serviceName, 37 | fieldObj: null, 38 | out_sr: { 39 | wkid: 102100, 40 | latestWkid: 3857 41 | } 42 | } 43 | }; 44 | } catch (err) { 45 | console.log(`ws initialization failed! reason : [${err}]`); 46 | process.exit(12); 47 | } 48 | return CONF; 49 | } 50 | 51 | function updateServiceInfo(obj) { 52 | let service = require('./templates/service.json'); 53 | Object.keys(obj).forEach(k => { 54 | service[k] = obj[k]; 55 | }); 56 | return service; 57 | } 58 | 59 | const JSAPI_VERSION = process.argv[3] || "4.11"; 60 | 61 | function doChallenge() { 62 | return !/^(3\.[1-9][0-9]|4\.[1-8]?)$/.test(JSAPI_VERSION); 63 | } 64 | 65 | function guessGeoFields(arr) { 66 | let candidates = arr 67 | .filter(fieldObj => fieldObj.type === "esriFieldTypeDouble") 68 | .filter(fieldObj => /\b(latitude|longitude|lat|lon|x|y)\b/.test(fieldObj.name)); 69 | 70 | if (candidates.length >= 2) { 71 | console.log(`found geofields candidates:`); 72 | candidates.map(e => console.log(e.name)) 73 | } 74 | return { 75 | latField: "lat", 76 | lonField: "lon" 77 | } 78 | } 79 | 80 | function start(conf) { 81 | // Maybe we can move this to setup() 82 | const SERVICE_NAME = conf.service.name; 83 | const BASE_URL = `/arcgis/rest/services/${SERVICE_NAME}/StreamServer`; 84 | let wsClientPort = conf.ws.client.port ? 85 | `:${conf.ws.client.port}` : 86 | ""; 87 | let wsServerPort = conf.ws.server.port ? 88 | `:${conf.ws.server.port}` : 89 | ""; 90 | let wsClientUrl = `${conf.ws.client.protocol}://${conf.ws.client.host}${wsClientPort}`; 91 | let wsServerUrl = `${conf.ws.server.protocol}://${conf.ws.server.host}${wsServerPort}${BASE_URL}`; 92 | let fields = esriTypes.convertToEsriFields(conf.service.fieldObj); 93 | let { 94 | latField, 95 | lonField 96 | } = guessGeoFields(fields); 97 | 98 | let serviceRes = updateServiceInfo({ 99 | fields: fields, 100 | streamUrls: [{ 101 | "transport": "ws", 102 | "urls": [ 103 | //`wss://${wsUrl}`, 104 | `${wsServerUrl}` 105 | ] 106 | }] 107 | }); 108 | 109 | var app = express(); 110 | // CORS middleware 111 | app.use(function(req, res, next) { 112 | res.header("Access-Control-Allow-Origin", "*"); 113 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 114 | next(); 115 | }); 116 | 117 | app.get(`${BASE_URL}`, function(req, res, next) { 118 | res.status(200).json(serviceRes); 119 | res.end(); 120 | }); 121 | 122 | // extend express app with app.ws() 123 | expressWebSocket(app, null, { 124 | perMessageDeflate: false, 125 | }); 126 | 127 | let pullStream = websocket(wsClientUrl, { 128 | perMessageDeflate: false 129 | }); 130 | 131 | app.ws('/subscribe', function(ws, req) { 132 | 133 | // convert ws instance to stream 134 | const stream = websocketStream(ws, { 135 | // websocket-stream options here 136 | binary: true, 137 | }); 138 | 139 | stream.socket.uuid = uuidv4(); 140 | stream.socket.challenge = false; 141 | var filter = false; 142 | stream.on('data', function(buf) { 143 | let data = buf.toString(); 144 | console.log(`${data} from [${stream.socket.uuid}]`); 145 | if (!connections[stream.socket.uuid].challenge && doChallenge()) { 146 | // Challenge 147 | stream.write(JSON.stringify({ 148 | error: null, 149 | ...JSON.parse(data) 150 | })); 151 | console.log("Challenge done!"); 152 | connections[stream.socket.uuid].challenge = true; 153 | } else { 154 | // Filters 155 | try { 156 | connections[stream.socket.uuid].filter = JSON.parse(data).filter.where; 157 | filter = true; 158 | } catch (err) { 159 | console.log(`Invalid filter received from ${stream.socket.uuid}: ${data}`); 160 | }; 161 | } 162 | }); 163 | stream.on('close', function() { 164 | console.log(`client [${stream.socket.uuid}] disconnected`); 165 | delete connections[stream.socket.uuid]; 166 | }) 167 | 168 | let pipeline = chain([ 169 | parser({ 170 | jsonStreaming: true 171 | }), 172 | streamValues(), 173 | data => { 174 | return filter ? 175 | streamServerFilter(data.value, connections[stream.socket.uuid].filter) ? 176 | data : 177 | null : 178 | data; 179 | }, 180 | data => { 181 | // Reprojection according to conf. 182 | let [lon, lat] = proj4(proj4.defs(`EPSG:${serviceRes.service.out_sr.latestWkid}`), [data.value[lonField], data.value[latField]]) 183 | data.value[latField] = lat; 184 | data.value[lonField] = lon; 185 | return data; 186 | 187 | }, 188 | data => Buffer.from(JSON.stringify(data.value)) 189 | ]); 190 | 191 | connections[stream.socket.uuid] = stream.socket; 192 | pullStream 193 | .pipe(pipeline) 194 | .pipe(stream) 195 | 196 | }); 197 | 198 | app.listen(conf.ws.server.port) 199 | } 200 | 201 | 202 | 203 | module.exports = { 204 | start: start, 205 | setup: setup 206 | }; 207 | -------------------------------------------------------------------------------- /streamserver/streamserver_simple.js: -------------------------------------------------------------------------------- 1 | const websocket = require('websocket-stream'); 2 | const http = require('http'); 3 | const Router = require('router'); 4 | const finalhandler = require('finalhandler'); 5 | const {chain} = require('stream-chain'); 6 | const uuidv4 = require('uuid/v4'); 7 | const esriTypes = require('./utils/esri_types.js'); 8 | const defaultPipeline = require('./pipelines/default.js'); 9 | 10 | const JSAPI_VERSION = process.argv[3] || "4.11"; 11 | 12 | function _doChallenge() { 13 | return !/^(3\.[1-9][0-9]|4\.[1-8]?)$/.test(JSAPI_VERSION); 14 | } 15 | 16 | function _updateServiceInfo(obj) { 17 | let service = require('../templates/service.json'); 18 | Object.keys(obj).forEach(k => { 19 | service[k] = obj[k]; 20 | }); 21 | return service; 22 | } 23 | 24 | function _guessGeoFields (arr) { 25 | let latFieldsNames = ["latitude","coordenadas_y","lat","y"]; 26 | let lonFieldsNames = ["longitude","coordenadas_x","long","lon","lng","x"]; 27 | let regexLat = new RegExp(`\\b(${latFieldsNames.join("|")})\\b`, "i"); 28 | let regexLon = new RegExp(`\\b(${lonFieldsNames.join("|")})\\b`, "i"); 29 | // Lat & and Lon has to be float numbers 30 | let candidates = arr 31 | .filter(fieldObj => fieldObj.type === "esriFieldTypeDouble"); 32 | 33 | let names = candidates.map(e => e.name); 34 | let latField = names.find(name => regexLat.test(name)); 35 | let lonField = names.find(name => regexLon.test(name)); 36 | // TO BE Reviewed 37 | return { 38 | latField : latField || null, 39 | lonField : lonField || null 40 | } 41 | } 42 | 43 | function _setup(config) { 44 | let fields = esriTypes.convertToEsriFields(config.payload); 45 | let newConfig = { 46 | ws : { 47 | server : { 48 | port : config.service.port, 49 | protocol : "ws", 50 | host : config.service.host, 51 | }, 52 | client : {...config.client} 53 | }, 54 | service : {...config.service, 55 | info : _updateServiceInfo({ fields : fields}), 56 | base_url : `/arcgis/rest/services/${config.service.name}/StreamServer` 57 | } 58 | }; 59 | 60 | return newConfig; 61 | } 62 | 63 | function _setupHTTPServer(serviceConf){ 64 | var router = Router(); 65 | let isNgrok = /ngrok.io/.test(serviceConf.host); 66 | let wsServerPort = isNgrok 67 | ? "" 68 | : serviceConf.port 69 | ? `:${serviceConf.port}` 70 | : ""; 71 | let wsServerUrl = `${isNgrok ? "wss" : "ws"}://${serviceConf.host}${wsServerPort}${serviceConf.base_url}`; 72 | // Update serviceConf.info prior to serve it from HTTPServer 73 | serviceConf.info.streamUrls = [{ 74 | transport : "ws", 75 | urls: [ 76 | //`wss://${wsUrl}`, 77 | `${wsServerUrl}` 78 | ] 79 | }]; 80 | 81 | router.get(`${serviceConf.base_url}`, function (req, res) { 82 | res.setHeader('Content-Type', 'application/json; charset=utf-8'); 83 | res.setHeader("Access-Control-Allow-Origin", "*"); 84 | res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 85 | res.statusCode = 200; 86 | res.write(JSON.stringify(serviceConf.info)); 87 | res.end(); 88 | 89 | }); 90 | 91 | // Retro-compatibility end-point (versions before 4.8) 92 | router.get(`/arcgis/rest/info`, function (req, res) { 93 | res.setHeader('Content-Type', 'application/json; charset=utf-8'); 94 | res.setHeader("Access-Control-Allow-Origin", "*"); 95 | res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 96 | res.statusCode = 200; 97 | res.write(JSON.stringify({ 98 | currentVersion: 10.5, 99 | fullVersion: "10.5.0", 100 | authInfo: { 101 | isTokenBasedSecurity: false 102 | } 103 | })); 104 | res.end(); 105 | }); 106 | 107 | let server = http.createServer(function(req, res) { 108 | router(req, res, finalhandler(req, res)); 109 | }); 110 | 111 | server.listen(serviceConf.port, function() { 112 | console.log(`Your StreamServer is ready on [${serviceConf.protocol}://${serviceConf.host}${wsServerPort}${serviceConf.base_url}]`); 113 | }); 114 | 115 | return server; 116 | } 117 | 118 | function _setupSource(obj) { 119 | //console.log( `WS Server ready at [${conf.ws.client.wsUrl}/${BASE_URL}/subscribe]`); 120 | return function handle(stream, request) { 121 | var serverRef = this; 122 | stream.binary = false; 123 | stream.socket.uuid = uuidv4(); 124 | console.log(`client [${stream.socket.uuid}] connected`); 125 | stream.socket.challenge = false; 126 | stream.on('data', function(buf){ 127 | let data = buf.toString(); 128 | console.log(`${data} from [${stream.socket.uuid}]`); 129 | if (!stream.socket.challenge && _doChallenge()) { 130 | // Challenge 131 | try { 132 | stream.write(JSON.stringify({ 133 | error: null, 134 | ...JSON.parse(data) 135 | })); 136 | } catch(err) { 137 | console.log(`bad payload[${data}]`); 138 | } 139 | console.log("Challenge done!"); 140 | stream.socket.challenge = true; 141 | } else { 142 | // Filters 143 | try{ 144 | stream.socket.filter = JSON.parse(data).filter.where; 145 | }catch(err){ 146 | console.log(`Invalid filter received from ${stream.socket.uuid}: ${data}`); 147 | }; 148 | } 149 | }); 150 | stream.on('close',function(){ 151 | console.log(`client [${stream.socket.uuid}] disconnected`); 152 | serverRef.clients.delete(stream.socket); 153 | stream.end(); 154 | }); 155 | 156 | let pipeline = chain([ 157 | ...defaultPipeline({ geo : obj.geo, service : obj.service}), 158 | data => { 159 | return stream.socket.hasOwnProperty("filter") 160 | ? streamServerFilter(data.value,stream.socket.filter) 161 | ? data 162 | : null 163 | : data; 164 | }, 165 | data => JSON.stringify(data.value) 166 | ]); 167 | 168 | pipeline.on("error", function(err){ 169 | console.error(err); 170 | }) 171 | 172 | obj.pullStream 173 | .pipe(pipeline) 174 | .pipe(stream) 175 | } 176 | } 177 | 178 | function start(cfg){ 179 | // First some Plumbing... 180 | let conf = _setup(cfg); 181 | let avoidGeo = false; 182 | let {latField,lonField} = _guessGeoFields(conf.service.info.fields); 183 | 184 | if (latField === null || lonField === null ) { 185 | avoidGeo = true; 186 | console.warn("Unable to find field candidates on payload. Skipping Re-Projecction"); 187 | } else { 188 | console.log(`Found spatial information in fields [${lonField},${latField}]`); 189 | } 190 | 191 | let HTTPServer = _setupHTTPServer(conf.service); 192 | let wsRemoteClient = websocket(conf.ws.client.wsUrl, { 193 | perMessageDeflate: false 194 | }); 195 | 196 | var fieldGeo = avoidGeo 197 | ? null 198 | : { 199 | lat : latField, 200 | lon : lonField 201 | }; 202 | 203 | var wss = websocket.createServer({ 204 | server: HTTPServer, 205 | path : `${conf.service.base_url}/subscribe`, 206 | binary: false }, 207 | _setupSource({ 208 | pullStream : wsRemoteClient, 209 | service: conf.service, 210 | geo : fieldGeo 211 | })) 212 | } 213 | 214 | module.exports = { 215 | start: start 216 | }; 217 | -------------------------------------------------------------------------------- /streamserver/utils/esri_types.js: -------------------------------------------------------------------------------- 1 | // Got & adapted from from https://github.com/koopjs/FeatureServer/blob/master/src/utils.js 2 | 3 | const moment = require('moment') 4 | const DATE_FORMATS = [moment.ISO_8601] 5 | 6 | function convertToEsriFields (obj) { 7 | return Object.entries(obj).map(([key,value]) => getField(key,value)); 8 | } 9 | 10 | function getField (k,v) { 11 | return { 12 | name: k, 13 | type: esriTypeMap(detectType(v)), 14 | alias: k, 15 | nullable: true 16 | } 17 | } 18 | 19 | function detectType (value) { 20 | var type = typeof value 21 | 22 | if (type === 'number') { 23 | return Number.isInteger(value) ? 'Integer' : 'Double' 24 | } else if (type && moment(value, DATE_FORMATS, true).isValid()) { 25 | return 'Date' 26 | } else { 27 | return 'String' 28 | } 29 | } 30 | 31 | function esriTypeMap (type) { 32 | switch (type.toLowerCase()) { 33 | case 'double': 34 | return 'esriFieldTypeDouble' 35 | case 'integer': 36 | return 'esriFieldTypeInteger' 37 | case 'date': 38 | return 'esriFieldTypeDate' 39 | case 'blob': 40 | return 'esriFieldTypeBlob' 41 | case 'geometry': 42 | return 'esriFieldTypeGeometry' 43 | case 'globalid': 44 | return 'esriFieldTypeGlobalID' 45 | case 'guid': 46 | return 'esriFieldTypeGUID' 47 | case 'raster': 48 | return 'esriFieldTypeRaster' 49 | case 'single': 50 | return 'esriFieldTypeSingle' 51 | case 'smallinteger': 52 | return 'esriFieldTypeSmallInteger' 53 | case 'xml': 54 | return 'esriFieldTypeXML' 55 | case 'string': 56 | default: 57 | return 'esriFieldTypeString' 58 | } 59 | } 60 | 61 | module.exports = { 62 | convertToEsriFields: convertToEsriFields 63 | } 64 | -------------------------------------------------------------------------------- /streamserver/utils/filter_utils.js: -------------------------------------------------------------------------------- 1 | var opsExt = '(AND|OR)'; 2 | var opsInt = '(NOT)?(=|<>|LIKE|IS)(NOT)?'; 3 | var reOpsExt = new RegExp(`\\)\\s${opsExt}\\s\\(`,"gi"); 4 | var reOpsInt = new RegExp(`\\s${opsInt}\\s`,"gi"); 5 | var str = `(ciudadanos = '1') AND (ciudadanos <> '1') AND (ciudadanos LIKE '1%') AND (ciudadanos LIKE '%1') AND (ciudadanos LIKE '%1%') AND (ciudadanos NOT LIKE '%1%') AND (ciudadanos IS NULL) AND (ciudadanos IS NOT NULL)`; 6 | const util = require("util"); 7 | 8 | function buildQuery(field, op, value) { 9 | return `(${field} ${op} ${value})`; 10 | } 11 | 12 | function normalizeValue (str) { 13 | 14 | let safestr = str.replace(/'/g,"").replace("%",""); 15 | let result = safestr; 16 | if (/(true|false)/i.test(safestr)) { 17 | result = safestr.replace(/'/g,"").toLowerCase() === "false" ? false : true; 18 | } 19 | if (/null/i.test(safestr)) { 20 | result = null; 21 | } 22 | if (/\b[0-9]+\b/.test(safestr)) { 23 | result = parseInt(safestr); 24 | } 25 | if (/\b[0-9\.]+\b/.test(safestr)) { 26 | result = parseFloat(safestr); 27 | } 28 | return result; 29 | } 30 | 31 | var operators = { 32 | "=" : function(data,op1,op2) { 33 | let op2fixed = normalizeValue(op2); 34 | return data.hasOwnProperty(op1) && data[op1] === op2fixed; 35 | }, 36 | "IS" : function(data,op1,op2) { 37 | let op2fixed = normalizeValue(op2); 38 | let cond = data[op1] === false && op2fixed === null 39 | ? true 40 | : data[op1] == op2fixed; 41 | return data.hasOwnProperty(op1) && cond; 42 | }, 43 | "<>" : function(data,op1,op2) { 44 | let op2fixed = normalizeValue(op2); 45 | return data.hasOwnProperty(op1) && data[op1] !== op2 46 | }, 47 | "IS NOT" : function(data,op1,op2) { 48 | let op2fixed = normalizeValue(op2); 49 | let cond = data[op1] === false && op2fixed === null 50 | ? true 51 | : data[op1] == op2fixed; 52 | return data.hasOwnProperty(op1) && data[op1] !== op2fixed 53 | }, 54 | "LIKE" : function(data,op1,op2) { 55 | let op2fixed = normalizeValue(op2); 56 | let re = new RegExp(op2fixed,"gi"); 57 | return data.hasOwnProperty(op1) && re.test(data[op1]) 58 | }, 59 | "NOT LIKE" : function(data,op1,op2) { 60 | let op2fixed = normalizeValue(op2); 61 | let re = new RegExp(op2fixed,"gi"); 62 | return data.hasOwnProperty(op1) && !re.test(data[op1]) 63 | }, 64 | "CONTAINS" : function(data,op1,op2) { 65 | let op2fixed = normalizeValue(op2); 66 | let re = new RegExp(op2fixed, "gi"); 67 | return data.hasOwnProperty(op1) && re.test(data[op1]) 68 | }, 69 | "NOT CONTAINS" : function(data,op1,op2) { 70 | let op2fixed = normalizeValue(op2); 71 | let re = new RegExp(op2fixed, "gi"); 72 | return data.hasOwnProperty(op1) && !re.test(data[op1]) 73 | } 74 | } 75 | 76 | function translate (d,arr) { 77 | // ["ciudadanos", "=", "true"] 78 | //console.log(`filter : ${arr.join(" ")}`); 79 | return operators[arr[1]](d,arr[0],arr[2]); 80 | 81 | } 82 | 83 | 84 | 85 | function evaluateQuery(d, queryStr) { 86 | let operatorChain = queryStr.split(/[^(AND|OR)]/).filter(el => /(AND|OR)/.test(el)); 87 | var lista = queryStr.split(reOpsExt) 88 | .map(exp => exp.replace(/(\(|\))/g,"")) 89 | .filter(el => !/(AND|OR)/.test(el.replace(/"/g, ""))) 90 | .map(exp => exp.split(/(NOT LIKE|LIKE|IS NOT|IS|NOT CONTAINS|CONTAINS|=|<>)/)) 91 | .map(exp => exp.map(el => el.trim().replace("%",""))) 92 | .map(exp => translate(d,exp)); 93 | 94 | 95 | let result = lista.length === 1 96 | ? lista[0] 97 | : lista.reduce((old,cur,i,arr) => { 98 | if(i < operatorChain.length) { 99 | old = i < arr.length 100 | ? operatorChain[i] === "AND" 101 | ? arr[i] && arr[i+1] 102 | : arr[i] || arr[i+1] 103 | : old; 104 | return old; 105 | } else { 106 | return old; 107 | } 108 | },true); 109 | 110 | //console.log(`data : [${util.inspect(d, { compact: true, depth: 5, breakLength: 80 })}]\nquery [${queryStr}] -> ${lista} -> ${result}` ); 111 | return result; 112 | 113 | } 114 | 115 | module.exports = evaluateQuery; 116 | -------------------------------------------------------------------------------- /templates/service.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": null, 3 | "objectIdField": null, 4 | "displayField": "id_str", 5 | "timeInfo": { 6 | "trackIdField": "id_str", 7 | "startTimeField": "MsgTime", 8 | "endTimeField": null 9 | }, 10 | "geometryType": "esriGeometryPoint", 11 | "geometryField": "Location", 12 | "spatialReference": { 13 | "wkid": 4326, 14 | "latestWkid": 4326 15 | }, 16 | "drawingInfo": { 17 | "renderer": { 18 | "type": "simple", 19 | "description": "", 20 | "symbol": { 21 | "color": [ 22 | 0, 23 | 169, 24 | 230, 25 | 131 26 | ], 27 | "size": 9, 28 | "angle": 0, 29 | "xoffset": 0, 30 | "yoffset": 0, 31 | "type": "esriSMS", 32 | "style": "esriSMSCircle", 33 | "outline": { 34 | "color": [ 35 | 255, 36 | 255, 37 | 255, 38 | 255 39 | ], 40 | "width": 1.125 41 | } 42 | } 43 | } 44 | 45 | }, 46 | "fields": null, 47 | "currentVersion": "10.5", 48 | "streamUrls": null, 49 | "capabilities": "broadcast,subscribe" 50 | } 51 | -------------------------------------------------------------------------------- /utils/test2.json: -------------------------------------------------------------------------------- 1 | {"category":null,"icon_url":"https://assets.chucknorris.host/img/avatar/chuck-norris.png","id":"TSKPCFDcTe-bJxrcrVNHkQ","url":"https://api.chucknorris.io/jokes/TSKPCFDcTe-bJxrcrVNHkQ","value":"Jesus wears a bracelet that says wwcnd (what would Chuck Norris do?)"} 2 | -------------------------------------------------------------------------------- /utils/test3.json: -------------------------------------------------------------------------------- 1 | {"a": 1, "b": "a", "c": [], "d": {}, "e": true} -------------------------------------------------------------------------------- /utils/test_json_ws.js: -------------------------------------------------------------------------------- 1 | const websocket = require('websocket-stream'); 2 | const http = require('http'); 3 | const through2 = require('through2'); 4 | const {parser} = require('stream-json/Parser'); 5 | const {streamValues} = require('stream-json/streamers/StreamValues'); 6 | const {chain} = require('stream-chain'); 7 | const fs = require('fs'); 8 | 9 | var server = http.createServer(function(request, response) { 10 | response.writeHead(404); 11 | response.end(); 12 | }); 13 | 14 | server.listen(9000, function() { 15 | console.log(`${(new Date())} Server is listening on port 9000`); 16 | }); 17 | 18 | 19 | 20 | 21 | var wss = websocket.createServer({server: server}, handle) 22 | function handle(stream, request) { 23 | // `request` is the upgrade request sent by the client. 24 | 25 | const pipeline = chain([ 26 | fs.createReadStream('test2.json'), 27 | parser({packValues: true}), 28 | streamValues(), 29 | data => JSON.stringify(data.value) 30 | ]); 31 | 32 | pipeline.on('data', (data) => console.log(data)); 33 | pipeline 34 | .pipe(stream); 35 | } 36 | -------------------------------------------------------------------------------- /utils/websocket_utils.js: -------------------------------------------------------------------------------- 1 | const websocket = require('websocket-stream'); 2 | const streamjson = require('stream-json'); 3 | const {parser} = require('stream-json/Parser'); 4 | const {streamValues} = require('stream-json/streamers/StreamValues'); 5 | const {chain} = require('stream-chain'); 6 | 7 | 8 | module.exports = function(wsUrl) { 9 | return new Promise((resolve, reject) => { 10 | try { 11 | let {hostname,port,protocol} = new URL(wsUrl); 12 | let ws = websocket(wsUrl); 13 | const pipemod = chain([ 14 | parser({packValues: true}), 15 | streamValues(), 16 | data => { 17 | ws.unpipe(); 18 | resolve({ 19 | payload : data.value, 20 | client : { 21 | host : hostname, 22 | port : port, 23 | protocol : protocol, 24 | wsUrl : wsUrl 25 | } 26 | }); 27 | } 28 | ]); 29 | 30 | ws.on("error", function(err){ 31 | reject(`Cannot connect to [${wsUrl}]`); 32 | }) 33 | 34 | pipemod 35 | .on("error", function(err){ 36 | reject(err); 37 | }); 38 | 39 | ws.pipe(pipemod); 40 | } catch(err) { 41 | reject(err); 42 | } 43 | 44 | }) 45 | } 46 | --------------------------------------------------------------------------------