├── .gitignore ├── .travis.yml ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── dist ├── leaflet-realtime.js └── leaflet-realtime.min.js ├── examples ├── earthquakes.html ├── earthquakes.js ├── index.html ├── index.js ├── trail.html └── trail.js ├── package-lock.json ├── package.json ├── src └── L.Realtime.js └── test └── L.Realtime.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | addons: 5 | apt: 6 | packages: 7 | - xvfb 8 | install: 9 | - export DISPLAY=':99.0' 10 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 11 | - npm install 12 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | pkg: grunt.file.readJSON('package.json'), 4 | browserify: { 5 | control: { 6 | src: ['src/L.Realtime.js'], 7 | dest: 'dist/leaflet-realtime.js', 8 | options: { 9 | browserifyOptions: { 10 | standalone: 'L.Realtime' 11 | } 12 | } 13 | } 14 | }, 15 | uglify: { 16 | options: { 17 | banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' + 18 | '<%= grunt.template.today("yyyy-mm-dd") %> */\n\n' 19 | }, 20 | build: { 21 | src: 'dist/leaflet-realtime.js', 22 | dest: 'dist/leaflet-realtime.min.js' 23 | } 24 | } 25 | }); 26 | 27 | grunt.loadNpmTasks('grunt-browserify'); 28 | grunt.loadNpmTasks('grunt-contrib-uglify'); 29 | grunt.registerTask('default', ['browserify', 'uglify']); 30 | }; 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## ISC License 2 | 3 | Copyright (c) 2014, Per Liedman (per@liedman.net) 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Leaflet Realtime 2 | 3 | [![Build status](https://travis-ci.org/perliedman/leaflet-realtime.svg)](https://travis-ci.org/perliedman/leaflet-realtime) 4 | [![NPM version](https://img.shields.io/npm/v/leaflet-realtime.svg)](https://www.npmjs.com/package/leaflet-realtime) ![Leaflet 1.0 compatible!](https://img.shields.io/badge/Leaflet%201.0-%E2%9C%93-1EB300.svg?style=flat) 5 | [![CDNJS](https://img.shields.io/cdnjs/v/leaflet-realtime.svg)](https://cdnjs.com/libraries/leaflet-realtime) [![Greenkeeper badge](https://badges.greenkeeper.io/perliedman/leaflet-realtime.svg)](https://greenkeeper.io/) 6 | 7 | Put realtime data on a Leaflet map: live tracking GPS units, sensor data or just about anything. 8 | 9 | _Note:_ version 2 and up of this plugin is _only compatible with Leaflet 1.0 and later. Use earlier versions of Leaflet Realtime if you need Leaflet 0.7 compatibility. 10 | 11 | ## Example 12 | 13 | Checkout the [Leaflet Realtime Demo](http://www.liedman.net/leaflet-realtime). Basic example: 14 | 15 | ```javascript 16 | var map = L.map('map'), 17 | realtime = L.realtime({ 18 | url: 'https://wanderdrone.appspot.com/', 19 | crossOrigin: true, 20 | type: 'json' 21 | }, { 22 | interval: 3 * 1000 23 | }).addTo(map); 24 | 25 | realtime.on('update', function() { 26 | map.fitBounds(realtime.getBounds(), {maxZoom: 3}); 27 | }); 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Overview 33 | 34 | By default, Leaflet Realtime reads and displays GeoJSON from a provided source. A "source" is usually a URL, and data can be fetched using AJAX (XHR), JSONP. This means Leaflet Realtime will _poll_ for data, pulling it from the source. Alternatively, you can write your own source, to provide data in just about any way you want. Leaflet Realtime can also be made work with _push_ data, for example data pushed from the server using [socket.io](http://socket.io/) or similar. 35 | 36 | To be able to figure out when new features are added, when old features are removed, and which features are just updated, Leaflet Realtime needs to identify each feature uniquely. This is done using a _feature id_. Usually, this can be done using one of the feature's `properties`. By default, Leaflet Realtime will try to look for a called property `id` and use that. 37 | 38 | By default, `L.Realtime` uses a `L.GeoJSON` layer to display the results. You can basically do anything you can do with `L.GeoJSON` with `L.Realtime` - styling, `onEachFeature`, gettings bounds, etc. as if you were working directly with a normal GeoJSON layer. 39 | 40 | `L.Realtime` can also use other layer types to display the results, for example it can use a `MarkerClusterGroup` from [Leaflet MarkerCluster](https://github.com/Leaflet/Leaflet.markercluster): pass a `LayerGroup` (or any class that implements `addLayer` and `removeLayer`) to `L.Realtime`'s `container` option. (This feature was added in version 2.1.0.) 41 | 42 | Typical usage involves instantiating `L.Realtime` with options for [`style`](http://leafletjs.com/reference.html#geojson-style) and/or [`onEachFeature`](http://leafletjs.com/reference.html#geojson-oneachfeature), to customize styling and interaction, as well as adding a listener for the [`update`](#event-update) event, to for example list the features currently visible in the map. 43 | 44 | Since version 2.0, Leaflet Realtime uses the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to request data (AJAX). If you are in the unfortunate situation that you need to support a browser without Fetch, you either need to use a polyfill, or write your own [source function](#source) to make the AJAX requests. 45 | 46 | ### Push data 47 | 48 | If you prefer getting data _pushed_ from the server, in contrast to Leaflet Realtime pulling it with a standard HTTP request, you can feed added and updated GeoJSON data to Leaflet Realtime using the `update` method. In this scenario, you will also need to remove features by explicit calls to `remove`. 49 | 50 | Since automatic updates do not make sense in a push scenario, you want to create the layer with the option `start` set to `false`. 51 | 52 | ### API 53 | 54 | #### L.Realtime 55 | 56 | This is a realtime updated layer that can be added to the map. It extends [L.GeoJSON](http://leafletjs.com/reference.html#geojson). 57 | 58 | ##### Creation 59 | 60 | Factory | Description 61 | -----------------------|------------------------------------------------------- 62 | `L.Realtime(<`[`Source`](#source)`> source, <`[`RealtimeOptions`](#realtimeoptions)`> options?)` | Instantiates a new realtime layer with the provided source and options 63 | 64 | ##### Options 65 | 66 | Provides these options, in addition to the options of [`L.GeoJSON`](http://leafletjs.com/reference.html#geojson). 67 | 68 | Option | Type | Default | Description 69 | -----------------------|---------------------|----------------------|--------------------------------------------------------- 70 | `start` | `Boolean` | `true` | Should automatic updates be enabled when layer is added on the map and stopped when layer is removed from the map 71 | `interval` | `Number` | 60000 | Automatic update interval, in milliseconds 72 | `getFeatureId( featureData)` | `Function` | Returns `featureData.properties.id` | Function used to get an identifier uniquely identify a feature over time 73 | `updateFeature( featureData, oldLayer)` | `Function` | Special | Used to update an existing feature's layer; by default, points (markers) are updated, other layers are discarded and replaced with a new, updated layer. Allows to create more complex transitions, for example, when a feature is updated | 74 | `container` | `LayerGroup` | L.geoJson() | Specifies the layer instance to display the results in 75 | `removeMissing` | `Boolean` | `false` | Should missing features between updates been automatically removed from the layer 76 | 77 | ##### Events 78 | 79 | Event | Data | Description 80 | --------------|----------------|--------------------------------------------------------------- 81 | `update` | [`UpdateEvent`](#updateevent) | Fires when the layer's data is updated 82 | 83 | ##### Methods 84 | 85 | Method | Returns | Description 86 | -----------------------|----------------|----------------------------------------------------------------- 87 | `start()` | `this` | Starts automatic updates 88 | `stop()` | `this` | Stops automatic updates 89 | `isRunning()` | `Boolean` | Tells if automatic updates are running 90 | `update( featureData?)` | `this` | Updates the layer's data. If `featureData` is provided, it is used to add or update data in the layer, otherwise the layer's source is queried for new data asynchronously 91 | `remove( featureData)` | `this` | Removes the provided feature or features from the layer 92 | `getLayer( featureId)` | `ILayer` | Retrieves the layer used for a certain feature 93 | `getFeature( featureId)` | `GeoJSON` | Retrieves the feature data for the given `featureId` 94 | 95 | #### Source 96 | 97 | The source can be one of: 98 | 99 | * a string with the URL to get data from 100 | * an options object that is passed to [fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) for fetching the data 101 | * a function in case you need more freedom. 102 | 103 | In case you use a function, the function should take two callbacks as arguments: `fn(success, error)`, with the callbacks: 104 | 105 | * a success callback that takes GeoJSON as argument: `success( features)` 106 | * an error callback that should take an error object and an error message (string) as argument: `error( error, message)` 107 | 108 | #### UpdateEvent 109 | 110 | An update event is fired when the layer's data is updated from its source. The data included loosely resembles D3's [join semantics](http://bost.ocks.org/mike/join/), to make it easy to handle new features (the _enter_ set), updated features (the _update_ set) and removed features (the _exit_ set). 111 | 112 | property | type | description 113 | --------------|------------|----------------------------------- 114 | `features` | Object | Complete hash of current features, with feature id as key 115 | `enter` | Object | Features added by this update, with feature id as key 116 | `update` | Object | Existing features updated by this update, with feature id as key 117 | `exit` | Object | Existing features that were removed by this update, with feature id as key 118 | -------------------------------------------------------------------------------- /dist/leaflet-realtime.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}(g.L || (g.L = {})).Realtime = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 2 | 3 | Leaflet Realtime - Earthquakes 4 | 5 | 6 | 7 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/earthquakes.js: -------------------------------------------------------------------------------- 1 | function createRealtimeLayer(url, container) { 2 | return L.realtime(url, { 3 | interval: 60 * 1000, 4 | getFeatureId: function(f) { 5 | return f.properties.url; 6 | }, 7 | cache: true, 8 | container: container, 9 | onEachFeature(f, l) { 10 | l.bindPopup(function() { 11 | return '

' + f.properties.place + '

' + 12 | '

' + new Date(f.properties.time) + 13 | '
Magnitude: ' + f.properties.mag + '

' + 14 | '

More information

'; 15 | }); 16 | } 17 | }); 18 | } 19 | 20 | var map = L.map('map'), 21 | clusterGroup = L.markerClusterGroup().addTo(map), 22 | subgroup1 = L.featureGroup.subGroup(clusterGroup), 23 | subgroup2 = L.featureGroup.subGroup(clusterGroup), 24 | realtime1 = createRealtimeLayer('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson', subgroup1).addTo(map), 25 | realtime2 = createRealtimeLayer('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson', subgroup2); 26 | 27 | L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { 28 | attribution: '© USGS Earthquake Hazards Program, © OpenStreetMap contributors' 29 | }).addTo(map); 30 | 31 | L.control.layers(null, { 32 | 'Earthquakes 2.5+': realtime1, 33 | 'All Earthquakes': realtime2 34 | }).addTo(map); 35 | 36 | realtime1.once('update', function() { 37 | map.fitBounds(realtime1.getBounds(), {maxZoom: 3}); 38 | }); 39 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Leaflet Realtime 4 | 5 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | var map = L.map('map'), 2 | realtime = L.realtime('https://wanderdrone.appspot.com/', { 3 | interval: 3 * 1000 4 | }).addTo(map); 5 | 6 | L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { 7 | attribution: '© OpenStreetMap contributors' 8 | }).addTo(map); 9 | 10 | realtime.on('update', function() { 11 | map.fitBounds(realtime.getBounds(), {maxZoom: 3}); 12 | }); 13 | -------------------------------------------------------------------------------- /examples/trail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Leaflet Realtime 4 | 5 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/trail.js: -------------------------------------------------------------------------------- 1 | var map = L.map('map'), 2 | trail = { 3 | type: 'Feature', 4 | properties: { 5 | id: 1 6 | }, 7 | geometry: { 8 | type: 'LineString', 9 | coordinates: [] 10 | } 11 | }, 12 | realtime = L.realtime(function(success, error) { 13 | fetch('https://wanderdrone.appspot.com/') 14 | .then(function(response) { return response.json(); }) 15 | .then(function(data) { 16 | var trailCoords = trail.geometry.coordinates; 17 | trailCoords.push(data.geometry.coordinates); 18 | trailCoords.splice(0, Math.max(0, trailCoords.length - 5)); 19 | success({ 20 | type: 'FeatureCollection', 21 | features: [data, trail] 22 | }); 23 | }) 24 | .catch(error); 25 | }, { 26 | interval: 250 27 | }).addTo(map); 28 | 29 | L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { 30 | attribution: '© OpenStreetMap contributors' 31 | }).addTo(map); 32 | 33 | realtime.on('update', function() { 34 | map.fitBounds(realtime.getBounds(), {maxZoom: 3}); 35 | }); 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-realtime", 3 | "version": "2.2.0", 4 | "description": "Show realtime updated GeoJSON in Leaflet", 5 | "main": "src/L.Realtime.js", 6 | "directories": { 7 | "example": "examples", 8 | "dist": "dist" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/perliedman/leaflet-realtime.git" 13 | }, 14 | "scripts": { 15 | "prepublish": "grunt", 16 | "test": "browserify test/L.Realtime | tape-run -b electron | faucet" 17 | }, 18 | "keywords": [ 19 | "leaflet", 20 | "realtime" 21 | ], 22 | "author": { 23 | "name": "Per Liedman", 24 | "email": "per@liedman.net" 25 | }, 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/perliedman/leaflet-realtime/issues" 29 | }, 30 | "homepage": "https://github.com/perliedman/leaflet-realtime", 31 | "devDependencies": { 32 | "browserify": "^16.5.0", 33 | "faucet": "0.0.1", 34 | "grunt": "^1.0.4", 35 | "grunt-browserify": "^5.3.0", 36 | "grunt-cli": "^1.3.2", 37 | "grunt-contrib-uglify": "^4.0.1", 38 | "leaflet": "^1.5.1", 39 | "tape": "^4.10.1", 40 | "tape-run": "^7.0.0" 41 | }, 42 | "dependencies": {} 43 | } 44 | -------------------------------------------------------------------------------- /src/L.Realtime.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | L.Realtime = L.Layer.extend({ 4 | options: { 5 | start: true, 6 | interval: 60 * 1000, 7 | getFeatureId: function(f) { 8 | return f.properties.id; 9 | }, 10 | updateFeature: function(feature, oldLayer) { 11 | if (!oldLayer) { return; } 12 | 13 | var type = feature.geometry && feature.geometry.type 14 | var coordinates = feature.geometry && feature.geometry.coordinates 15 | switch (type) { 16 | case 'Point': 17 | oldLayer.setLatLng(L.GeoJSON.coordsToLatLng(coordinates)); 18 | break; 19 | case 'LineString': 20 | case 'MultiLineString': 21 | oldLayer.setLatLngs(L.GeoJSON.coordsToLatLngs(coordinates, type === 'LineString' ? 0 : 1)); 22 | break; 23 | case 'Polygon': 24 | case 'MultiPolygon': 25 | oldLayer.setLatLngs(L.GeoJSON.coordsToLatLngs(coordinates, type === 'Polygon' ? 1 : 2)); 26 | break; 27 | default: 28 | return null; 29 | } 30 | return oldLayer; 31 | }, 32 | logErrors: true, 33 | cache: false, 34 | removeMissing: true, 35 | onlyRunWhenAdded: false 36 | }, 37 | 38 | initialize: function(src, options) { 39 | L.setOptions(this, options); 40 | this._container = options.container || L.geoJson(null, options); 41 | 42 | if (typeof(src) === 'function') { 43 | this._src = src; 44 | } else { 45 | this._fetchOptions = src && src.url ? src : {url: src}; 46 | this._src = L.bind(this._defaultSource, this); 47 | } 48 | 49 | this._features = {}; 50 | this._featureLayers = {}; 51 | this._requestCount = 0; 52 | 53 | if (this.options.start && !this.options.onlyRunWhenAdded) { 54 | this.start(); 55 | } 56 | }, 57 | 58 | start: function() { 59 | if (!this._timer) { 60 | this._timer = setInterval(L.bind(this.update, this), 61 | this.options.interval); 62 | this.update(); 63 | } 64 | 65 | return this; 66 | }, 67 | 68 | stop: function() { 69 | if (this._timer) { 70 | clearTimeout(this._timer); 71 | delete this._timer; 72 | } 73 | 74 | return this; 75 | }, 76 | 77 | isRunning: function() { 78 | return this._timer; 79 | }, 80 | 81 | setUrl: function (url) { 82 | if (this._fetchOptions) { 83 | this._fetchOptions.url = url; 84 | this.update(); 85 | } else { 86 | throw new Error('Custom sources does not support setting URL.'); 87 | } 88 | }, 89 | 90 | update: function(geojson) { 91 | var requestCount = ++this._requestCount, 92 | checkRequestCount = L.bind(function(cb) { 93 | return L.bind(function() { 94 | if (requestCount === this._requestCount) { 95 | return cb.apply(this, arguments); 96 | } 97 | }, this); 98 | }, this), 99 | responseHandler, 100 | errorHandler; 101 | 102 | if (geojson) { 103 | this._onNewData(false, geojson); 104 | } else { 105 | responseHandler = L.bind(function(data) { this._onNewData(this.options.removeMissing, data); }, this); 106 | errorHandler = L.bind(this._onError, this); 107 | 108 | this._src(checkRequestCount(responseHandler), checkRequestCount(errorHandler)); 109 | } 110 | 111 | return this; 112 | }, 113 | 114 | remove: function(geojson) { 115 | if (typeof geojson === 'undefined') { 116 | return L.Layer.prototype.remove.call(this); 117 | } 118 | 119 | var features = L.Util.isArray(geojson) ? geojson : geojson.features ? geojson.features : [geojson], 120 | exit = {}, 121 | i, 122 | len, 123 | fId; 124 | 125 | for (i = 0, len = features.length; i < len; i++) { 126 | fId = this.options.getFeatureId(features[i]); 127 | this._container.removeLayer(this._featureLayers[fId]); 128 | exit[fId] = this._features[fId]; 129 | delete this._features[fId]; 130 | delete this._featureLayers[fId]; 131 | } 132 | 133 | this.fire('update', { 134 | features: this._features, 135 | enter: {}, 136 | update: {}, 137 | exit: exit 138 | }); 139 | 140 | return this; 141 | }, 142 | 143 | getLayer: function(featureId) { 144 | return this._featureLayers[featureId]; 145 | }, 146 | 147 | getFeature: function(featureId) { 148 | return this._features[featureId]; 149 | }, 150 | 151 | getBounds: function() { 152 | var container = this._container; 153 | if (container.getBounds) { 154 | return container.getBounds(); 155 | } 156 | 157 | throw new Error('Container has no getBounds method'); 158 | }, 159 | 160 | onAdd: function(map) { 161 | map.addLayer(this._container); 162 | if (this.options.start) { 163 | this.start(); 164 | } 165 | }, 166 | 167 | onRemove: function(map) { 168 | if (this.options.onlyRunWhenAdded) { 169 | this.stop(); 170 | } 171 | 172 | map.removeLayer(this._container); 173 | }, 174 | 175 | _onNewData: function(removeMissing, geojson) { 176 | var layersToRemove = [], 177 | enter = {}, 178 | update = {}, 179 | exit = {}, 180 | seenFeatures = {}, 181 | i, len, feature; 182 | 183 | var handleData = L.bind(function(geojson) { 184 | var features = L.Util.isArray(geojson) ? geojson : geojson.features; 185 | if (features) { 186 | for (i = 0, len = features.length; i < len; i++) { 187 | // only add this if geometry or geometries are set and not null 188 | feature = features[i]; 189 | if (feature.geometries || feature.geometry || feature.features || feature.coordinates) { 190 | handleData(feature); 191 | } 192 | } 193 | return; 194 | } 195 | 196 | var container = this._container; 197 | var options = this.options; 198 | 199 | if (options.filter && !options.filter(geojson)) { return; } 200 | 201 | var f = L.GeoJSON.asFeature(geojson); 202 | var fId = options.getFeatureId(f); 203 | var oldLayer = this._featureLayers[fId]; 204 | 205 | var layer = this.options.updateFeature(f, oldLayer); 206 | if (!layer) { 207 | layer = L.GeoJSON.geometryToLayer(geojson, options); 208 | if (!layer) { 209 | return; 210 | } 211 | layer.defaultOptions = layer.options; 212 | layer.feature = f; 213 | 214 | if (options.onEachFeature) { 215 | options.onEachFeature(geojson, layer); 216 | } 217 | 218 | if (options.style && layer.setStyle) { 219 | layer.setStyle(options.style(geojson)); 220 | } 221 | 222 | } 223 | 224 | layer.feature = f; 225 | if (container.resetStyle) { 226 | container.resetStyle(layer); 227 | } 228 | 229 | if (oldLayer) { 230 | update[fId] = geojson; 231 | if (oldLayer != layer) { 232 | layersToRemove.push(oldLayer); 233 | container.addLayer(layer); 234 | } 235 | } else { 236 | enter[fId] = geojson; 237 | container.addLayer(layer); 238 | } 239 | 240 | this._featureLayers[fId] = layer; 241 | this._features[fId] = seenFeatures[fId] = f; 242 | }, this); 243 | 244 | handleData(geojson); 245 | 246 | if (removeMissing) { 247 | exit = this._removeUnknown(seenFeatures); 248 | } 249 | for (i = 0; i < layersToRemove.length; i++) { 250 | this._container.removeLayer(layersToRemove[i]); 251 | } 252 | 253 | this.fire('update', { 254 | features: this._features, 255 | enter: enter, 256 | update: update, 257 | exit: exit 258 | }); 259 | }, 260 | 261 | _onError: function(err, msg) { 262 | if (this.options.logErrors) { 263 | console.warn(err, msg); 264 | } 265 | 266 | this.fire('error', { 267 | error: err, 268 | message: msg 269 | }); 270 | }, 271 | 272 | _removeUnknown: function(known) { 273 | var fId, 274 | removed = {}; 275 | for (fId in this._featureLayers) { 276 | if (!known[fId]) { 277 | this._container.removeLayer(this._featureLayers[fId]); 278 | removed[fId] = this._features[fId]; 279 | delete this._featureLayers[fId]; 280 | delete this._features[fId]; 281 | } 282 | } 283 | 284 | return removed; 285 | }, 286 | 287 | _bustCache: function(url) { 288 | return url + L.Util.getParamString({'_': new Date().getTime()}, url); 289 | }, 290 | 291 | _defaultSource: function(responseHandler, errorHandler) { 292 | var fetchOptions = this._fetchOptions, 293 | url = fetchOptions.url; 294 | 295 | url = this.options.cache ? url : this._bustCache(url); 296 | 297 | fetch(url, fetchOptions) 298 | .then(function(response) { 299 | return response.json(); 300 | }) 301 | .then(responseHandler) 302 | .catch(errorHandler); 303 | } 304 | }); 305 | 306 | L.realtime = function(src, options) { 307 | return new L.Realtime(src, options); 308 | }; 309 | 310 | module.exports = L.Realtime; 311 | -------------------------------------------------------------------------------- /test/L.Realtime.js: -------------------------------------------------------------------------------- 1 | var L = require('leaflet'), 2 | test = require('tape'), 3 | pointFeature = { 4 | type: 'Feature', 5 | geometry: { 6 | type: 'Point', 7 | coordinates: [11.94, 57.73] 8 | }, 9 | properties: { 10 | id: 1 11 | } 12 | }, 13 | pointFeature2 = { 14 | type: 'Feature', 15 | geometry: { 16 | type: 'Point', 17 | coordinates: [11.9, 57.75] 18 | }, 19 | properties: { 20 | id: 2 21 | } 22 | }, 23 | pointFeature2_2 = { 24 | type: 'Feature', 25 | geometry: { 26 | type: 'Point', 27 | coordinates: [11.8, 57.75] 28 | }, 29 | properties: { 30 | id: 2 31 | } 32 | }; 33 | 34 | function setupRealtime(t, source, options, updateHandler) { 35 | var realtime = L.realtime(source, L.extend({ 36 | start: false 37 | }, options || {})), 38 | done = false; 39 | 40 | realtime.on('update', function(e) { 41 | updateHandler(e); 42 | done = true; 43 | }); 44 | 45 | setTimeout(function() { 46 | if (!done) { 47 | t.fail('Update event didn\'t fire, or wasn\'t handled.'); 48 | t.end(); 49 | } 50 | }, 100); 51 | 52 | return realtime; 53 | } 54 | 55 | require('../src/L.Realtime'); 56 | 57 | test('update event is fired', function(t) { 58 | var realtime = setupRealtime(t, function(success) { 59 | success([]); 60 | }, {}, function() { 61 | t.end(); 62 | }); 63 | 64 | realtime.update(); 65 | }); 66 | 67 | test('enter set is valid', function(t) { 68 | var realtime = setupRealtime(t, function(success) { 69 | success(pointFeature); 70 | }, {}, function(e) { 71 | var f = e.enter[pointFeature.properties.id]; 72 | t.ok(f); 73 | t.deepEqual(f, pointFeature); 74 | t.end(); 75 | }); 76 | 77 | realtime.update(); 78 | }); 79 | 80 | test('enter set does not contain updates', function(t) { 81 | var updateCount = 0, 82 | realtime = setupRealtime(t, function(success) { 83 | success(pointFeature); 84 | }, {}, function(e) { 85 | var f = e.enter[pointFeature.properties.id]; 86 | updateCount++; 87 | 88 | if (updateCount > 1) { 89 | t.notOk(f); 90 | t.end(); 91 | } 92 | }); 93 | 94 | realtime.update(); 95 | realtime.update(); 96 | }); 97 | 98 | test('update set contains updates', function(t) { 99 | var updateCount = 0, 100 | realtime = setupRealtime(t, function(success) { 101 | success(pointFeature); 102 | }, {}, function(e) { 103 | var f = e.update[pointFeature.properties.id]; 104 | updateCount++; 105 | 106 | if (updateCount === 1) { 107 | t.notOk(f); 108 | } else if (updateCount > 1) { 109 | t.ok(f); 110 | } 111 | }); 112 | 113 | t.plan(3); 114 | realtime.update(); 115 | realtime.update(); 116 | realtime.update(); 117 | }); 118 | 119 | test('exit set contains removed features', function(t) { 120 | var updateCount = 0, 121 | realtime = setupRealtime(t, function(success) { 122 | success(updateCount === 0 ? pointFeature : []); 123 | }, {}, function(e) { 124 | var f = e.exit[pointFeature.properties.id]; 125 | updateCount++; 126 | 127 | if (updateCount === 2) { 128 | t.ok(f); 129 | } else { 130 | t.notOk(f); 131 | } 132 | }); 133 | 134 | t.plan(3); 135 | realtime.update(); 136 | realtime.update(); 137 | realtime.update(); 138 | }); 139 | 140 | test('features set contains current features', function(t) { 141 | var updateCount = 0, 142 | realtime = setupRealtime(t, function(success) { 143 | success(updateCount < 2 ? pointFeature : []); 144 | }, {}, function(e) { 145 | var f = e.features[pointFeature.properties.id]; 146 | updateCount++; 147 | 148 | if (updateCount < 3) { 149 | t.ok(f); 150 | t.equal(Object.keys(e.features).length, 1); 151 | } else { 152 | t.notOk(f); 153 | t.equal(Object.keys(e.features).length, 0); 154 | } 155 | }); 156 | 157 | t.plan(6); 158 | realtime.update(); 159 | realtime.update(); 160 | realtime.update(); 161 | }); 162 | 163 | test('update with explicit data adds data', function(t) { 164 | var updateCount = 0, 165 | realtime = setupRealtime(t, undefined, {}, function(e) { 166 | updateCount++; 167 | 168 | switch (updateCount) { 169 | case 1: 170 | t.equal(e.enter[pointFeature.properties.id], pointFeature); 171 | t.equal(Object.keys(e.update).length, 0); 172 | t.equal(Object.keys(e.exit).length, 0); 173 | break; 174 | case 2: 175 | t.equal(e.enter[pointFeature2.properties.id], pointFeature2); 176 | t.equal(e.update[pointFeature.properties.id], pointFeature); 177 | t.equal(Object.keys(e.exit).length, 0); 178 | break; 179 | case 3: 180 | t.equal(Object.keys(e.enter).length, 0); 181 | t.equal(Object.keys(e.update).length, 0); 182 | t.equal(Object.keys(e.exit).length, 0); 183 | break; 184 | } 185 | }); 186 | 187 | t.plan(9); 188 | realtime.update(pointFeature); 189 | realtime.update([pointFeature, pointFeature2]); 190 | realtime.update([]); 191 | }); 192 | 193 | test('remove removes data', function(t) { 194 | var updateCount = 0, 195 | realtime = setupRealtime(t, undefined, {}, function(e) { 196 | updateCount++; 197 | 198 | if (updateCount === 2) { 199 | t.equal(Object.keys(e.features).length, 0, 'Feature set empty after removal'); 200 | t.equal(e.exit[pointFeature.properties.id], pointFeature, 'Removed feature is in exit set'); 201 | } 202 | }); 203 | 204 | t.plan(2); 205 | realtime.update(pointFeature); 206 | realtime.remove(pointFeature); 207 | }); 208 | 209 | test('point layer is preserved through updates', function(t) { 210 | var updateCount = 0, 211 | layer, 212 | realtime = setupRealtime(t, function(success) { 213 | success(pointFeature); 214 | }, {}, function(e) { 215 | updateCount++; 216 | 217 | if (updateCount === 1) { 218 | layer = realtime.getLayer(1); 219 | } else if (updateCount > 1) { 220 | t.equal(realtime.getLayer(1), layer); 221 | } 222 | }); 223 | 224 | t.plan(1); 225 | realtime.update(); 226 | realtime.update(); 227 | }); 228 | 229 | test('errors from old requests are ignored', function(t) { 230 | var updateCount = 0, 231 | realtime = setupRealtime(t, function(success, error) { 232 | if (updateCount === 0) { 233 | var f = function() { 234 | if (updateCount === 2) { 235 | error(); 236 | setTimeout(function() { 237 | t.end(); 238 | }, 100); 239 | return; 240 | } 241 | setTimeout(f, 10); 242 | }; 243 | f(); 244 | } else if (updateCount === 1) { 245 | success(pointFeature2_2); 246 | } 247 | updateCount++; 248 | }, {}, function(e) { 249 | }); 250 | 251 | realtime.on('error', function() { 252 | t.fail(); 253 | }); 254 | 255 | realtime.update(); 256 | realtime.update(); 257 | }); 258 | --------------------------------------------------------------------------------