├── .gitignore ├── LICENSE ├── README.md ├── demo ├── README.md ├── demo_config.sh ├── index.html ├── package.json ├── points.html └── pointserver.js ├── lib └── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | *.DS_Store 28 | *.zip 29 | *.vrt 30 | *.csv 31 | *.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Faraday 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TILESPLASH 2 | ========== 3 | 4 | A light and quick nodejs webserver for serving topojson and mapbox vector tiles from a [postgis](http://www.postgis.net/) backend. inspired by [Michal Migurski](http://mike.teczno.com/)'s [TileStache](http://tilestache.org/). Works great for powering [Mapbox-GL-based](https://www.mapbox.com/mapbox-gl-js/example/set-perspective/) apps like this: 5 | 6 |  7 | 8 | # Dependencies 9 | 10 | Tilesplash depends on `node` and `npm` 11 | 12 | # Installation 13 | 14 | ```bash 15 | npm install tilesplash 16 | ``` 17 | 18 | # Example 19 | 20 | Here's a simple tile server with one layer 21 | 22 | ```javascript 23 | var Tilesplash = require('tilesplash'); 24 | 25 | // invoke tilesplash with DB options 26 | var app = new Tilesplash({ 27 | user: myUser, 28 | password: myPassword, 29 | host: localhost, 30 | port: 5432, 31 | database: myDb 32 | }); 33 | 34 | // define a layer 35 | app.layer('test_layer', function(tile, render){ 36 | render('SELECT ST_AsGeoJSON(the_geom) as the_geom_geojson FROM layer WHERE ST_Intersects(the_geom, !bbox_4326!)'); 37 | }); 38 | 39 | // serve tiles at port 3000 40 | app.server.listen(3000); 41 | ``` 42 | 43 | - Topojson tiles will be available at `http://localhost:3000/test_layer/{z}/{x}/{y}.topojson` 44 | - Mapbox vector tiles will be available at `http://localhost:3000/test_layer/{z}/{x}/{y}.mvt` 45 | 46 | (See [client implementation examples](https://github.com/faradayio/tilesplash#client) below, and complete demo implementation in [`demo/`](demo/README.md)) 47 | 48 | # Usage 49 | 50 | ## `new Tilesplash(connection_details, [cacheType])` 51 | 52 | creates a new tilesplash server using the given postgres database 53 | 54 | ```javascript 55 | var dbConfig = { 56 | user: username, 57 | password: password, 58 | host: hostname, 59 | port: 5432, 60 | database: dbname 61 | } 62 | 63 | var app = new Tilesplash(dbConfig); 64 | ``` 65 | 66 | To cache using redis, pass `'redis'` as the second argument. Otherwise an in-process cache will be used. 67 | 68 | ### `Tilesplash.server` 69 | 70 | an [express](http://expressjs.com/) object, mostly used internally but you can use it to add middleware for authentication, browser caching, gzip, etc. 71 | 72 | ### `Tilesplash.layer(name, [middleware, ...], [mvtOptions], callback)` 73 | 74 | __name__: the name of your layer. Tiles will be served at /__name__/z/x/y.topojson 75 | 76 | __middleware__: a [middleware function](#middleware) 77 | 78 | __mvtOptions__: optional [mapnik parameters](http://mapnik.org/documentation/node-mapnik/3.5/#VectorTile.addGeoJSON), e.g. `{ strictly_simple: true }` 79 | 80 | __callback__: your tile building function with the following arguments. function([tile](#tile), [render](#render)) 81 | 82 | 83 | #### Simple layer 84 | 85 | This layer renders tiles containing geometry from the `the_geom` column in `test_table` 86 | 87 | ```javascript 88 | app.layer('simpleLayer', function(tile, render){ 89 | render('SELECT ST_AsGeoJSON(the_geom) as the_geom_geojson FROM test_table WHERE ST_Intersects(the_geom, !bbox_4326!)'); 90 | }); 91 | ``` 92 | 93 | #### Combined layers 94 | 95 | Tilesplash can render tiles from multiple queries at once 96 | 97 | ```javascript 98 | app.layer('multiLayer', function(tile, render){ 99 | render({ 100 | circles: 'SELECT ST_AsGeoJSON(the_geom) as the_geom_geojson FROM circles WHERE ST_Intersects(the_geom, !bbox_4326!)', 101 | squares: 'SELECT ST_AsGeoJSON(the_geom) as the_geom_geojson FROM squares WHERE ST_Intersects(the_geom, !bbox_4326!)' 102 | }); 103 | }); 104 | ``` 105 | 106 | In fact, `simpleLayer` is just a shorthand for `multiLayer` with the only query named `vectile`. 107 | The code below is equivalent to the _simple layer example_: 108 | 109 | ```javascript 110 | app.layer('multiLayer', function(tile, render){ 111 | render({ 112 | vectile: 'SELECT ST_AsGeoJSON(the_geom) as the_geom_geojson FROM test_table WHERE ST_Intersects(the_geom, !bbox_4326!)', 113 | }); 114 | }); 115 | ``` 116 | 117 | Knowing the name of the query (i.e. `vectile`, `circles`, `squares`, etc.) is important when rendering tiles on the client. 118 | The value corresponds to `source-layer` in [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/) layer spec (see [example](https://www.mapbox.com/mapbox-gl-js/example/vector-source/)). 119 | 120 | 121 | #### Using mapnik geometry parameters 122 | 123 | This layer renders tiles containing geometry features simplified to a threshold of `4`. Full parameters are documented [here](http://mapnik.org/documentation/node-mapnik/3.5/#VectorTile.addGeoJSON). 124 | 125 | ```javascript 126 | app.layer('simpleLayer', { simplify_distance: 4 }, function(tile, render){ 127 | render('SELECT ST_AsGeoJSON(the_geom) as the_geom_geojson FROM test_table WHERE ST_Intersects(the_geom, !bbox_4326!)'); 128 | }); 129 | ``` 130 | 131 | #### Escaping variables 132 | 133 | Tilesplash has support for escaping variables in sql queries. You can do so by passing an array instead of a string wherever a sql string is accepted. 134 | 135 | ```javascript 136 | app.layer('escapedLayer', function(tile, render){ 137 | render(['SELECT ST_AsGeoJSON(the_geom) as the_geom_geojson FROM points WHERE ST_Intersects(the_geom, !bbox_4326!) AND state=$1', 'California']); 138 | }); 139 | 140 | app.layer('escapedMultiLayer', function(tile, render){ 141 | render({ 142 | hotels: ['SELECT ST_AsGeoJSON(the_geom) as the_geom_geojson FROM hotels WHERE ST_Intersects(the_geom, !bbox_4326!) AND state=$1', 'California'], 143 | restaurants: ['SELECT ST_AsGeoJSON(the_geom) as the_geom_geojson FROM restaurants WHERE ST_Intersects(the_geom, !bbox_4326!) AND state=$1', 'California'] 144 | }); 145 | }); 146 | ``` 147 | 148 | #### Restricting zoom level 149 | 150 | Sometimes you only want a layer to be visible on certain zoom levels. To do that, we simply render an empty tile when tile.z is too low or too high. 151 | 152 | ```javascript 153 | app.layer('zoomDependentLayer', function(tile, render){ 154 | if (tile.z < 8 || tile.z > 20) { 155 | render.empty(); //render an empty tile 156 | } else { 157 | render('SELECT ST_AsGeoJSON(the_geom) as the_geom_geojson FROM points WHERE ST_Intersects(the_geom, !bbox_4326!)'); 158 | } 159 | }); 160 | ``` 161 | 162 | You can also adapt your layer by zoom level to show different views in different situations. 163 | 164 | In this example we show data from the `heatmap` table when the zoom level is below 8, data from `points` up to zoom 20, and empty tiles when you zoom in further than that. 165 | 166 | ```javascript 167 | app.layer('fancyLayer', function(tile, render){ 168 | if (tile.z < 8) { 169 | render('SELECT ST_AsGeoJSON(the_geom) as the_geom_geojson FROM heatmap WHERE ST_Intersects(the_geom, !bbox_4326!)'); 170 | } else if (tile.z > 20) { 171 | render.empty(); 172 | } else { 173 | render('SELECT ST_AsGeoJSON(the_geom) as the_geom_geojson FROM points WHERE ST_Intersects(the_geom, !bbox_4326!)'); 174 | } 175 | }); 176 | ``` 177 | 178 | ## Middleware 179 | 180 | Middleware allows you to easily extend tilesplash to add additional functionality. Middleware is defined like this: 181 | 182 | ```javascript 183 | var userMiddleware = function(req, res, tile, next){ 184 | tile.logged_in = true; 185 | tile.user_id = req.query.user_id; 186 | next(); 187 | }; 188 | ``` 189 | 190 | You can layer include this in your layers 191 | 192 | ```javascript 193 | app.layer('thisOneHasMiddleware', userMiddleware, function(tile, render){ 194 | if (!tile.logged_in) { 195 | render.error(); 196 | } else { 197 | render(['SELECT ST_AsGeoJSON(the_geom) as the_geom_geojson FROM placesVisited WHERE ST_Intersects(the_geom, !bbox_4326!) AND visitor=$1', tile.user_id]); 198 | } 199 | }); 200 | ``` 201 | 202 | Middleware can be synchronous or asynchronous, just be sure to call `next()` when you're done! 203 | 204 | ## tile 205 | 206 | `tile` is a parameter passed to middleware and layer callbacks. It is an object containing information about the tile being requested. It will look something like this: 207 | 208 | ```javascript 209 | { 210 | x: 100, 211 | y: 100, 212 | z: 10, 213 | bounds: [w, s, e, n] //output from SphericalMercator.bounds(x,y,z) using https://github.com/mapbox/node-sphericalmercator 214 | bbox: 'BBOX SQL for webmercator', 215 | bbox_4326: 'BBOX SQL for 4326 projection' //you probably need this 216 | } 217 | ``` 218 | 219 | Anything in __tile__ can be substituted into your SQL query by wrapping it in exclamation marks like `!this!` 220 | 221 | You can add custom items into __tile__ like so: 222 | 223 | ```javascript 224 | tile.table = "states"; 225 | render('SELECT ST_AsGeoJSON(the_geom) as the_geom_geojson FROM !table! WHERE !bbox!') 226 | ``` 227 | 228 | Note that when you interpolate tile variables into your queries with the exclamation point syntax, that data will __not be escaped__. This allows you to insert custom SQL from tile variables, like with `!bbox!`, but it can be a security risk if you allow any user input to be interpolated that way. 229 | 230 | When you want to use user input in a query, see [Escaping variables](#escaping-variables) above. 231 | 232 | ## `render` 233 | 234 | `render` is the second argument passed to your layer callback function. You can use it to render different kinds of tiles. 235 | 236 | ### `render(sql)` 237 | 238 | Runs a SQL query and displays the result as a tile 239 | 240 | ### `render(object)` 241 | 242 | Runs multiple SQL queries and renders them in seperate topojson layers. See [Combined layers](#combined-layers) above. 243 | 244 | ### `render.query()` 245 | 246 | Alias of render() 247 | 248 | ### `render.queryFile(fileName)` 249 | 250 | Use this if your SQL is really long and/or you want to keep it seperate. 251 | 252 | ```javascript 253 | app.layer('complicatedLayer', function(tile, render){ 254 | render.queryFile('important_stuff/advanced_tile.sql'); 255 | }); 256 | ``` 257 | 258 | ### `render.empty()` 259 | 260 | Renders an empty tile 261 | 262 | ### `render.error()` 263 | 264 | Replies with a 500 error 265 | 266 | ### `render.raw(string or http code)` 267 | 268 | Sends a raw reply. I can't think of any reason you would want to do this, but feel free to experiment. 269 | 270 | ```javascript 271 | app.layer('smileyLayer', function(tile, render){ 272 | render.raw(':)'); 273 | }); 274 | ``` 275 | 276 | ```javascript 277 | app.layer('notThereLayer', function(tile, render){ 278 | render.raw(404); 279 | }); 280 | ``` 281 | 282 | ### `render.rawFile(fileName)` 283 | 284 | Replies with the specified file 285 | 286 | ```javascript 287 | app.layer('staticLayer', function(tile, render){ 288 | render.rawFile('thing.topojson'); 289 | }); 290 | ``` 291 | 292 | ## Caching 293 | 294 | Caching is very important. By default, Tilesplash uses an in-memory cache. You can use redis instead by passing `'redis'` as the second argument when initializing a Tilesplash server. 295 | 296 | There are two ways to implement caching. You can either do it globally or on a layer by layer basis. 297 | 298 | ### app.cache([keyGenerator], ttl) 299 | 300 | Use this to define caching across your entire application 301 | 302 | __`keyGenerator(tile)`__ 303 | 304 | keyGenerator is a function that takes a `tile` object as it's only parameter and returns a cache key (__string__) 305 | 306 | If you don't specify a key generator, `app.defaultCacheKeyGenerator` will be used, which returns a key derived from your database connection, tile layer, and tile x, y, and z. 307 | 308 | __`ttl`__ 309 | 310 | TTL stands for time-to-live. It's how long tiles will remain in your cache, and it's defined in milliseconds. For most applications, anywhere between one day (86400000) to one week (604800000) should be fine. 311 | 312 | __Example__ 313 | 314 | In this example, we have `tile.user_id` available to us and we don't want to show one user tiles belonging to another user. By starting with `app.defaultCacheKeyGenerator(tile)` we get a cache key based on things we already want to cache by (like `x`, `y`, and `z`) and we can then add `user_id` to prevent people from seeing cached tiles unless their `user_id` matches. 315 | 316 | ```javascript 317 | app.cache(function(tile){ 318 | return app.defaultCacheKeyGenerator(tile) + ':' + tile.user_id; //cache by tile.user_id as well 319 | }, 1000 * 60 * 60 * 24 * 30); //ttl 30 days 320 | ``` 321 | 322 | ### `this.cache([keyGenerator], ttl)` 323 | 324 | Layer-specific caching works identically to global caching as defined above, except that it only applies to one layer and you define it within that layer. 325 | 326 | In this example, slowLayer uses the same key generator as the rest of the app, but specifies a longer TTL. 327 | 328 | ```javascript 329 | app.cache(keyGenerator, 1000 * 60 * 60 * 24); //cache for one day 330 | 331 | app.layer('slowLayer', function(tile, render){ 332 | this.cache(1000 * 60 * 60 * 24 * 30); //cache for 30 days 333 | 334 | render.queryFile('slowQuery.sql'); 335 | }); 336 | ``` 337 | 338 | In this example, only slowLayer is cached. 339 | 340 | ```javascript 341 | app.layer('fastLayer', function(tile, render){ 342 | render.queryFile('fastQuery.sql'); 343 | }); 344 | 345 | var userMiddleware = function(req, res, tile, next){ 346 | tile.user_id = 1; 347 | next(); 348 | }; 349 | 350 | app.layer('slowLayer', userMiddleware, function(tile, render){ 351 | this.cache(function(tile){ 352 | return app.defaultCacheKeyGenerator(tile) + ':' + tile.user_id; 353 | }, 1000 * 60 * 60 * 24); //cache for one day 354 | 355 | render.queryFile('slowQuery.sql'); 356 | }); 357 | ``` 358 | 359 | ## Client 360 | Some in-browser examples of how to use the tiles generated by tilesplash: 361 | 362 | ### .mvt endpoint 363 | - [Mapbox GL third-party source example](https://www.mapbox.com/mapbox-gl-js/example/third-party/) 364 | - [Mapzen's Tangram](https://github.com/tangrams/tangram) 365 | - [Mapzen's d3 vector tiles implementation](http://mapzen.github.io/d3-vector-tiles) 366 | 367 | ### .topojson endpoint 368 | - [D3 + leaflet](http://bl.ocks.org/wboykinm/7393674) 369 | - [Mapzen Tangram](https://github.com/tangrams/tangram) 370 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Tilesplash local demo 2 | 3 | This is a simple [Mapbox GL](https://www.mapbox.com/mapbox-gl-js/api/) example of tilesplash in use, serving points from the [openaddresses project](https://openaddresses.io/). 4 | 5 |  6 | 7 | ## Dependencies 8 | 9 | - [PostgreSQL](https://www.postgresql.org/download/) 10 | - [PostGIS](http://postgis.net/install/) 11 | - [Node v6+](https://nodejs.org/en/download/) 12 | - [wget](https://www.gnu.org/software/wget/) 13 | 14 | ## Setup 15 | 16 | 1. Get a [Mapbox Token](http://mapbox.com/signup) and put it [in index.html](https://github.com/faradayio/tilesplash/blob/master/demo/index.html#L18) 17 | 18 | 2. Run this collection of should-really-be-in-a-docker-container commands to get sample data and configure the local DB: 19 | 20 | ``` 21 | bash demo_config.sh 22 | ``` 23 | 24 | ## Run the demo 25 | 26 | From this `demo/` directory: 27 | ``` 28 | npm install 29 | npm run points 30 | ``` 31 | 32 | Then visit [http://localhost:8000/points.html](http://localhost:8000/points.html) and watch as the glorious city of Denver unfolds in point form. -------------------------------------------------------------------------------- /demo/demo_config.sh: -------------------------------------------------------------------------------- 1 | # set up a testing environment for tilesplash using data from openaddresses 2 | 3 | # get the data 4 | wget -c https://s3.amazonaws.com/data.openaddresses.io/runs/227042/us/co/denver.zip 5 | # when feeling more ambitious, use all of the US West (~1GB): 6 | # wget -c https://s3.amazonaws.com/data.openaddresses.io/openaddr-collected-us_west.zip 7 | unzip denver.zip 8 | 9 | # create and configure the db 10 | dropdb tilesplash_demo --if-exists 11 | createdb tilesplash_demo 12 | psql tilesplash_demo -c "CREATE EXTENSION IF NOT EXISTS postgis" 13 | psql tilesplash_demo -c "CREATE TABLE oa ( 14 | lon float, 15 | lat float, 16 | number text, 17 | street text, 18 | unit text, 19 | city text, 20 | district text, 21 | region text, 22 | postcode text, 23 | id text, 24 | hash text 25 | )" 26 | psql tilesplash_demo -c "\COPY oa FROM 'us/co/denver.csv' CSV HEADER" 27 | psql tilesplash_demo -c "SELECT AddGeometryColumn ('public','oa','the_geom',4326,'GEOMETRY',2)" 28 | psql tilesplash_demo -c "UPDATE oa SET the_geom = ST_GeomFromText('POINT(' || lon || ' ' || lat || ')',4326)" 29 | 30 | echo "Ready to run the demo!" -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |