├── .gitignore ├── README.md ├── bower.json ├── build.sh ├── dist ├── hoverboard.js └── hoverboard.min.js ├── example.css ├── example.js ├── geojson.html ├── grenoble.png ├── index.html ├── package.json ├── src ├── hoverboard.js └── renderingInterface.js ├── topojson.html └── watch.sh /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hoverboard 2 | 3 | Render vector tiles on canvas with leaflet. 4 | 5 | ![O little town of Grenoble](grenoble.png) 6 | 7 | [Try it out](https://devtristan.github.io/hoverboard/) 8 | 9 | Supports: 10 | 11 | - [geojson](http://geojson.org/) 12 | - [topojson](https://github.com/mbostock/topojson/wiki) 13 | - [mapbox vector tiles (protobuf)](https://github.com/mapbox/vector-tile-spec) 14 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hoverboard", 3 | "version": "1.1.3", 4 | "homepage": "https://github.com/devTristan/hoverboard", 5 | "authors": [ 6 | "Tristan Davies " 7 | ], 8 | "description": "Render vector tiles on canvas with leaflet. Supports geojson, topojson, and mapbox vector tiles (protobuf)", 9 | "main": "src/hoverboard.js", 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "leaflet", 17 | "vector", 18 | "geojson", 19 | "topojson", 20 | "protobuf" 21 | ], 22 | "license": "MIT", 23 | "ignore": [ 24 | "**/.*", 25 | "node_modules", 26 | "bower_components" 27 | ], 28 | "dependencies": { 29 | "d3": "~3.5.3", 30 | "hidpi-canvas": "~1.0.9", 31 | "leaflet": "~0.7.3", 32 | "pbf": "~0.0.3", 33 | "topojson": "~1.6.18", 34 | "vector-tile": "~0.1.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | browserify -s Hoverboard src/hoverboard.js > dist/hoverboard.js 2 | uglify -s dist/hoverboard.js -o dist/hoverboard.min.js 3 | 4 | -------------------------------------------------------------------------------- /dist/hoverboard.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var o;"undefined"!=typeof window?o=window:"undefined"!=typeof global?o=global:"undefined"!=typeof self&&(o=self);var f=o;f=f.L||(f.L={}),f=f.tileLayer||(f.tileLayer={}),f.hoverboard=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o this.maxZoom) return; 302 | 303 | this.whereConditions.forEach(function(fn){ 304 | features = features.filter(fn); 305 | }); 306 | 307 | this.instructions.forEach(function(instruction){ 308 | if (instruction.type == 'fill') { 309 | if (typeof instruction.color == 'string') { 310 | //fill all at once 311 | context.fillStyle = instruction.color; 312 | draw(features); 313 | context.fill(); 314 | } else if (typeof instruction.color == 'function') { 315 | //fill individually 316 | features.forEach(function(feature){ 317 | context.fillStyle = instruction.color(feature); 318 | draw(feature); 319 | context.fill(); 320 | }); 321 | } else { 322 | throw new Error('fill color must be string or function, is type '+(typeof instruction.color)); 323 | } 324 | } else if (instruction.type == 'stroke') { 325 | if (typeof instruction.width == 'number' && typeof instruction.color == 'string') { 326 | //draw all at once 327 | context.lineWidth = instruction.width; 328 | context.strokeStyle = instruction.color; 329 | draw(features); 330 | context.stroke(); 331 | } else if (typeof instruction.width == 'function' || typeof instruction.color == 'function') { 332 | //draw individually 333 | features.forEach(function(feature){ 334 | var lineWidth = (typeof instruction.width == 'function') ? instruction.width(feature) : instruction.width; 335 | var strokeStyle = (typeof instruction.color == 'function') ? instruction.color(feature) : instruction.color; 336 | 337 | if (typeof instruction.color == 'undefined' && Array.isArray(lineWidth)) { 338 | strokeStyle = lineWidth[1]; 339 | lineWidth = lineWidth[0]; 340 | } 341 | 342 | draw(feature); 343 | context.stroke(); 344 | }); 345 | } else { 346 | throw new Error('Expected stroke(number or function, string or function) or stroke(function), got stroke('+(typeof instruction.width)+', '+(typeof instruction.color)+')'); 347 | } 348 | } 349 | }); 350 | }; 351 | 352 | module.exports = RenderingInterface; 353 | },{}]},{},[1])(1) 354 | }); -------------------------------------------------------------------------------- /dist/hoverboard.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;"undefined"!=typeof window?t=window:"undefined"!=typeof global?t=global:"undefined"!=typeof self&&(t=self);var r=t;r=r.L||(r.L={}),r=r.tileLayer||(r.tileLayer={}),r.hoverboard=e()}}(function(){return function e(t,r,o){function n(f,s){if(!r[f]){if(!t[f]){var a="function"==typeof require&&require;if(!s&&a)return a(f,!0);if(i)return i(f,!0);var u=new Error("Cannot find module '"+f+"'");throw u.code="MODULE_NOT_FOUND",u}var c=r[f]={exports:{}};t[f][0].call(c.exports,function(e){var r=t[f][1][e];return n(r?r:e)},c,c.exports,e,t,r,o)}return r[f].exports}for(var i="function"==typeof require&&require,f=0;fthis.maxZoom||(this.whereConditions.forEach(function(e){t=t.filter(e)}),this.instructions.forEach(function(r){if("fill"==r.type)if("string"==typeof r.color)e.fillStyle=r.color,o(t),e.fill();else{if("function"!=typeof r.color)throw new Error("fill color must be string or function, is type "+typeof r.color);t.forEach(function(t){e.fillStyle=r.color(t),o(t),e.fill()})}else if("stroke"==r.type)if("number"==typeof r.width&&"string"==typeof r.color)e.lineWidth=r.width,e.strokeStyle=r.color,o(t),e.stroke();else{if("function"!=typeof r.width&&"function"!=typeof r.color)throw new Error("Expected stroke(number or function, string or function) or stroke(function), got stroke("+typeof r.width+", "+typeof r.color+")");t.forEach(function(t){var n="function"==typeof r.width?r.width(t):r.width,i="function"==typeof r.color?r.color(t):r.color;"undefined"==typeof r.color&&Array.isArray(n)&&(i=n[1],n=n[0]),o(t),e.stroke()})}}))},t.exports=r},{}]},{},[1])(1)}); -------------------------------------------------------------------------------- /example.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | #map { 8 | width: 100%; 9 | height: 100%; 10 | background: #bcd9bf; 11 | } 12 | 13 | .leaflet-tile-container:first-child { 14 | /* This prevents reference labels from being drawn twice at the same time, which is bad */ 15 | display: none; 16 | } 17 | 18 | /* Hide zoom control on touch devices, which interferes with project page navigation overlay */ 19 | .leaflet-touch .leaflet-control-zoom { 20 | display: none; 21 | } 22 | 23 | /* For non touch devices, move control out of the way at small window widths */ 24 | @media (max-width: 752px) { 25 | /* 767px - 15px for scrollbar */ 26 | .mapzen-demo-iframed .leaflet-top { 27 | top: 50px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var map = L.map('map', { 2 | //tokyo 3 | center: [35.6841,139.7546], 4 | zoom: 16 5 | }); 6 | 7 | if (window.self !== window.top) { 8 | map.scrollWheelZoom.disable(); 9 | document.documentElement.className += ' mapzen-demo-iframed'; 10 | } 11 | 12 | if (map.attributionControl) { 13 | map.attributionControl.addAttribution('Hoverboard | © OpenStreetMap contributors | Mapzen'); 14 | } 15 | 16 | var url = window.xyz_tile_source_url; 17 | 18 | var colors = { 19 | base: '#f7ecdc', 20 | land: '#f7ecdc', 21 | water: '#357abf', 22 | grass: '#E6F2C1', 23 | beach: '#FFEEC7', 24 | park: '#a5af6e', 25 | cemetery: '#D6DED2', 26 | wooded: '#C3D9AD', 27 | agriculture: '#F2E8B6', 28 | building: '#b3bdc4', 29 | hospital: 'rgb(229,198,195)', 30 | school: '#FFF5CC', 31 | sports: '#B8E6B8', 32 | residential: '#f7ecdc', 33 | commercial: '#f7ecdc', 34 | industrial: '#f7ecdc', 35 | parking: '#EEE', 36 | big_road: '#673919', 37 | little_road: '#b29176', 38 | railway: '#ef7369' 39 | }; 40 | 41 | L.tileLayer.hoverboard(url, {hidpiPolyfill: true}) 42 | 43 | .render('landuse') 44 | .minZoom(12) 45 | .fillBy('kind', { 46 | allotments: colors.base, 47 | apron: colors.base, 48 | cemetery: colors.cemetery, 49 | cinema: colors.base, 50 | college: colors.school, 51 | commercial: colors.industrial, 52 | common: colors.residential, 53 | farm: colors.park, 54 | farmland: colors.park, 55 | farmyard: colors.park, 56 | footway: colors.little_road, 57 | forest: colors.park, 58 | fuel: colors.base, 59 | garden: colors.park, 60 | glacier: colors.water, 61 | golf_course: colors.sports, 62 | grass: colors.park, 63 | hospital: colors.hospital, 64 | industrial: colors.industrial, 65 | land: colors.land, 66 | library: colors.school, 67 | meadow: colors.park, 68 | nature_reserve: colors.park, 69 | park: colors.park, 70 | parking: colors.parking, 71 | pedestrian: colors.little_road, 72 | pitch: colors.base, 73 | place_of_worship: colors.base, 74 | playground: colors.sports, 75 | quarry: colors.industrial, 76 | railway: colors.railway, 77 | recreation_ground: colors.park, 78 | residential: colors.residential, 79 | retail: colors.industrial, 80 | runway: colors.base, 81 | school: colors.school, 82 | scrub: colors.park, 83 | sports_centre: colors.sports, 84 | stadium: colors.sports, 85 | taxiway: colors.little_road, 86 | theatre: colors.industrial, 87 | university: colors.school, 88 | village_green: colors.park, 89 | wetland: colors.water, 90 | conservation: colors.park, 91 | wood: colors.wooded, 92 | urban_area: colors.residential, 93 | park: colors.park, 94 | brownfield: colors.park, 95 | protected: colors.park, 96 | protected_area: colors.park 97 | }) 98 | 99 | .render('roads') 100 | .where('kind', ['minor_road', 'path']) 101 | .stroke(1, 'rgba(255, 255, 255, 0.5)') 102 | .stroke(0.5, colors.little_road) 103 | 104 | .render('buildings') 105 | .fill('#888896') 106 | .stroke(0.5, 'rgba(0,0,0,0.4)') 107 | 108 | .render('water') 109 | .where('kind', ['ocean', 'water']) 110 | .whereNot('boundary', ['yes']) 111 | .fill(colors.water) 112 | .stroke(0.5, colors.water) 113 | 114 | .render('water') 115 | .where('kind', ['river', 'stream', 'canal']) 116 | .stroke(0.5, colors.water) 117 | 118 | .render('water') 119 | .where('kind', ['riverbank']) 120 | .whereNot('boundary', ['yes']) 121 | .fill(colors.water) 122 | 123 | .render('roads') 124 | .where('kind', ['major_road', 'highway', 'rail']) 125 | .stroke(1.75, 'rgba(255, 255, 255, 0.5)') 126 | .stroke(0.75, colors.big_road) 127 | 128 | .addTo(map); 129 | 130 | var hash = L.hash(map); 131 | -------------------------------------------------------------------------------- /geojson.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Map on Canvas using geojson 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /grenoble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapzen/hoverboard/e1437d9853c3aa28f7f117b613c2af51fbf51d25/grenoble.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Map on Canvas using vector PBF 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-hoverboard", 3 | "version": "1.1.3", 4 | "description": "Render vector tiles on canvas with leaflet. Supports geojson, topojson, and mapbox vector tiles (protobuf)", 5 | "main": "src/hoverboard.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/devTristan/hoverboard.git" 12 | }, 13 | "keywords": [ 14 | "leaflet", 15 | "vector", 16 | "geojson", 17 | "topojson", 18 | "protobuf" 19 | ], 20 | "author": "Tristan Davies ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/devTristan/hoverboard/issues" 24 | }, 25 | "homepage": "https://github.com/devTristan/hoverboard", 26 | "dependencies": { 27 | "topojson": "^1.6.18" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/hoverboard.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var o;"undefined"!=typeof window?o=window:"undefined"!=typeof global?o=global:"undefined"!=typeof self&&(o=self);var f=o;f=f.L||(f.L={}),f=f.tileLayer||(f.tileLayer={}),f.hoverboard=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o this.maxZoom) return; 302 | 303 | this.whereConditions.forEach(function(fn){ 304 | features = features.filter(fn); 305 | }); 306 | 307 | this.instructions.forEach(function(instruction){ 308 | if (instruction.type == 'fill') { 309 | if (typeof instruction.color == 'string') { 310 | //fill all at once 311 | context.fillStyle = instruction.color; 312 | draw(features); 313 | context.fill(); 314 | } else if (typeof instruction.color == 'function') { 315 | //fill individually 316 | features.forEach(function(feature){ 317 | context.fillStyle = instruction.color(feature); 318 | draw(feature); 319 | context.fill(); 320 | }); 321 | } else { 322 | throw new Error('fill color must be string or function, is type '+(typeof instruction.color)); 323 | } 324 | } else if (instruction.type == 'stroke') { 325 | if (typeof instruction.width == 'number' && typeof instruction.color == 'string') { 326 | //draw all at once 327 | context.lineWidth = instruction.width; 328 | context.strokeStyle = instruction.color; 329 | draw(features); 330 | context.stroke(); 331 | } else if (typeof instruction.width == 'function' || typeof instruction.color == 'function') { 332 | //draw individually 333 | features.forEach(function(feature){ 334 | var lineWidth = (typeof instruction.width == 'function') ? instruction.width(feature) : instruction.width; 335 | var strokeStyle = (typeof instruction.color == 'function') ? instruction.color(feature) : instruction.color; 336 | 337 | if (typeof instruction.color == 'undefined' && Array.isArray(lineWidth)) { 338 | strokeStyle = lineWidth[1]; 339 | lineWidth = lineWidth[0]; 340 | } 341 | 342 | draw(feature); 343 | context.stroke(); 344 | }); 345 | } else { 346 | throw new Error('Expected stroke(number or function, string or function) or stroke(function), got stroke('+(typeof instruction.width)+', '+(typeof instruction.color)+')'); 347 | } 348 | } 349 | }); 350 | }; 351 | 352 | module.exports = RenderingInterface; 353 | },{}]},{},[1])(1) 354 | }); -------------------------------------------------------------------------------- /src/renderingInterface.js: -------------------------------------------------------------------------------- 1 | var RenderingInterface = function(layer, name){ 2 | this.layer = layer; 3 | this.layerName = name; 4 | 5 | this.instructions = []; 6 | this.whereConditions = []; 7 | 8 | var self = this; 9 | Object.keys(layer.__proto__).forEach(function(key){ 10 | self[key] = function(){ 11 | return layer[key].apply(layer, arguments); 12 | }; 13 | }); 14 | ['render', 'data', 'mode', 'addTo'].forEach(function(key){ 15 | self[key] = function(){ 16 | return layer[key].apply(layer, arguments); 17 | }; 18 | }); 19 | }; 20 | 21 | RenderingInterface.prototype.minZoom = function(minZoom){ 22 | this.minZoom = minZoom; 23 | return this; 24 | }; 25 | RenderingInterface.prototype.maxZoom = function(maxZoom){ 26 | this.maxZoom = maxZoom; 27 | return this; 28 | }; 29 | 30 | RenderingInterface.prototype.fill = function(color){ 31 | this.instructions.push({ 32 | type: 'fill', 33 | color: color 34 | }); 35 | return this; 36 | }; 37 | RenderingInterface.prototype.stroke = function(width, color){ 38 | this.instructions.push({ 39 | type: 'stroke', 40 | width: width, 41 | color: color 42 | }); 43 | return this; 44 | }; 45 | 46 | RenderingInterface.prototype.fillBy = function(property, colors, fallback){ 47 | this.fill(function(d){ 48 | return colors[d.properties[property]] || fallback; 49 | }); 50 | return this; 51 | }; 52 | RenderingInterface.prototype.strokeBy = function(property, strokes, fallback){ 53 | this.stroke(function(d){ 54 | return strokes[d.properties[property]] || fallback; 55 | }); 56 | return this; 57 | }; 58 | 59 | RenderingInterface.prototype._where = function(options){ 60 | var field = options.field; 61 | var value = options.value; 62 | 63 | if (typeof value == 'undefined') { 64 | if (typeof field == 'string') { 65 | this.where(function(d){ 66 | return d.properties[field] ? true : false; 67 | }, undefined, options.invert); 68 | } else if (typeof field == 'object') { 69 | for (var key in field) { 70 | this.where(key, field[key], options.invert); 71 | } 72 | } else if (typeof field == 'function') { 73 | if (options.invert) { 74 | var oldField = field; 75 | field = function(){ 76 | return !oldField.apply(null, arguments); 77 | }; 78 | } 79 | this.whereConditions.push(field); 80 | } else { 81 | throw new Error('with RenderingInterface.where(field, value) if value is undefined then field must be a string, object, or function!'); 82 | } 83 | } else if (typeof value == 'string' || typeof value == 'number'){ 84 | this.where(function(d){ 85 | return d.properties[field] == value; 86 | }, undefined, options.invert); 87 | } else if (typeof value == 'object' && Array.isArray(value)) { 88 | this.where(function(d){ 89 | return value.indexOf(d.properties[field]) != -1; 90 | }, undefined, options.invert); 91 | } else { 92 | throw new Error('RenderingInterface.where(field, value) cannot be called with field as type '+(typeof field)+' and value as type '+(typeof value)); 93 | } 94 | return this; 95 | }; 96 | 97 | RenderingInterface.prototype.where = function(field, value, invert){ 98 | return this._where({field: field, value: value, invert: invert}); 99 | } 100 | RenderingInterface.prototype.whereNot = function(field, value){ 101 | return this._where({field: field, value: value, invert: true}); 102 | } 103 | 104 | RenderingInterface.prototype.run = function(context, features, tile, draw){ 105 | if (typeof this.minZoom == 'number' && tile.z < this.minZoom) return; 106 | if (typeof this.maxZoom == 'number' && tile.z > this.maxZoom) return; 107 | 108 | this.whereConditions.forEach(function(fn){ 109 | features = features.filter(fn); 110 | }); 111 | 112 | this.instructions.forEach(function(instruction){ 113 | if (instruction.type == 'fill') { 114 | if (typeof instruction.color == 'string') { 115 | //fill all at once 116 | context.fillStyle = instruction.color; 117 | draw(features); 118 | context.fill(); 119 | } else if (typeof instruction.color == 'function') { 120 | //fill individually 121 | features.forEach(function(feature){ 122 | context.fillStyle = instruction.color(feature); 123 | draw(feature); 124 | context.fill(); 125 | }); 126 | } else { 127 | throw new Error('fill color must be string or function, is type '+(typeof instruction.color)); 128 | } 129 | } else if (instruction.type == 'stroke') { 130 | if (typeof instruction.width == 'number' && typeof instruction.color == 'string') { 131 | //draw all at once 132 | context.lineWidth = instruction.width; 133 | context.strokeStyle = instruction.color; 134 | draw(features); 135 | context.stroke(); 136 | } else if (typeof instruction.width == 'function' || typeof instruction.color == 'function') { 137 | //draw individually 138 | features.forEach(function(feature){ 139 | var lineWidth = (typeof instruction.width == 'function') ? instruction.width(feature) : instruction.width; 140 | var strokeStyle = (typeof instruction.color == 'function') ? instruction.color(feature) : instruction.color; 141 | 142 | if (typeof instruction.color == 'undefined' && Array.isArray(lineWidth)) { 143 | strokeStyle = lineWidth[1]; 144 | lineWidth = lineWidth[0]; 145 | } 146 | 147 | draw(feature); 148 | context.stroke(); 149 | }); 150 | } else { 151 | throw new Error('Expected stroke(number or function, string or function) or stroke(function), got stroke('+(typeof instruction.width)+', '+(typeof instruction.color)+')'); 152 | } 153 | } 154 | }); 155 | }; 156 | 157 | module.exports = RenderingInterface; -------------------------------------------------------------------------------- /topojson.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Map on Canvas using topojson 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /watch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -z $(which fswatch) ] 4 | then 5 | ./build.sh 6 | fswatch -o -1 ./src | xargs -n1 ./build.sh 7 | else 8 | while true; do 9 | ./build.sh 10 | inotifywait src/ 11 | done 12 | fi 13 | --------------------------------------------------------------------------------