├── .gitignore ├── .npmignore ├── CITATION.cff ├── LICENSE.md ├── README.md ├── demo ├── demo.css ├── demo.html ├── demo.js ├── water-gbr.json ├── wind-gbr.json └── wind-global.json ├── dist ├── leaflet-velocity.css ├── leaflet-velocity.js ├── leaflet-velocity.min.css └── leaflet-velocity.min.js ├── gulpfile.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── screenshots └── velocity.gif └── src ├── css └── leaflet-velocity.css └── js ├── L.CanvasLayer.js ├── L.Control.Velocity.js ├── L.VelocityLayer.js └── windy.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | screenshots/ 2 | demo/ 3 | .idea -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | title: Leaflet Velocity 3 | version: 2.1.2 4 | message: 'If you use this software, please cite it as below.' 5 | type: software 6 | url: "https://github.com/onaci/leaflet-velocity" 7 | authors: 8 | - given-names: Daniel Wild 9 | family-names: Wild 10 | email: mail@danwild.io 11 | affiliation: >- 12 | Commonwealth Scientific and Industrial Research 13 | Organisation (CSIRO) 14 | orcid: 'https://orcid.org/0000-0002-5127-2327' 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | CSIRO Open Source Software Licence Agreement (variation of the BSD / MIT License) 2 | 3 | Copyright (c) 2021, Commonwealth Scientific and Industrial Research Organisation (CSIRO) ABN 41 687 119 230. 4 | 5 | All rights reserved. CSIRO is willing to grant you a licence to this leaflet-velocity on the following terms, except where otherwise indicated for third party material. 6 | Redistribution and use of this software in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | - Neither the name of CSIRO nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission of CSIRO. 11 | EXCEPT AS EXPRESSLY STATED IN THIS AGREEMENT AND TO THE FULL EXTENT PERMITTED BY APPLICABLE LAW, THE SOFTWARE IS PROVIDED "AS-IS". CSIRO MAKES NO REPRESENTATIONS, WARRANTIES OR CONDITIONS OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY REPRESENTATIONS, WARRANTIES OR CONDITIONS REGARDING THE CONTENTS OR ACCURACY OF THE SOFTWARE, OR OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, THE ABSENCE OF LATENT OR OTHER DEFECTS, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. 12 | TO THE FULL EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL CSIRO BE LIABLE ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, IN AN ACTION FOR BREACH OF CONTRACT, NEGLIGENCE OR OTHERWISE) FOR ANY CLAIM, LOSS, DAMAGES OR OTHER LIABILITY HOWSOEVER INCURRED. WITHOUT LIMITING THE SCOPE OF THE PREVIOUS SENTENCE THE EXCLUSION OF LIABILITY SHALL INCLUDE: LOSS OF PRODUCTION OR OPERATION TIME, LOSS, DAMAGE OR CORRUPTION OF DATA OR RECORDS; OR LOSS OF ANTICIPATED SAVINGS, OPPORTUNITY, REVENUE, PROFIT OR GOODWILL, OR OTHER ECONOMIC LOSS; OR ANY SPECIAL, INCIDENTAL, INDIRECT, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES, ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT, ACCESS OF THE SOFTWARE OR ANY OTHER DEALINGS WITH THE SOFTWARE, EVEN IF CSIRO HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH CLAIM, LOSS, DAMAGES OR OTHER LIABILITY. 13 | APPLICABLE LEGISLATION SUCH AS THE AUSTRALIAN CONSUMER LAW MAY APPLY REPRESENTATIONS, WARRANTIES, OR CONDITIONS, OR IMPOSES OBLIGATIONS OR LIABILITY ON CSIRO THAT CANNOT BE EXCLUDED, RESTRICTED OR MODIFIED TO THE FULL EXTENT SET OUT IN THE EXPRESS TERMS OF THIS CLAUSE ABOVE "CONSUMER GUARANTEES". TO THE EXTENT THAT SUCH CONSUMER GUARANTEES CONTINUE TO APPLY, THEN TO THE FULL EXTENT PERMITTED BY THE APPLICABLE LEGISLATION, THE LIABILITY OF CSIRO UNDER THE RELEVANT CONSUMER GUARANTEE IS LIMITED (WHERE PERMITTED AT CSIRO'S OPTION) TO ONE OF FOLLOWING REMEDIES OR SUBSTANTIALLY EQUIVALENT REMEDIES: 14 | (a) THE REPLACEMENT OF THE SOFTWARE, THE SUPPLY OF EQUIVALENT SOFTWARE, OR SUPPLYING RELEVANT SERVICES AGAIN; 15 | (b) THE REPAIR OF THE SOFTWARE; 16 | (c) THE PAYMENT OF THE COST OF REPLACING THE SOFTWARE, OF ACQUIRING EQUIVALENT SOFTWARE, HAVING THE RELEVANT SERVICES SUPPLIED AGAIN, OR HAVING THE SOFTWARE REPAIRED. 17 | IN THIS CLAUSE, CSIRO INCLUDES ANY THIRD PARTY AUTHOR OR OWNER OF ANY PART OF THE SOFTWARE OR MATERIAL DISTRIBUTED WITH IT. CSIRO MAY ENFORCE ANY RIGHTS ON BEHALF OF THE RELEVANT THIRD PARTY. 18 | Third Party Components 19 | The following third party components are distributed with the Software. You agree to comply with the licence terms for these components as part of accessing the Software. Other third party software may also be identified in separate files distributed with the Software. 20 | 21 | --- 22 | 23 | The MIT License (MIT) 24 | 25 | Copyright (c) 2013 Cameron Beccario 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy 28 | of this software and associated documentation files (the "Software"), to deal 29 | in the Software without restriction, including without limitation the rights 30 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 31 | copies of the Software, and to permit persons to whom the Software is 32 | furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in 35 | all copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 43 | THE SOFTWARE. 44 | 45 | --- 46 | 47 | L.CanvasLayer.js : 48 | Licensed under MIT 49 | Copyright (c) 2016 Stanislav Sumbera, 50 | http://blog.sumbera.com/2014/04/20/leaflet-canvas/ 51 | 52 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 53 | (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, 54 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 55 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 56 | 57 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 58 | 59 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 60 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 61 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 62 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 63 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 64 | 65 | --- 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # leaflet-velocity [![NPM version][npm-image]][npm-url] [![NPM Downloads][npm-downloads-image]][npm-url] 2 | 3 | ## Version 2 Notice 4 | 5 | As of version 2, `leaflet-velocity` is now under [CSIRO](https://www.csiro.au)'s [Open Source Software Licence Agreement](LICENSE.md), which is variation of the BSD / MIT License. 6 | 7 | There are no other plans for changes to licensing, and the project will remain open source. 8 | 9 | --- 10 | 11 | A plugin for Leaflet (v1.0.3, and v0.7.7) to create a canvas visualisation layer for direction and intensity of arbitrary velocities (e.g. wind, ocean current). 12 | 13 | Live Demo: https://onaci.github.io/leaflet-velocity/ 14 | 15 | - Uses a modified version of [WindJS](https://github.com/Esri/wind-js) for core functionality. 16 | - Similar to [wind-js-leaflet](https://github.com/danwild/wind-js-leaflet), however much more versatile (provides a generic leaflet layer, and not restricted to wind). 17 | - Data input format is the same as output by [wind-js-server](https://github.com/danwild/wind-js-server), using [grib2json](https://github.com/cambecc/grib2json). 18 | 19 | ![Screenshot](/screenshots/velocity.gif?raw=true) 20 | 21 | ## Example use: 22 | 23 | ```javascript 24 | var velocityLayer = L.velocityLayer({ 25 | displayValues: true, 26 | displayOptions: { 27 | // label prefix 28 | velocityType: "Global Wind", 29 | 30 | // leaflet control position 31 | position: "bottomleft", 32 | 33 | // no data at cursor 34 | emptyString: "No velocity data", 35 | 36 | // see explanation below 37 | angleConvention: "bearingCW", 38 | 39 | // display cardinal direction alongside degrees 40 | showCardinal: false, 41 | 42 | // one of: ['ms', 'k/h', 'mph', 'kt'] 43 | speedUnit: "ms", 44 | 45 | // direction label prefix 46 | directionString: "Direction", 47 | 48 | // speed label prefix 49 | speedString: "Speed", 50 | }, 51 | data: data, // see demo/*.json, or wind-js-server for example data service 52 | 53 | // OPTIONAL 54 | minVelocity: 0, // used to align color scale 55 | maxVelocity: 10, // used to align color scale 56 | velocityScale: 0.005, // modifier for particle animations, arbitrarily defaults to 0.005 57 | colorScale: [], // define your own array of hex/rgb colors 58 | onAdd: null, // callback function 59 | onRemove: null, // callback function 60 | opacity: 0.97, // layer opacity, default 0.97 61 | 62 | // optional pane to add the layer, will be created if doesn't exist 63 | // leaflet v1+ only (falls back to overlayPane for < v1) 64 | paneName: "overlayPane", 65 | }); 66 | ``` 67 | 68 | The angle convention option refers to the convention used to express the wind direction as an angle from north direction in the control. 69 | It can be any combination of `bearing` (angle toward which the flow goes) or `meteo` (angle from which the flow comes), 70 | and `CW` (angle value increases clock-wise) or `CCW` (angle value increases counter clock-wise). If not given defaults to `bearingCCW`. 71 | 72 | The speed unit option refers to the unit used to express the wind speed in the control. 73 | It can be `m/s` for meter per second, `k/h` for kilometer per hour or `kt` for knots. If not given defaults to `m/s`. 74 | 75 | ## Public methods 76 | 77 | | method | params | description | 78 | | ------------ | ---------- | --------------------------------- | 79 | | `setData` | `{Object}` | update the layer with new data | 80 | | `setOptions` | `{Object}` | update the layer with new options | 81 | 82 | ## Build / watch 83 | 84 | ```shell 85 | npm run watch 86 | ``` 87 | 88 | ## Reference 89 | 90 | `leaflet-velocity` is possible because of things like: 91 | 92 | - [L.CanvasOverlay.js](https://gist.github.com/Sumbera/11114288) 93 | - [WindJS](https://github.com/Esri/wind-js) 94 | - [earth](https://github.com/cambecc/earth) 95 | 96 | ## Example data 97 | 98 | Data shown for the Great Barrier Reef has been derived from [CSIRO's eReefs products](https://research.csiro.au/ereefs/) 99 | 100 | ## License 101 | 102 | CSIRO Open Source Software Licence Agreement (variation of the BSD / MIT License) 103 | 104 | [npm-image]: https://badge.fury.io/js/leaflet-velocity.svg 105 | [npm-url]: https://www.npmjs.com/package/leaflet-velocity 106 | [npm-downloads-image]: https://img.shields.io/npm/dt/leaflet-velocity.svg 107 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; 3 | } 4 | 5 | #heading { 6 | text-align: center; 7 | padding: 20px; 8 | background: #333; 9 | color: #CCC; 10 | } 11 | 12 | a { 13 | color: #3388ff; 14 | } 15 | 16 | #map { 17 | position: absolute; 18 | height: 100%; 19 | width: 100%; 20 | background-color: #333; 21 | } 22 | 23 | .leaflet-canvas-layer { 24 | opacity: 0.55; 25 | } -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LeafletVelocity Demo 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | function initDemoMap() { 2 | var Esri_WorldImagery = L.tileLayer( 3 | "http://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", 4 | { 5 | attribution: 6 | "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, " + 7 | "AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community" 8 | } 9 | ); 10 | 11 | var Esri_DarkGreyCanvas = L.tileLayer( 12 | "http://{s}.sm.mapstack.stamen.com/" + 13 | "(toner-lite,$fff[difference],$fff[@23],$fff[hsl-saturation@20])/" + 14 | "{z}/{x}/{y}.png", 15 | { 16 | attribution: 17 | "Tiles © Esri — Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, " + 18 | "NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community" 19 | } 20 | ); 21 | 22 | var baseLayers = { 23 | Satellite: Esri_WorldImagery, 24 | "Grey Canvas": Esri_DarkGreyCanvas 25 | }; 26 | 27 | var map = L.map("map", { 28 | layers: [Esri_WorldImagery] 29 | }); 30 | 31 | var layerControl = L.control.layers(baseLayers); 32 | layerControl.addTo(map); 33 | map.setView([-22, 150], 5); 34 | 35 | return { 36 | map: map, 37 | layerControl: layerControl 38 | }; 39 | } 40 | 41 | // demo map 42 | var mapStuff = initDemoMap(); 43 | var map = mapStuff.map; 44 | var layerControl = mapStuff.layerControl; 45 | 46 | // load data (u, v grids) from somewhere (e.g. https://github.com/danwild/wind-js-server) 47 | $.getJSON("wind-gbr.json", function(data) { 48 | var velocityLayer = L.velocityLayer({ 49 | displayValues: true, 50 | displayOptions: { 51 | velocityType: "GBR Wind", 52 | position: "bottomleft", 53 | emptyString: "No wind data", 54 | showCardinal: true 55 | }, 56 | data: data, 57 | maxVelocity: 10 58 | }); 59 | 60 | layerControl.addOverlay(velocityLayer, "Wind - Great Barrier Reef"); 61 | }); 62 | 63 | $.getJSON("water-gbr.json", function(data) { 64 | var velocityLayer = L.velocityLayer({ 65 | displayValues: true, 66 | displayOptions: { 67 | velocityType: "GBR Water", 68 | position: "bottomleft", 69 | emptyString: "No water data" 70 | }, 71 | data: data, 72 | maxVelocity: 0.6, 73 | velocityScale: 0.1 // arbitrary default 0.005 74 | }); 75 | 76 | layerControl.addOverlay(velocityLayer, "Ocean Current - Great Barrier Reef"); 77 | }); 78 | 79 | $.getJSON("wind-global.json", function(data) { 80 | var velocityLayer = L.velocityLayer({ 81 | displayValues: true, 82 | displayOptions: { 83 | velocityType: "Global Wind", 84 | position: "bottomleft", 85 | emptyString: "No wind data" 86 | }, 87 | data: data, 88 | maxVelocity: 15 89 | }); 90 | 91 | layerControl.addOverlay(velocityLayer, "Wind - Global"); 92 | }); 93 | -------------------------------------------------------------------------------- /demo/water-gbr.json: -------------------------------------------------------------------------------- 1 | [{"header": {"parameterUnit": "m.s-1", "parameterNumber": 2, "dx": 1.0, "dy": 1.0, "parameterNumberName": "Eastward current", "la1": -7.5, "la2": -28.5, "parameterCategory": 2, "lo2": 156, "nx": 14, "ny": 22, "refTime": "2017-02-01 23:00:00", "lo1": 143}, "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.33000001311302185, 0.25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.12999999523162842, -0.0, 0.6499999761581421, 0.44999998807907104, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.03999999910593033, -0.1899999976158142, 0.4300000071525574, 0.23999999463558197, 0.44999998807907104, 0.23000000417232513, 0.4399999976158142, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.1599999964237213, 0.0, -0.019999999552965164, -0.05000000074505806, 0.23000000417232513, 0.3700000047683716, 0.550000011920929, 0.2800000011920929, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.11999999731779099, -0.33000001311302185, 0.05999999865889549, 0.12999999523162842, 0.3199999928474426, 0.28999999165534973, 0.2800000011920929, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.20999999344348907, -0.30000001192092896, -0.8199999928474426, -0.27000001072883606, -0.019999999552965164, 0.23000000417232513, 0.20000000298023224, 0.10000000149011612, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.12999999523162842, -0.1899999976158142, -0.18000000715255737, -0.44999998807907104, 0.11999999731779099, -0.05000000074505806, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.20999999344348907, -0.1899999976158142, -0.27000001072883606, -0.6499999761581421, -0.3199999928474426, -0.3199999928474426, -0.07000000029802322, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.23999999463558197, -0.14000000059604645, -0.15000000596046448, -0.09000000357627869, -0.3799999952316284, -0.33000001311302185, -0.17000000178813934, -0.28999999165534973, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.03999999910593033, -0.17000000178813934, 0.07000000029802322, -0.27000001072883606, -0.10999999940395355, -0.1899999976158142, -0.019999999552965164, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.14000000059604645, -0.05999999865889549, -0.05000000074505806, -0.36000001430511475, -0.3700000047683716, -0.20000000298023224, -0.20999999344348907, -0.09000000357627869, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.05000000074505806, -0.019999999552965164, 0.1599999964237213, 0.3100000023841858, -0.18000000715255737, -0.1599999964237213, -0.25999999046325684, -0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.05999999865889549, -0.27000001072883606, -0.27000001072883606, 0.4099999964237213, 0.23999999463558197, -0.09000000357627869, 0.009999999776482582, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.20999999344348907, -0.3499999940395355, -0.23999999463558197, 0.009999999776482582, -0.14000000059604645, -0.18000000715255737, -0.1599999964237213, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.25, -0.23000000417232513, 0.17000000178813934, -0.17000000178813934, 0.09000000357627869, 0.1599999964237213, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.05000000074505806, -0.1899999976158142, 0.10999999940395355, -0.03999999910593033, -0.10999999940395355, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.009999999776482582, -0.009999999776482582, -0.23999999463558197, -0.33000001311302185, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.25999999046325684, -0.03999999910593033, 0.11999999731779099, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.11999999731779099, -0.17000000178813934, 0.25999999046325684, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.05000000074505806, -0.6100000143051147, -0.2199999988079071, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5600000023841858, 0.3100000023841858, 0.36000001430511475]}, {"header": {"parameterUnit": "m.s-1", "parameterNumber": 3, "dx": 1.0, "dy": 1.0, "parameterNumberName": "Northward current", "la1": -7.5, "la2": -28.5, "parameterCategory": 2, "lo2": 156, "nx": 14, "ny": 22, "refTime": "2017-02-01 23:00:00", "lo1": 143}, "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1899999976158142, 0.28999999165534973, 0.019999999552965164, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2800000011920929, 0.20999999344348907, 0.44999998807907104, 0.11999999731779099, -0.3100000023841858, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.18000000715255737, 0.36000001430511475, 0.20000000298023224, -0.20000000298023224, -0.25, -0.3100000023841858, 0.10000000149011612, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.15000000596046448, 0.38999998569488525, 0.14000000059604645, -0.1599999964237213, -0.28999999165534973, -0.1599999964237213, -0.1599999964237213, 0.10999999940395355, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.75, 0.23000000417232513, -0.07000000029802322, -0.3400000035762787, -0.18000000715255737, -0.3799999952316284, -0.3700000047683716, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.17000000178813934, 0.3400000035762787, -0.009999999776482582, -0.029999999329447746, -0.25999999046325684, -0.07000000029802322, -0.25999999046325684, -0.33000001311302185, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.05999999865889549, 0.03999999910593033, 0.05000000074505806, -0.1599999964237213, -0.0, 0.009999999776482582, -0.2199999988079071, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.05000000074505806, -0.2199999988079071, -0.05999999865889549, -0.3799999952316284, -0.20000000298023224, -0.07000000029802322, 0.2199999988079071, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.019999999552965164, 0.2199999988079071, 0.05000000074505806, -0.10999999940395355, -0.25, -0.12999999523162842, 0.11999999731779099, 0.07000000029802322, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.07000000029802322, -0.05999999865889549, -0.1899999976158142, -0.41999998688697815, -0.10000000149011612, -0.12999999523162842, 0.009999999776482582, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.10000000149011612, -0.12999999523162842, 0.019999999552965164, -0.28999999165534973, -0.12999999523162842, 0.20000000298023224, 0.07999999821186066, -0.4399999976158142, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.11999999731779099, -0.7599999904632568, -0.49000000953674316, -0.550000011920929, -0.10000000149011612, 0.09000000357627869, 0.07000000029802322, -0.23999999463558197, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.47999998927116394, -0.5299999713897705, -0.550000011920929, -0.47999998927116394, -0.05000000074505806, 0.03999999910593033, -0.3700000047683716, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5400000214576721, -0.019999999552965164, -0.3199999928474426, -0.18000000715255737, -0.09000000357627869, -0.1599999964237213, -0.3499999940395355, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.029999999329447746, 0.029999999329447746, -0.15000000596046448, -0.23000000417232513, -0.019999999552965164, -0.3799999952316284, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.1599999964237213, -0.18000000715255737, -0.05999999865889549, 0.5199999809265137, -0.07000000029802322, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.3100000023841858, -0.4699999988079071, 0.2800000011920929, -0.41999998688697815, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5799999833106995, 0.019999999552965164, -0.4000000059604645, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.47999998927116394, -0.019999999552965164, -0.09000000357627869, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.7699999809265137, -0.019999999552965164, -0.05999999865889549, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.1699999570846558, 0.33000001311302185, -0.05000000074505806]}] -------------------------------------------------------------------------------- /demo/wind-gbr.json: -------------------------------------------------------------------------------- 1 | [{"header": {"parameterUnit": "m.s-1", "parameterNumber": 2, "dx": 1.0, "dy": 1.0, "parameterNumberName": "eastward_wind", "la1": -7.5, "la2": -28.5, "parameterCategory": 2, "lo2": 156.0, "nx": 14, "ny": 22, "refTime": "2017-02-01 23:00:00", "lo1": 143.0}, "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 4.170000076293945, 2.9800000190734863, 2.5799999237060547, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 5.400000095367432, 4.360000133514404, 3.859999895095825, 3.5799999237060547, 3.440000057220459, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.3399999141693115, 3.9100000858306885, 3.9000000953674316, 3.890000104904175, 3.7799999713897705, 6.46999979019165, 5.840000152587891, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.20000000298023224, -0.03999999910593033, 0.9300000071525574, 1.5199999809265137, 2.390000104904175, 3.930000066757202, 4.25, 3.069999933242798, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.850000023841858, -2.4700000286102295, -2.2899999618530273, -1.309999942779541, 0.2800000011920929, 1.0299999713897705, 0.7400000095367432, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -3.9000000953674316, -3.1500000953674316, -3.569999933242798, -3.5999999046325684, -2.819999933242798, -2.509999990463257, -2.9600000381469727, -3.2200000286102295, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -3.880000114440918, -4.5, -4.960000038146973, -5.090000152587891, -4.949999809265137, -4.679999828338623, -4.789999961853027, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -5.300000190734863, -5.590000152587891, -5.659999847412109, -5.489999771118164, -5.550000190734863, -5.400000095367432, -5.429999828338623, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -5.389999866485596, -5.800000190734863, -6.179999828338623, -6.059999942779541, -5.570000171661377, -5.880000114440918, -6.5, -7.239999771118164, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -6.210000038146973, -6.699999809265137, -6.789999961853027, -7.059999942779541, -7.670000076293945, -7.789999961853027, -8.130000114440918, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -7.420000076293945, -7.489999771118164, -7.239999771118164, -7.889999866485596, -8.079999923706055, -7.909999847412109, -7.559999942779541, -7.360000133514404, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -7.940000057220459, -7.260000228881836, -7.590000152587891, -7.920000076293945, -7.630000114440918, -7.710000038146973, -7.480000019073486, -7.21999979019165, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -6.539999961853027, -7.400000095367432, -7.519999980926514, -7.28000020980835, -7.769999980926514, -7.480000019073486, -7.389999866485596, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -6.760000228881836, -6.659999847412109, -6.889999866485596, -7.559999942779541, -7.389999866485596, -7.639999866485596, -8.069999694824219, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -5.710000038146973, -6.420000076293945, -6.880000114440918, -7.079999923706055, -7.380000114440918, -7.71999979019165, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -5.900000095367432, -6.510000228881836, -6.460000038146973, -6.710000038146973, -6.599999904632568, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -5.260000228881836, -5.71999979019165, -5.829999923706055, -6.059999942779541, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -4.260000228881836, -4.5, -5.360000133514404, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -2.859999895095825, -2.75, -3.869999885559082, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.8199999928474426, -1.7699999809265137, -2.25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.20999999344348907, -1.4199999570846558, -1.25]}, {"header": {"parameterUnit": "m.s-1", "parameterNumber": 3, "dx": 1.0, "dy": 1.0, "parameterNumberName": "northward_wind", "la1": -7.5, "la2": -28.5, "parameterCategory": 2, "lo2": 156.0, "nx": 14, "ny": 22, "refTime": "2017-02-01 23:00:00", "lo1": 143.0}, "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -3.880000114440918, -3.759999990463257, -3.5799999237060547, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.2599999904632568, -0.25, -1.75, -3.430000066757202, -4.690000057220459, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.8799999952316284, -0.7300000190734863, -1.1699999570846558, -2.680000066757202, -4.329999923706055, -3.1700000762939453, -1.059999942779541, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5099999904632568, -0.38999998569488525, -0.550000011920929, -1.2799999713897705, -3.0399999618530273, -2.75, -0.7200000286102295, 1.4500000476837158, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.8799999952316284, 1.2999999523162842, -0.5299999713897705, -1.9700000286102295, -1.3899999856948853, -0.75, -0.12999999523162842, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.9700000286102295, 2.059999942779541, 1.3700000047683716, 0.019999999552965164, -0.6700000166893005, -0.47999998927116394, 0.4399999976158142, 1.340000033378601, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.880000114440918, 2.5299999713897705, 1.159999966621399, 0.6800000071525574, 0.6399999856948853, 0.8799999952316284, 1.2000000476837158, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.190000057220459, 1.2899999618530273, 1.3799999952316284, 1.5700000524520874, 1.7999999523162842, 2.0199999809265137, 2.2100000381469727, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 4.329999923706055, 2.299999952316284, 2.440000057220459, 2.3499999046325684, 2.259999990463257, 1.2599999904632568, 1.7799999713897705, 1.600000023841858, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.9200000762939453, 2.690000057220459, 2.7799999713897705, 2.390000104904175, 1.5399999618530273, 1.4500000476837158, 1.9299999475479126, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.809999942779541, 3.359999895095825, 3.3499999046325684, 2.3399999141693115, 1.940000057220459, 1.7999999523162842, 2.359999895095825, 3.2200000286102295, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.4200000762939453, 2.990000009536743, 1.3200000524520874, 1.1799999475479126, 1.7999999523162842, 2.609999895095825, 3.259999990463257, 4.139999866485596, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.440000057220459, 1.6299999952316284, 1.4199999570846558, 1.2100000381469727, 1.7100000381469727, 2.6700000762939453, 2.2799999713897705, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.3600000143051147, 1.4500000476837158, 1.1799999475479126, 1.6299999952316284, 1.5299999713897705, 1.9800000190734863, 2.1500000953674316, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.1899999976158142, -0.30000001192092896, 0.6800000071525574, 0.25, 0.3700000047683716, 1.0499999523162842, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.8600000143051147, -0.9399999976158142, -0.7300000190734863, -0.28999999165534973, -0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -3.4800000190734863, -2.9200000762939453, -2.140000104904175, -1.590000033378601, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -4.510000228881836, -3.809999942779541, -3.059999942779541, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -6.559999942779541, -4.96999979019165, -4.449999809265137, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -7.840000152587891, -6.809999942779541, -5.869999885559082, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -9.369999885559082, -8.289999961853027, -7.130000114440918]}] -------------------------------------------------------------------------------- /dist/leaflet-velocity.css: -------------------------------------------------------------------------------- 1 | .leaflet-control-velocity { 2 | background-color: rgba(255, 255, 255, 0.7); 3 | padding: 0 5px; 4 | margin: 0 !important; 5 | color: #333; 6 | font: 11px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; 7 | } 8 | 9 | .velocity-overlay { 10 | position: absolute; 11 | z-index: 1; 12 | } -------------------------------------------------------------------------------- /dist/leaflet-velocity.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | Generic Canvas Layer for leaflet 0.7 and 1.0-rc, 5 | copyright Stanislav Sumbera, 2016 , sumbera.com , license MIT 6 | originally created and motivated by L.CanvasOverlay available here: https://gist.github.com/Sumbera/11114288 7 | 8 | */ 9 | // -- L.DomUtil.setTransform from leaflet 1.0.0 to work on 0.0.7 10 | //------------------------------------------------------------------------------ 11 | if (!L.DomUtil.setTransform) { 12 | L.DomUtil.setTransform = function (el, offset, scale) { 13 | var pos = offset || new L.Point(0, 0); 14 | el.style[L.DomUtil.TRANSFORM] = (L.Browser.ie3d ? "translate(" + pos.x + "px," + pos.y + "px)" : "translate3d(" + pos.x + "px," + pos.y + "px,0)") + (scale ? " scale(" + scale + ")" : ""); 15 | }; 16 | } // -- support for both 0.0.7 and 1.0.0 rc2 leaflet 17 | 18 | 19 | L.CanvasLayer = (L.Layer ? L.Layer : L.Class).extend({ 20 | // -- initialized is called on prototype 21 | initialize: function initialize(options) { 22 | this._map = null; 23 | this._canvas = null; 24 | this._frame = null; 25 | this._delegate = null; 26 | L.setOptions(this, options); 27 | }, 28 | delegate: function delegate(del) { 29 | this._delegate = del; 30 | return this; 31 | }, 32 | needRedraw: function needRedraw() { 33 | if (!this._frame) { 34 | this._frame = L.Util.requestAnimFrame(this.drawLayer, this); 35 | } 36 | 37 | return this; 38 | }, 39 | //------------------------------------------------------------- 40 | _onLayerDidResize: function _onLayerDidResize(resizeEvent) { 41 | this._canvas.width = resizeEvent.newSize.x; 42 | this._canvas.height = resizeEvent.newSize.y; 43 | }, 44 | //------------------------------------------------------------- 45 | _onLayerDidMove: function _onLayerDidMove() { 46 | var topLeft = this._map.containerPointToLayerPoint([0, 0]); 47 | 48 | L.DomUtil.setPosition(this._canvas, topLeft); 49 | this.drawLayer(); 50 | }, 51 | //------------------------------------------------------------- 52 | getEvents: function getEvents() { 53 | var events = { 54 | resize: this._onLayerDidResize, 55 | moveend: this._onLayerDidMove 56 | }; 57 | 58 | if (this._map.options.zoomAnimation && L.Browser.any3d) { 59 | events.zoomanim = this._animateZoom; 60 | } 61 | 62 | return events; 63 | }, 64 | //------------------------------------------------------------- 65 | onAdd: function onAdd(map) { 66 | this._map = map; 67 | this._canvas = L.DomUtil.create("canvas", "leaflet-layer"); 68 | this.tiles = {}; 69 | 70 | var size = this._map.getSize(); 71 | 72 | this._canvas.width = size.x; 73 | this._canvas.height = size.y; 74 | var animated = this._map.options.zoomAnimation && L.Browser.any3d; 75 | L.DomUtil.addClass(this._canvas, "leaflet-zoom-" + (animated ? "animated" : "hide")); 76 | this.options.pane.appendChild(this._canvas); 77 | map.on(this.getEvents(), this); 78 | var del = this._delegate || this; 79 | del.onLayerDidMount && del.onLayerDidMount(); // -- callback 80 | 81 | this.needRedraw(); 82 | var self = this; 83 | setTimeout(function () { 84 | self._onLayerDidMove(); 85 | }, 0); 86 | }, 87 | //------------------------------------------------------------- 88 | onRemove: function onRemove(map) { 89 | var del = this._delegate || this; 90 | del.onLayerWillUnmount && del.onLayerWillUnmount(); // -- callback 91 | 92 | this.options.pane.removeChild(this._canvas); 93 | map.off(this.getEvents(), this); 94 | this._canvas = null; 95 | }, 96 | //------------------------------------------------------------ 97 | addTo: function addTo(map) { 98 | map.addLayer(this); 99 | return this; 100 | }, 101 | //------------------------------------------------------------------------------ 102 | drawLayer: function drawLayer() { 103 | // -- todo make the viewInfo properties flat objects. 104 | var size = this._map.getSize(); 105 | 106 | var bounds = this._map.getBounds(); 107 | 108 | var zoom = this._map.getZoom(); 109 | 110 | var center = this._map.options.crs.project(this._map.getCenter()); 111 | 112 | var corner = this._map.options.crs.project(this._map.containerPointToLatLng(this._map.getSize())); 113 | 114 | var del = this._delegate || this; 115 | del.onDrawLayer && del.onDrawLayer({ 116 | layer: this, 117 | canvas: this._canvas, 118 | bounds: bounds, 119 | size: size, 120 | zoom: zoom, 121 | center: center, 122 | corner: corner 123 | }); 124 | this._frame = null; 125 | }, 126 | // -- L.DomUtil.setTransform from leaflet 1.0.0 to work on 0.0.7 127 | //------------------------------------------------------------------------------ 128 | _setTransform: function _setTransform(el, offset, scale) { 129 | var pos = offset || new L.Point(0, 0); 130 | el.style[L.DomUtil.TRANSFORM] = (L.Browser.ie3d ? "translate(" + pos.x + "px," + pos.y + "px)" : "translate3d(" + pos.x + "px," + pos.y + "px,0)") + (scale ? " scale(" + scale + ")" : ""); 131 | }, 132 | //------------------------------------------------------------------------------ 133 | _animateZoom: function _animateZoom(e) { 134 | var scale = this._map.getZoomScale(e.zoom); // -- different calc of offset in leaflet 1.0.0 and 0.0.7 thanks for 1.0.0-rc2 calc @jduggan1 135 | 136 | 137 | var offset = L.Layer ? this._map._latLngToNewLayerPoint(this._map.getBounds().getNorthWest(), e.zoom, e.center) : this._map._getCenterOffset(e.center)._multiplyBy(-scale).subtract(this._map._getMapPanePos()); 138 | L.DomUtil.setTransform(this._canvas, offset, scale); 139 | } 140 | }); 141 | 142 | L.canvasLayer = function (pane) { 143 | return new L.CanvasLayer(pane); 144 | }; 145 | 146 | L.Control.Velocity = L.Control.extend({ 147 | options: { 148 | position: "bottomleft", 149 | emptyString: "Unavailable", 150 | // Could be any combination of 'bearing' (angle toward which the flow goes) or 'meteo' (angle from which the flow comes) 151 | // and 'CW' (angle value increases clock-wise) or 'CCW' (angle value increases counter clock-wise) 152 | angleConvention: "bearingCCW", 153 | showCardinal: false, 154 | // Could be 'm/s' for meter per second, 'k/h' for kilometer per hour, 'mph' for miles per hour or 'kt' for knots 155 | speedUnit: "m/s", 156 | directionString: "Direction", 157 | speedString: "Speed", 158 | onAdd: null, 159 | onRemove: null 160 | }, 161 | onAdd: function onAdd(map) { 162 | this._container = L.DomUtil.create("div", "leaflet-control-velocity"); 163 | L.DomEvent.disableClickPropagation(this._container); 164 | map.on("mousemove", this._onMouseMove, this); 165 | this._container.innerHTML = this.options.emptyString; 166 | if (this.options.leafletVelocity.options.onAdd) this.options.leafletVelocity.options.onAdd(); 167 | return this._container; 168 | }, 169 | onRemove: function onRemove(map) { 170 | map.off("mousemove", this._onMouseMove, this); 171 | if (this.options.leafletVelocity.options.onRemove) this.options.leafletVelocity.options.onRemove(); 172 | }, 173 | vectorToSpeed: function vectorToSpeed(uMs, vMs, unit) { 174 | var velocityAbs = Math.sqrt(Math.pow(uMs, 2) + Math.pow(vMs, 2)); // Default is m/s 175 | 176 | if (unit === "k/h") { 177 | return this.meterSec2kilometerHour(velocityAbs); 178 | } else if (unit === "kt") { 179 | return this.meterSec2Knots(velocityAbs); 180 | } else if (unit === "mph") { 181 | return this.meterSec2milesHour(velocityAbs); 182 | } else { 183 | return velocityAbs; 184 | } 185 | }, 186 | vectorToDegrees: function vectorToDegrees(uMs, vMs, angleConvention) { 187 | // Default angle convention is CW 188 | if (angleConvention.endsWith("CCW")) { 189 | // vMs comes out upside-down.. 190 | vMs = vMs > 0 ? vMs = -vMs : Math.abs(vMs); 191 | } 192 | 193 | var velocityAbs = Math.sqrt(Math.pow(uMs, 2) + Math.pow(vMs, 2)); 194 | var velocityDir = Math.atan2(uMs / velocityAbs, vMs / velocityAbs); 195 | var velocityDirToDegrees = velocityDir * 180 / Math.PI + 180; 196 | 197 | if (angleConvention === "bearingCW" || angleConvention === "meteoCCW") { 198 | velocityDirToDegrees += 180; 199 | if (velocityDirToDegrees >= 360) velocityDirToDegrees -= 360; 200 | } 201 | 202 | return velocityDirToDegrees; 203 | }, 204 | degreesToCardinalDirection: function degreesToCardinalDirection(deg) { 205 | var cardinalDirection = ''; 206 | 207 | if (deg >= 0 && deg < 11.25 || deg >= 348.75) { 208 | cardinalDirection = 'N'; 209 | } else if (deg >= 11.25 && deg < 33.75) { 210 | cardinalDirection = 'NNW'; 211 | } else if (deg >= 33.75 && deg < 56.25) { 212 | cardinalDirection = 'NW'; 213 | } else if (deg >= 56.25 && deg < 78.75) { 214 | cardinalDirection = 'WNW'; 215 | } else if (deg >= 78.25 && deg < 101.25) { 216 | cardinalDirection = 'W'; 217 | } else if (deg >= 101.25 && deg < 123.75) { 218 | cardinalDirection = 'WSW'; 219 | } else if (deg >= 123.75 && deg < 146.25) { 220 | cardinalDirection = 'SW'; 221 | } else if (deg >= 146.25 && deg < 168.75) { 222 | cardinalDirection = 'SSW'; 223 | } else if (deg >= 168.75 && deg < 191.25) { 224 | cardinalDirection = 'S'; 225 | } else if (deg >= 191.25 && deg < 213.75) { 226 | cardinalDirection = 'SSE'; 227 | } else if (deg >= 213.75 && deg < 236.25) { 228 | cardinalDirection = 'SE'; 229 | } else if (deg >= 236.25 && deg < 258.75) { 230 | cardinalDirection = 'ESE'; 231 | } else if (deg >= 258.75 && deg < 281.25) { 232 | cardinalDirection = 'E'; 233 | } else if (deg >= 281.25 && deg < 303.75) { 234 | cardinalDirection = 'ENE'; 235 | } else if (deg >= 303.75 && deg < 326.25) { 236 | cardinalDirection = 'NE'; 237 | } else if (deg >= 326.25 && deg < 348.75) { 238 | cardinalDirection = 'NNE'; 239 | } 240 | 241 | return cardinalDirection; 242 | }, 243 | meterSec2Knots: function meterSec2Knots(meters) { 244 | return meters / 0.514; 245 | }, 246 | meterSec2kilometerHour: function meterSec2kilometerHour(meters) { 247 | return meters * 3.6; 248 | }, 249 | meterSec2milesHour: function meterSec2milesHour(meters) { 250 | return meters * 2.23694; 251 | }, 252 | _onMouseMove: function _onMouseMove(e) { 253 | var self = this; 254 | 255 | var pos = this.options.leafletVelocity._map.containerPointToLatLng(L.point(e.containerPoint.x, e.containerPoint.y)); 256 | 257 | var gridValue = this.options.leafletVelocity._windy.interpolatePoint(pos.lng, pos.lat); 258 | 259 | var htmlOut = ""; 260 | 261 | if (gridValue && !isNaN(gridValue[0]) && !isNaN(gridValue[1]) && gridValue[2]) { 262 | var deg = self.vectorToDegrees(gridValue[0], gridValue[1], this.options.angleConvention); 263 | var cardinal = this.options.showCardinal ? " (".concat(self.degreesToCardinalDirection(deg), ") ") : ''; 264 | htmlOut = " ".concat(this.options.velocityType, " ").concat(this.options.directionString, ": ").concat(deg.toFixed(2), "\xB0").concat(cardinal, ", ").concat(this.options.velocityType, " ").concat(this.options.speedString, ": ").concat(self.vectorToSpeed(gridValue[0], gridValue[1], this.options.speedUnit).toFixed(2), " ").concat(this.options.speedUnit); 265 | } else { 266 | htmlOut = this.options.emptyString; 267 | } 268 | 269 | self._container.innerHTML = htmlOut; 270 | } 271 | }); 272 | L.Map.mergeOptions({ 273 | positionControl: false 274 | }); 275 | L.Map.addInitHook(function () { 276 | if (this.options.positionControl) { 277 | this.positionControl = new L.Control.MousePosition(); 278 | this.addControl(this.positionControl); 279 | } 280 | }); 281 | 282 | L.control.velocity = function (options) { 283 | return new L.Control.Velocity(options); 284 | }; 285 | 286 | L.VelocityLayer = (L.Layer ? L.Layer : L.Class).extend({ 287 | options: { 288 | displayValues: true, 289 | displayOptions: { 290 | velocityType: "Velocity", 291 | position: "bottomleft", 292 | emptyString: "No velocity data" 293 | }, 294 | maxVelocity: 10, 295 | // used to align color scale 296 | colorScale: null, 297 | data: null 298 | }, 299 | _map: null, 300 | _canvasLayer: null, 301 | _windy: null, 302 | _context: null, 303 | _timer: 0, 304 | _mouseControl: null, 305 | initialize: function initialize(options) { 306 | L.setOptions(this, options); 307 | }, 308 | onAdd: function onAdd(map) { 309 | // determine where to add the layer 310 | this._paneName = this.options.paneName || "overlayPane"; // fall back to overlayPane for leaflet < 1 311 | 312 | var pane = map._panes.overlayPane; 313 | 314 | if (map.getPane) { 315 | // attempt to get pane first to preserve parent (createPane voids this) 316 | pane = map.getPane(this._paneName); 317 | 318 | if (!pane) { 319 | pane = map.createPane(this._paneName); 320 | } 321 | } // create canvas, add to map pane 322 | 323 | 324 | this._canvasLayer = L.canvasLayer({ 325 | pane: pane 326 | }).delegate(this); 327 | 328 | this._canvasLayer.addTo(map); 329 | 330 | this._map = map; 331 | }, 332 | onRemove: function onRemove(map) { 333 | this._destroyWind(); 334 | }, 335 | setData: function setData(data) { 336 | this.options.data = data; 337 | 338 | if (this._windy) { 339 | this._windy.setData(data); 340 | 341 | this._clearAndRestart(); 342 | } 343 | 344 | this.fire("load"); 345 | }, 346 | setOpacity: function setOpacity(opacity) { 347 | this._canvasLayer.setOpacity(opacity); 348 | }, 349 | setOptions: function setOptions(options) { 350 | this.options = Object.assign(this.options, options); 351 | 352 | if (options.hasOwnProperty("displayOptions")) { 353 | this.options.displayOptions = Object.assign(this.options.displayOptions, options.displayOptions); 354 | 355 | this._initMouseHandler(true); 356 | } 357 | 358 | if (options.hasOwnProperty("data")) this.options.data = options.data; 359 | 360 | if (this._windy) { 361 | this._windy.setOptions(options); 362 | 363 | if (options.hasOwnProperty("data")) this._windy.setData(options.data); 364 | 365 | this._clearAndRestart(); 366 | } 367 | 368 | this.fire("load"); 369 | }, 370 | 371 | /*------------------------------------ PRIVATE ------------------------------------------*/ 372 | onDrawLayer: function onDrawLayer(overlay, params) { 373 | var self = this; 374 | 375 | if (!this._windy) { 376 | this._initWindy(this); 377 | 378 | return; 379 | } 380 | 381 | if (!this.options.data) { 382 | return; 383 | } 384 | 385 | if (this._timer) clearTimeout(self._timer); 386 | this._timer = setTimeout(function () { 387 | self._startWindy(); 388 | }, 750); // showing velocity is delayed 389 | }, 390 | _startWindy: function _startWindy() { 391 | var bounds = this._map.getBounds(); 392 | 393 | var size = this._map.getSize(); // bounds, width, height, extent 394 | 395 | 396 | this._windy.start([[0, 0], [size.x, size.y]], size.x, size.y, [[bounds._southWest.lng, bounds._southWest.lat], [bounds._northEast.lng, bounds._northEast.lat]]); 397 | }, 398 | _initWindy: function _initWindy(self) { 399 | // windy object, copy options 400 | var options = Object.assign({ 401 | canvas: self._canvasLayer._canvas, 402 | map: this._map 403 | }, self.options); 404 | this._windy = new Windy(options); // prepare context global var, start drawing 405 | 406 | this._context = this._canvasLayer._canvas.getContext("2d"); 407 | 408 | this._canvasLayer._canvas.classList.add("velocity-overlay"); 409 | 410 | this.onDrawLayer(); 411 | 412 | this._map.on("dragstart", self._windy.stop); 413 | 414 | this._map.on("dragend", self._clearAndRestart); 415 | 416 | this._map.on("zoomstart", self._windy.stop); 417 | 418 | this._map.on("zoomend", self._clearAndRestart); 419 | 420 | this._map.on("resize", self._clearWind); 421 | 422 | this._initMouseHandler(false); 423 | }, 424 | _initMouseHandler: function _initMouseHandler(voidPrevious) { 425 | if (voidPrevious) { 426 | this._map.removeControl(this._mouseControl); 427 | 428 | this._mouseControl = false; 429 | } 430 | 431 | if (!this._mouseControl && this.options.displayValues) { 432 | var options = this.options.displayOptions || {}; 433 | options["leafletVelocity"] = this; 434 | this._mouseControl = L.control.velocity(options).addTo(this._map); 435 | } 436 | }, 437 | _clearAndRestart: function _clearAndRestart() { 438 | if (this._context) this._context.clearRect(0, 0, 3000, 3000); 439 | if (this._windy) this._startWindy(); 440 | }, 441 | _clearWind: function _clearWind() { 442 | if (this._windy) this._windy.stop(); 443 | if (this._context) this._context.clearRect(0, 0, 3000, 3000); 444 | }, 445 | _destroyWind: function _destroyWind() { 446 | if (this._timer) clearTimeout(this._timer); 447 | if (this._windy) this._windy.stop(); 448 | if (this._context) this._context.clearRect(0, 0, 3000, 3000); 449 | if (this._mouseControl) this._map.removeControl(this._mouseControl); 450 | this._mouseControl = null; 451 | this._windy = null; 452 | 453 | this._map.removeLayer(this._canvasLayer); 454 | } 455 | }); 456 | 457 | L.velocityLayer = function (options) { 458 | return new L.VelocityLayer(options); 459 | }; 460 | /* Global class for simulating the movement of particle through a 1km wind grid 461 | 462 | credit: All the credit for this work goes to: https://github.com/cambecc for creating the repo: 463 | https://github.com/cambecc/earth. The majority of this code is directly take nfrom there, since its awesome. 464 | 465 | This class takes a canvas element and an array of data (1km GFS from http://www.emc.ncep.noaa.gov/index.php?branch=GFS) 466 | and then uses a mercator (forward/reverse) projection to correctly map wind vectors in "map space". 467 | 468 | The "start" method takes the bounds of the map at its current extent and starts the whole gridding, 469 | interpolation and animation process. 470 | */ 471 | 472 | 473 | var Windy = function Windy(params) { 474 | var MIN_VELOCITY_INTENSITY = params.minVelocity || 0; // velocity at which particle intensity is minimum (m/s) 475 | 476 | var MAX_VELOCITY_INTENSITY = params.maxVelocity || 10; // velocity at which particle intensity is maximum (m/s) 477 | 478 | var VELOCITY_SCALE = (params.velocityScale || 0.005) * (Math.pow(window.devicePixelRatio, 1 / 3) || 1); // scale for wind velocity (completely arbitrary--this value looks nice) 479 | 480 | var MAX_PARTICLE_AGE = params.particleAge || 90; // max number of frames a particle is drawn before regeneration 481 | 482 | var PARTICLE_LINE_WIDTH = params.lineWidth || 1; // line width of a drawn particle 483 | 484 | var PARTICLE_MULTIPLIER = params.particleMultiplier || 1 / 300; // particle count scalar (completely arbitrary--this values looks nice) 485 | 486 | var PARTICLE_REDUCTION = Math.pow(window.devicePixelRatio, 1 / 3) || 1.6; // multiply particle count for mobiles by this amount 487 | 488 | var FRAME_RATE = params.frameRate || 15; 489 | var FRAME_TIME = 1000 / FRAME_RATE; // desired frames per second 490 | 491 | var OPACITY = 0.97; 492 | var defaulColorScale = ["rgb(36,104, 180)", "rgb(60,157, 194)", "rgb(128,205,193 )", "rgb(151,218,168 )", "rgb(198,231,181)", "rgb(238,247,217)", "rgb(255,238,159)", "rgb(252,217,125)", "rgb(255,182,100)", "rgb(252,150,75)", "rgb(250,112,52)", "rgb(245,64,32)", "rgb(237,45,28)", "rgb(220,24,32)", "rgb(180,0,35)"]; 493 | var colorScale = params.colorScale || defaulColorScale; 494 | var NULL_WIND_VECTOR = [NaN, NaN, null]; // singleton for no wind in the form: [u, v, magnitude] 495 | 496 | var builder; 497 | var grid; 498 | var gridData = params.data; 499 | var date; 500 | var λ0, φ0, Δλ, Δφ, ni, nj; 501 | 502 | var setData = function setData(data) { 503 | gridData = data; 504 | }; 505 | 506 | var setOptions = function setOptions(options) { 507 | if (options.hasOwnProperty("minVelocity")) MIN_VELOCITY_INTENSITY = options.minVelocity; 508 | if (options.hasOwnProperty("maxVelocity")) MAX_VELOCITY_INTENSITY = options.maxVelocity; 509 | if (options.hasOwnProperty("velocityScale")) VELOCITY_SCALE = (options.velocityScale || 0.005) * (Math.pow(window.devicePixelRatio, 1 / 3) || 1); 510 | if (options.hasOwnProperty("particleAge")) MAX_PARTICLE_AGE = options.particleAge; 511 | if (options.hasOwnProperty("lineWidth")) PARTICLE_LINE_WIDTH = options.lineWidth; 512 | if (options.hasOwnProperty("particleMultiplier")) PARTICLE_MULTIPLIER = options.particleMultiplier; 513 | if (options.hasOwnProperty("opacity")) OPACITY = +options.opacity; 514 | if (options.hasOwnProperty("frameRate")) FRAME_RATE = options.frameRate; 515 | FRAME_TIME = 1000 / FRAME_RATE; 516 | }; // interpolation for vectors like wind (u,v,m) 517 | 518 | 519 | var bilinearInterpolateVector = function bilinearInterpolateVector(x, y, g00, g10, g01, g11) { 520 | var rx = 1 - x; 521 | var ry = 1 - y; 522 | var a = rx * ry, 523 | b = x * ry, 524 | c = rx * y, 525 | d = x * y; 526 | var u = g00[0] * a + g10[0] * b + g01[0] * c + g11[0] * d; 527 | var v = g00[1] * a + g10[1] * b + g01[1] * c + g11[1] * d; 528 | return [u, v, Math.sqrt(u * u + v * v)]; 529 | }; 530 | 531 | var createWindBuilder = function createWindBuilder(uComp, vComp) { 532 | var uData = uComp.data, 533 | vData = vComp.data; 534 | return { 535 | header: uComp.header, 536 | //recipe: recipeFor("wind-" + uComp.header.surface1Value), 537 | data: function data(i) { 538 | return [uData[i], vData[i]]; 539 | }, 540 | interpolate: bilinearInterpolateVector 541 | }; 542 | }; 543 | 544 | var createBuilder = function createBuilder(data) { 545 | var uComp = null, 546 | vComp = null, 547 | scalar = null; 548 | data.forEach(function (record) { 549 | switch (record.header.parameterCategory + "," + record.header.parameterNumber) { 550 | case "1,2": 551 | case "2,2": 552 | uComp = record; 553 | break; 554 | 555 | case "1,3": 556 | case "2,3": 557 | vComp = record; 558 | break; 559 | 560 | default: 561 | scalar = record; 562 | } 563 | }); 564 | return createWindBuilder(uComp, vComp); 565 | }; 566 | 567 | var buildGrid = function buildGrid(data, callback) { 568 | var supported = true; 569 | if (data.length < 2) supported = false; 570 | if (!supported) console.log("Windy Error: data must have at least two components (u,v)"); 571 | builder = createBuilder(data); 572 | var header = builder.header; 573 | if (header.hasOwnProperty("gridDefinitionTemplate") && header.gridDefinitionTemplate != 0) supported = false; 574 | 575 | if (!supported) { 576 | console.log("Windy Error: Only data with Latitude_Longitude coordinates is supported"); 577 | } 578 | 579 | supported = true; // reset for futher checks 580 | 581 | λ0 = header.lo1; 582 | φ0 = header.la1; // the grid's origin (e.g., 0.0E, 90.0N) 583 | 584 | Δλ = header.dx; 585 | Δφ = header.dy; // distance between grid points (e.g., 2.5 deg lon, 2.5 deg lat) 586 | 587 | ni = header.nx; 588 | nj = header.ny; // number of grid points W-E and N-S (e.g., 144 x 73) 589 | 590 | if (header.hasOwnProperty("scanMode")) { 591 | var scanModeMask = header.scanMode.toString(2); 592 | scanModeMask = ('0' + scanModeMask).slice(-8); 593 | var scanModeMaskArray = scanModeMask.split('').map(Number).map(Boolean); 594 | if (scanModeMaskArray[0]) Δλ = -Δλ; 595 | if (scanModeMaskArray[1]) Δφ = -Δφ; 596 | if (scanModeMaskArray[2]) supported = false; 597 | if (scanModeMaskArray[3]) supported = false; 598 | if (scanModeMaskArray[4]) supported = false; 599 | if (scanModeMaskArray[5]) supported = false; 600 | if (scanModeMaskArray[6]) supported = false; 601 | if (scanModeMaskArray[7]) supported = false; 602 | if (!supported) console.log("Windy Error: Data with scanMode: " + header.scanMode + " is not supported."); 603 | } 604 | 605 | date = new Date(header.refTime); 606 | date.setHours(date.getHours() + header.forecastTime); // Scan modes 0, 64 allowed. 607 | // http://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_table3-4.shtml 608 | 609 | grid = []; 610 | var p = 0; 611 | var isContinuous = Math.floor(ni * Δλ) >= 360; 612 | 613 | for (var j = 0; j < nj; j++) { 614 | var row = []; 615 | 616 | for (var i = 0; i < ni; i++, p++) { 617 | row[i] = builder.data(p); 618 | } 619 | 620 | if (isContinuous) { 621 | // For wrapped grids, duplicate first column as last column to simplify interpolation logic 622 | row.push(row[0]); 623 | } 624 | 625 | grid[j] = row; 626 | } 627 | 628 | callback({ 629 | date: date, 630 | interpolate: interpolate 631 | }); 632 | }; 633 | /** 634 | * Get interpolated grid value from Lon/Lat position 635 | * @param λ {Float} Longitude 636 | * @param φ {Float} Latitude 637 | * @returns {Object} 638 | */ 639 | 640 | 641 | var interpolate = function interpolate(λ, φ) { 642 | if (!grid) return null; 643 | var i = floorMod(λ - λ0, 360) / Δλ; // calculate longitude index in wrapped range [0, 360) 644 | 645 | var j = (φ0 - φ) / Δφ; // calculate latitude index in direction +90 to -90 646 | 647 | var fi = Math.floor(i), 648 | ci = fi + 1; 649 | var fj = Math.floor(j), 650 | cj = fj + 1; 651 | var row; 652 | 653 | if (row = grid[fj]) { 654 | var g00 = row[fi]; 655 | var g10 = row[ci]; 656 | 657 | if (isValue(g00) && isValue(g10) && (row = grid[cj])) { 658 | var g01 = row[fi]; 659 | var g11 = row[ci]; 660 | 661 | if (isValue(g01) && isValue(g11)) { 662 | // All four points found, so interpolate the value. 663 | return builder.interpolate(i - fi, j - fj, g00, g10, g01, g11); 664 | } 665 | } 666 | } 667 | 668 | return null; 669 | }; 670 | /** 671 | * @returns {Boolean} true if the specified value is not null and not undefined. 672 | */ 673 | 674 | 675 | var isValue = function isValue(x) { 676 | return x !== null && x !== undefined; 677 | }; 678 | /** 679 | * @returns {Number} returns remainder of floored division, i.e., floor(a / n). Useful for consistent modulo 680 | * of negative numbers. See http://en.wikipedia.org/wiki/Modulo_operation. 681 | */ 682 | 683 | 684 | var floorMod = function floorMod(a, n) { 685 | return a - n * Math.floor(a / n); 686 | }; 687 | /** 688 | * @returns {Number} the value x clamped to the range [low, high]. 689 | */ 690 | 691 | 692 | var clamp = function clamp(x, range) { 693 | return Math.max(range[0], Math.min(x, range[1])); 694 | }; 695 | /** 696 | * @returns {Boolean} true if agent is probably a mobile device. Don't really care if this is accurate. 697 | */ 698 | 699 | 700 | var isMobile = function isMobile() { 701 | return /android|blackberry|iemobile|ipad|iphone|ipod|opera mini|webos/i.test(navigator.userAgent); 702 | }; 703 | /** 704 | * Calculate distortion of the wind vector caused by the shape of the projection at point (x, y). The wind 705 | * vector is modified in place and returned by this function. 706 | */ 707 | 708 | 709 | var distort = function distort(projection, λ, φ, x, y, scale, wind) { 710 | var u = wind[0] * scale; 711 | var v = wind[1] * scale; 712 | var d = distortion(projection, λ, φ, x, y); // Scale distortion vectors by u and v, then add. 713 | 714 | wind[0] = d[0] * u + d[2] * v; 715 | wind[1] = d[1] * u + d[3] * v; 716 | return wind; 717 | }; 718 | 719 | var distortion = function distortion(projection, λ, φ, x, y) { 720 | var τ = 2 * Math.PI; // var H = Math.pow(10, -5.2); // 0.00000630957344480193 721 | // var H = 0.0000360; // 0.0000360°φ ~= 4m (from https://github.com/cambecc/earth/blob/master/public/libs/earth/1.0.0/micro.js#L13) 722 | 723 | var H = 5; // ToDo: Why does this work? 724 | 725 | var hλ = λ < 0 ? H : -H; 726 | var hφ = φ < 0 ? H : -H; 727 | var pλ = project(φ, λ + hλ); 728 | var pφ = project(φ + hφ, λ); // Meridian scale factor (see Snyder, equation 4-3), where R = 1. This handles issue where length of 1º λ 729 | // changes depending on φ. Without this, there is a pinching effect at the poles. 730 | 731 | var k = Math.cos(φ / 360 * τ); 732 | return [(pλ[0] - x) / hλ / k, (pλ[1] - y) / hλ / k, (pφ[0] - x) / hφ, (pφ[1] - y) / hφ]; 733 | }; 734 | 735 | var createField = function createField(columns, bounds, callback) { 736 | /** 737 | * @returns {Array} wind vector [u, v, magnitude] at the point (x, y), or [NaN, NaN, null] if wind 738 | * is undefined at that point. 739 | */ 740 | function field(x, y) { 741 | var column = columns[Math.round(x)]; 742 | return column && column[Math.round(y)] || NULL_WIND_VECTOR; 743 | } // Frees the massive "columns" array for GC. Without this, the array is leaked (in Chrome) each time a new 744 | // field is interpolated because the field closure's context is leaked, for reasons that defy explanation. 745 | 746 | 747 | field.release = function () { 748 | columns = []; 749 | }; 750 | 751 | field.randomize = function (o) { 752 | // UNDONE: this method is terrible 753 | var x, y; 754 | var safetyNet = 0; 755 | 756 | do { 757 | x = Math.round(Math.floor(Math.random() * bounds.width) + bounds.x); 758 | y = Math.round(Math.floor(Math.random() * bounds.height) + bounds.y); 759 | } while (field(x, y)[2] === null && safetyNet++ < 30); 760 | 761 | o.x = x; 762 | o.y = y; 763 | return o; 764 | }; 765 | 766 | callback(bounds, field); 767 | }; 768 | 769 | var buildBounds = function buildBounds(bounds, width, height) { 770 | var upperLeft = bounds[0]; 771 | var lowerRight = bounds[1]; 772 | var x = Math.round(upperLeft[0]); //Math.max(Math.floor(upperLeft[0], 0), 0); 773 | 774 | var y = Math.max(Math.floor(upperLeft[1], 0), 0); 775 | var xMax = Math.min(Math.ceil(lowerRight[0], width), width - 1); 776 | var yMax = Math.min(Math.ceil(lowerRight[1], height), height - 1); 777 | return { 778 | x: x, 779 | y: y, 780 | xMax: width, 781 | yMax: yMax, 782 | width: width, 783 | height: height 784 | }; 785 | }; 786 | 787 | var deg2rad = function deg2rad(deg) { 788 | return deg / 180 * Math.PI; 789 | }; 790 | 791 | var invert = function invert(x, y, windy) { 792 | var latlon = params.map.containerPointToLatLng(L.point(x, y)); 793 | return [latlon.lng, latlon.lat]; 794 | }; 795 | 796 | var project = function project(lat, lon, windy) { 797 | var xy = params.map.latLngToContainerPoint(L.latLng(lat, lon)); 798 | return [xy.x, xy.y]; 799 | }; 800 | 801 | var interpolateField = function interpolateField(grid, bounds, extent, callback) { 802 | var projection = {}; // map.crs used instead 803 | 804 | var mapArea = (extent.south - extent.north) * (extent.west - extent.east); 805 | var velocityScale = VELOCITY_SCALE * Math.pow(mapArea, 0.4); 806 | var columns = []; 807 | var x = bounds.x; 808 | 809 | function interpolateColumn(x) { 810 | var column = []; 811 | 812 | for (var y = bounds.y; y <= bounds.yMax; y += 2) { 813 | var coord = invert(x, y); 814 | 815 | if (coord) { 816 | var λ = coord[0], 817 | φ = coord[1]; 818 | 819 | if (isFinite(λ)) { 820 | var wind = grid.interpolate(λ, φ); 821 | 822 | if (wind) { 823 | wind = distort(projection, λ, φ, x, y, velocityScale, wind); 824 | column[y + 1] = column[y] = wind; 825 | } 826 | } 827 | } 828 | } 829 | 830 | columns[x + 1] = columns[x] = column; 831 | } 832 | 833 | (function batchInterpolate() { 834 | var start = Date.now(); 835 | 836 | while (x < bounds.width) { 837 | interpolateColumn(x); 838 | x += 2; 839 | 840 | if (Date.now() - start > 1000) { 841 | //MAX_TASK_TIME) { 842 | setTimeout(batchInterpolate, 25); 843 | return; 844 | } 845 | } 846 | 847 | createField(columns, bounds, callback); 848 | })(); 849 | }; 850 | 851 | var animationLoop; 852 | 853 | var animate = function animate(bounds, field) { 854 | function windIntensityColorScale(min, max) { 855 | colorScale.indexFor = function (m) { 856 | // map velocity speed to a style 857 | return Math.max(0, Math.min(colorScale.length - 1, Math.round((m - min) / (max - min) * (colorScale.length - 1)))); 858 | }; 859 | 860 | return colorScale; 861 | } 862 | 863 | var colorStyles = windIntensityColorScale(MIN_VELOCITY_INTENSITY, MAX_VELOCITY_INTENSITY); 864 | var buckets = colorStyles.map(function () { 865 | return []; 866 | }); 867 | var particleCount = Math.round(bounds.width * bounds.height * PARTICLE_MULTIPLIER); 868 | 869 | if (isMobile()) { 870 | particleCount *= PARTICLE_REDUCTION; 871 | } 872 | 873 | var fadeFillStyle = "rgba(0, 0, 0, ".concat(OPACITY, ")"); 874 | var particles = []; 875 | 876 | for (var i = 0; i < particleCount; i++) { 877 | particles.push(field.randomize({ 878 | age: Math.floor(Math.random() * MAX_PARTICLE_AGE) + 0 879 | })); 880 | } 881 | 882 | function evolve() { 883 | buckets.forEach(function (bucket) { 884 | bucket.length = 0; 885 | }); 886 | particles.forEach(function (particle) { 887 | if (particle.age > MAX_PARTICLE_AGE) { 888 | field.randomize(particle).age = 0; 889 | } 890 | 891 | var x = particle.x; 892 | var y = particle.y; 893 | var v = field(x, y); // vector at current position 894 | 895 | var m = v[2]; 896 | 897 | if (m === null) { 898 | particle.age = MAX_PARTICLE_AGE; // particle has escaped the grid, never to return... 899 | } else { 900 | var xt = x + v[0]; 901 | var yt = y + v[1]; 902 | 903 | if (field(xt, yt)[2] !== null) { 904 | // Path from (x,y) to (xt,yt) is visible, so add this particle to the appropriate draw bucket. 905 | particle.xt = xt; 906 | particle.yt = yt; 907 | buckets[colorStyles.indexFor(m)].push(particle); 908 | } else { 909 | // Particle isn't visible, but it still moves through the field. 910 | particle.x = xt; 911 | particle.y = yt; 912 | } 913 | } 914 | 915 | particle.age += 1; 916 | }); 917 | } 918 | 919 | var g = params.canvas.getContext("2d"); 920 | g.lineWidth = PARTICLE_LINE_WIDTH; 921 | g.fillStyle = fadeFillStyle; 922 | g.globalAlpha = 0.6; 923 | 924 | function draw() { 925 | // Fade existing particle trails. 926 | var prev = "lighter"; 927 | g.globalCompositeOperation = "destination-in"; 928 | g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); 929 | g.globalCompositeOperation = prev; 930 | g.globalAlpha = OPACITY === 0 ? 0 : OPACITY * 0.9; // Draw new particle trails. 931 | 932 | buckets.forEach(function (bucket, i) { 933 | if (bucket.length > 0) { 934 | g.beginPath(); 935 | g.strokeStyle = colorStyles[i]; 936 | bucket.forEach(function (particle) { 937 | g.moveTo(particle.x, particle.y); 938 | g.lineTo(particle.xt, particle.yt); 939 | particle.x = particle.xt; 940 | particle.y = particle.yt; 941 | }); 942 | g.stroke(); 943 | } 944 | }); 945 | } 946 | 947 | var then = Date.now(); 948 | 949 | (function frame() { 950 | animationLoop = requestAnimationFrame(frame); 951 | var now = Date.now(); 952 | var delta = now - then; 953 | 954 | if (delta > FRAME_TIME) { 955 | then = now - delta % FRAME_TIME; 956 | evolve(); 957 | draw(); 958 | } 959 | })(); 960 | }; 961 | 962 | var start = function start(bounds, width, height, extent) { 963 | var mapBounds = { 964 | south: deg2rad(extent[0][1]), 965 | north: deg2rad(extent[1][1]), 966 | east: deg2rad(extent[1][0]), 967 | west: deg2rad(extent[0][0]), 968 | width: width, 969 | height: height 970 | }; 971 | stop(); // build grid 972 | 973 | buildGrid(gridData, function (grid) { 974 | // interpolateField 975 | interpolateField(grid, buildBounds(bounds, width, height), mapBounds, function (bounds, field) { 976 | // animate the canvas with random points 977 | windy.field = field; 978 | animate(bounds, field); 979 | }); 980 | }); 981 | }; 982 | 983 | var stop = function stop() { 984 | if (windy.field) windy.field.release(); 985 | if (animationLoop) cancelAnimationFrame(animationLoop); 986 | }; 987 | 988 | var windy = { 989 | params: params, 990 | start: start, 991 | stop: stop, 992 | createField: createField, 993 | interpolatePoint: interpolate, 994 | setData: setData, 995 | setOptions: setOptions 996 | }; 997 | return windy; 998 | }; 999 | 1000 | if (!window.cancelAnimationFrame) { 1001 | window.cancelAnimationFrame = function (id) { 1002 | clearTimeout(id); 1003 | }; 1004 | } -------------------------------------------------------------------------------- /dist/leaflet-velocity.min.css: -------------------------------------------------------------------------------- 1 | .leaflet-control-velocity{background-color:hsla(0,0%,100%,.7);color:#333;font:11px/1.5 Helvetica Neue,Arial,Helvetica,sans-serif;margin:0!important;padding:0 5px}.velocity-overlay{position:absolute;z-index:1} -------------------------------------------------------------------------------- /dist/leaflet-velocity.min.js: -------------------------------------------------------------------------------- 1 | "use strict";L.DomUtil.setTransform||(L.DomUtil.setTransform=function(t,e,n){var i=e||new L.Point(0,0);t.style[L.DomUtil.TRANSFORM]=(L.Browser.ie3d?"translate("+i.x+"px,"+i.y+"px)":"translate3d("+i.x+"px,"+i.y+"px,0)")+(n?" scale("+n+")":"")}),L.CanvasLayer=(L.Layer?L.Layer:L.Class).extend({initialize:function(t){this._map=null,this._canvas=null,this._frame=null,this._delegate=null,L.setOptions(this,t)},delegate:function(t){return this._delegate=t,this},needRedraw:function(){return this._frame||(this._frame=L.Util.requestAnimFrame(this.drawLayer,this)),this},_onLayerDidResize:function(t){this._canvas.width=t.newSize.x,this._canvas.height=t.newSize.y},_onLayerDidMove:function(){var t=this._map.containerPointToLayerPoint([0,0]);L.DomUtil.setPosition(this._canvas,t),this.drawLayer()},getEvents:function(){var t={resize:this._onLayerDidResize,moveend:this._onLayerDidMove};return this._map.options.zoomAnimation&&L.Browser.any3d&&(t.zoomanim=this._animateZoom),t},onAdd:function(t){this._map=t,this._canvas=L.DomUtil.create("canvas","leaflet-layer"),this.tiles={};var e=this._map.getSize();this._canvas.width=e.x,this._canvas.height=e.y;var n=this._map.options.zoomAnimation&&L.Browser.any3d;L.DomUtil.addClass(this._canvas,"leaflet-zoom-"+(n?"animated":"hide")),this.options.pane.appendChild(this._canvas),t.on(this.getEvents(),this);var i=this._delegate||this;i.onLayerDidMount&&i.onLayerDidMount(),this.needRedraw();var o=this;setTimeout(function(){o._onLayerDidMove()},0)},onRemove:function(t){var e=this._delegate||this;e.onLayerWillUnmount&&e.onLayerWillUnmount(),this.options.pane.removeChild(this._canvas),t.off(this.getEvents(),this),this._canvas=null},addTo:function(t){return t.addLayer(this),this},drawLayer:function(){var t=this._map.getSize(),e=this._map.getBounds(),n=this._map.getZoom(),i=this._map.options.crs.project(this._map.getCenter()),o=this._map.options.crs.project(this._map.containerPointToLatLng(this._map.getSize())),a=this._delegate||this;a.onDrawLayer&&a.onDrawLayer({layer:this,canvas:this._canvas,bounds:e,size:t,zoom:n,center:i,corner:o}),this._frame=null},_setTransform:function(t,e,n){var i=e||new L.Point(0,0);t.style[L.DomUtil.TRANSFORM]=(L.Browser.ie3d?"translate("+i.x+"px,"+i.y+"px)":"translate3d("+i.x+"px,"+i.y+"px,0)")+(n?" scale("+n+")":"")},_animateZoom:function(t){var e=this._map.getZoomScale(t.zoom),n=L.Layer?this._map._latLngToNewLayerPoint(this._map.getBounds().getNorthWest(),t.zoom,t.center):this._map._getCenterOffset(t.center)._multiplyBy(-e).subtract(this._map._getMapPanePos());L.DomUtil.setTransform(this._canvas,n,e)}}),L.canvasLayer=function(t){return new L.CanvasLayer(t)},L.Control.Velocity=L.Control.extend({options:{position:"bottomleft",emptyString:"Unavailable",angleConvention:"bearingCCW",showCardinal:!1,speedUnit:"m/s",directionString:"Direction",speedString:"Speed",onAdd:null,onRemove:null},onAdd:function(t){return this._container=L.DomUtil.create("div","leaflet-control-velocity"),L.DomEvent.disableClickPropagation(this._container),t.on("mousemove",this._onMouseMove,this),this._container.innerHTML=this.options.emptyString,this.options.leafletVelocity.options.onAdd&&this.options.leafletVelocity.options.onAdd(),this._container},onRemove:function(t){t.off("mousemove",this._onMouseMove,this),this.options.leafletVelocity.options.onRemove&&this.options.leafletVelocity.options.onRemove()},vectorToSpeed:function(t,e,n){var i=Math.sqrt(Math.pow(t,2)+Math.pow(e,2));return"k/h"===n?this.meterSec2kilometerHour(i):"kt"===n?this.meterSec2Knots(i):"mph"===n?this.meterSec2milesHour(i):i},vectorToDegrees:function(t,e,n){n.endsWith("CCW")&&(e=0 ").concat(o.toFixed(2),"°").concat(a,", ").concat(this.options.velocityType," ").concat(this.options.speedString,": ").concat(this.vectorToSpeed(n[0],n[1],this.options.speedUnit).toFixed(2)," ").concat(this.options.speedUnit)}else i=this.options.emptyString;this._container.innerHTML=i}}),L.Map.mergeOptions({positionControl:!1}),L.Map.addInitHook(function(){this.options.positionControl&&(this.positionControl=new L.Control.MousePosition,this.addControl(this.positionControl))}),L.control.velocity=function(t){return new L.Control.Velocity(t)},L.VelocityLayer=(L.Layer?L.Layer:L.Class).extend({options:{displayValues:!0,displayOptions:{velocityType:"Velocity",position:"bottomleft",emptyString:"No velocity data"},maxVelocity:10,colorScale:null,data:null},_map:null,_canvasLayer:null,_windy:null,_context:null,_timer:0,_mouseControl:null,initialize:function(t){L.setOptions(this,t)},onAdd:function(t){this._paneName=this.options.paneName||"overlayPane";var e=t._panes.overlayPane;t.getPane&&(e=(e=t.getPane(this._paneName))||t.createPane(this._paneName)),this._canvasLayer=L.canvasLayer({pane:e}).delegate(this),this._canvasLayer.addTo(t),this._map=t},onRemove:function(t){this._destroyWind()},setData:function(t){this.options.data=t,this._windy&&(this._windy.setData(t),this._clearAndRestart()),this.fire("load")},setOpacity:function(t){this._canvasLayer.setOpacity(t)},setOptions:function(t){this.options=Object.assign(this.options,t),t.hasOwnProperty("displayOptions")&&(this.options.displayOptions=Object.assign(this.options.displayOptions,t.displayOptions),this._initMouseHandler(!0)),t.hasOwnProperty("data")&&(this.options.data=t.data),this._windy&&(this._windy.setOptions(t),t.hasOwnProperty("data")&&this._windy.setData(t.data),this._clearAndRestart()),this.fire("load")},onDrawLayer:function(t,e){var n=this;this._windy?this.options.data&&(this._timer&&clearTimeout(n._timer),this._timer=setTimeout(function(){n._startWindy()},750)):this._initWindy(this)},_startWindy:function(){var t=this._map.getBounds(),e=this._map.getSize();this._windy.start([[0,0],[e.x,e.y]],e.x,e.y,[[t._southWest.lng,t._southWest.lat],[t._northEast.lng,t._northEast.lat]])},_initWindy:function(t){var e=Object.assign({canvas:t._canvasLayer._canvas,map:this._map},t.options);this._windy=new Windy(e),this._context=this._canvasLayer._canvas.getContext("2d"),this._canvasLayer._canvas.classList.add("velocity-overlay"),this.onDrawLayer(),this._map.on("dragstart",t._windy.stop),this._map.on("dragend",t._clearAndRestart),this._map.on("zoomstart",t._windy.stop),this._map.on("zoomend",t._clearAndRestart),this._map.on("resize",t._clearWind),this._initMouseHandler(!1)},_initMouseHandler:function(t){if(t&&(this._map.removeControl(this._mouseControl),this._mouseControl=!1),!this._mouseControl&&this.options.displayValues){var e=this.options.displayOptions||{};(e.leafletVelocity=this)._mouseControl=L.control.velocity(e).addTo(this._map)}},_clearAndRestart:function(){this._context&&this._context.clearRect(0,0,3e3,3e3),this._windy&&this._startWindy()},_clearWind:function(){this._windy&&this._windy.stop(),this._context&&this._context.clearRect(0,0,3e3,3e3)},_destroyWind:function(){this._timer&&clearTimeout(this._timer),this._windy&&this._windy.stop(),this._context&&this._context.clearRect(0,0,3e3,3e3),this._mouseControl&&this._map.removeControl(this._mouseControl),this._mouseControl=null,this._windy=null,this._map.removeLayer(this._canvasLayer)}}),L.velocityLayer=function(t){return new L.VelocityLayer(t)};var Windy=function(S){function o(t,e,n,i,o,a){var r=1-t,s=1-e,l=r*s,h=t*s,c=r*e,d=t*e,p=n[0]*l+i[0]*h+o[0]*c+a[0]*d,u=n[1]*l+i[1]*h+o[1]*c+a[1]*d;return[p,u,Math.sqrt(p*p+u*u)]}function d(t){var e=null,n=null;return t.forEach(function(t){switch(t.header.parameterCategory+","+t.header.parameterNumber){case"1,2":case"2,2":e=t;break;case"1,3":case"2,3":n=t;break;default:t}}),function(t,e){var n=t.data,i=e.data;return{header:t.header,data:function(t){return[n[t],i[t]]},interpolate:o}}(e,n)}function a(i,o,t){function a(t,e){var n=i[Math.round(t)];return n&&n[Math.round(e)]||h}a.release=function(){i=[]},a.randomize=function(t){for(var e,n,i=0;null===a(e=Math.round(Math.floor(Math.random()*o.width)+o.x),n=Math.round(Math.floor(Math.random()*o.height)+o.y))[2]&&i++<30;);return t.x=e,t.y=n,t},t(o,a)}function r(t){return t/180*Math.PI}function s(i,s){var e,n,l=(e=x,n=C,R.indexFor=function(t){return Math.max(0,Math.min(R.length-1,Math.round((t-e)/(n-e)*(R.length-1))))},R),h=l.map(function(){return[]}),t=Math.round(i.width*i.height*D);/android|blackberry|iemobile|ipad|iphone|ipod|opera mini|webos/i.test(navigator.userAgent)&&(t*=T);for(var o="rgba(0, 0, 0, ".concat(O,")"),a=[],r=0;rP&&(s.randomize(t).age=0);var e=t.x,n=t.y,i=s(e,n),o=i[2];if(null===o)t.age=P;else{var a=e+i[0],r=n+i[1];null!==s(a,r)[2]?(t.xt=a,t.yt=r,h[l.indexFor(o)].push(t)):(t.x=a,t.y=r)}t.age+=1}),c.globalCompositeOperation="destination-in",c.fillRect(i.x,i.y,i.width,i.height),c.globalCompositeOperation="lighter",c.globalAlpha=0===O?0:.9*O,h.forEach(function(t,e){0 0 ? (vMs = -vMs) : Math.abs(vMs); 52 | } 53 | var velocityAbs = Math.sqrt(Math.pow(uMs, 2) + Math.pow(vMs, 2)); 54 | 55 | var velocityDir = Math.atan2(uMs / velocityAbs, vMs / velocityAbs); 56 | var velocityDirToDegrees = (velocityDir * 180) / Math.PI + 180; 57 | 58 | if (angleConvention === "bearingCW" || angleConvention === "meteoCCW") { 59 | velocityDirToDegrees += 180; 60 | if (velocityDirToDegrees >= 360) velocityDirToDegrees -= 360; 61 | } 62 | 63 | return velocityDirToDegrees; 64 | }, 65 | 66 | degreesToCardinalDirection: function(deg) { 67 | 68 | let cardinalDirection = '' 69 | if (deg >= 0 && deg < 11.25 || deg >= 348.75) { 70 | cardinalDirection = 'N' 71 | } 72 | else if (deg >= 11.25 && deg < 33.75){ 73 | cardinalDirection = 'NNW' 74 | } 75 | else if (deg >= 33.75 && deg < 56.25){ 76 | cardinalDirection = 'NW' 77 | } 78 | else if (deg >= 56.25 && deg < 78.75){ 79 | cardinalDirection = 'WNW' 80 | } 81 | else if (deg >= 78.25 && deg < 101.25){ 82 | cardinalDirection = 'W' 83 | } 84 | else if (deg >= 101.25 && deg < 123.75){ 85 | cardinalDirection = 'WSW' 86 | } 87 | else if (deg >= 123.75 && deg < 146.25){ 88 | cardinalDirection = 'SW' 89 | } 90 | else if (deg >= 146.25 && deg < 168.75){ 91 | cardinalDirection = 'SSW' 92 | } 93 | else if (deg >= 168.75 && deg < 191.25){ 94 | cardinalDirection = 'S' 95 | } 96 | else if (deg >= 191.25 && deg < 213.75){ 97 | cardinalDirection = 'SSE' 98 | } 99 | else if (deg >= 213.75 && deg < 236.25){ 100 | cardinalDirection = 'SE' 101 | } 102 | else if (deg >= 236.25 && deg < 258.75){ 103 | cardinalDirection = 'ESE' 104 | } 105 | else if (deg >= 258.75 && deg < 281.25){ 106 | cardinalDirection = 'E' 107 | } 108 | else if (deg >= 281.25 && deg < 303.75){ 109 | cardinalDirection = 'ENE' 110 | } 111 | else if (deg >= 303.75 && deg < 326.25){ 112 | cardinalDirection = 'NE' 113 | } 114 | else if (deg >= 326.25 && deg < 348.75){ 115 | cardinalDirection = 'NNE' 116 | } 117 | 118 | return cardinalDirection; 119 | }, 120 | 121 | meterSec2Knots: function(meters) { 122 | return meters / 0.514; 123 | }, 124 | 125 | meterSec2kilometerHour: function(meters) { 126 | return meters * 3.6; 127 | }, 128 | 129 | meterSec2milesHour: function(meters) { 130 | return meters * 2.23694; 131 | }, 132 | 133 | _onMouseMove: function(e) { 134 | var self = this; 135 | var pos = this.options.leafletVelocity._map.containerPointToLatLng( 136 | L.point(e.containerPoint.x, e.containerPoint.y) 137 | ); 138 | var gridValue = this.options.leafletVelocity._windy.interpolatePoint( 139 | pos.lng, 140 | pos.lat 141 | ); 142 | var htmlOut = ""; 143 | 144 | if ( 145 | gridValue && 146 | !isNaN(gridValue[0]) && 147 | !isNaN(gridValue[1]) && 148 | gridValue[2] 149 | ) { 150 | var deg = self.vectorToDegrees(gridValue[0], gridValue[1], this.options.angleConvention); 151 | var cardinal = this.options.showCardinal ? ` (${self.degreesToCardinalDirection(deg)}) ` : ''; 152 | 153 | htmlOut = ` ${this.options.velocityType} ${ 154 | this.options.directionString 155 | }: ${deg.toFixed(2)}°${cardinal}, ${this.options.velocityType} ${ 156 | this.options.speedString 157 | }: ${self 158 | .vectorToSpeed(gridValue[0], gridValue[1], this.options.speedUnit) 159 | .toFixed(2)} ${this.options.speedUnit}`; 160 | } else { 161 | htmlOut = this.options.emptyString; 162 | } 163 | 164 | self._container.innerHTML = htmlOut; 165 | } 166 | }); 167 | 168 | L.Map.mergeOptions({ 169 | positionControl: false 170 | }); 171 | 172 | L.Map.addInitHook(function() { 173 | if (this.options.positionControl) { 174 | this.positionControl = new L.Control.MousePosition(); 175 | this.addControl(this.positionControl); 176 | } 177 | }); 178 | 179 | L.control.velocity = function(options) { 180 | return new L.Control.Velocity(options); 181 | }; 182 | -------------------------------------------------------------------------------- /src/js/L.VelocityLayer.js: -------------------------------------------------------------------------------- 1 | L.VelocityLayer = (L.Layer ? L.Layer : L.Class).extend({ 2 | options: { 3 | displayValues: true, 4 | displayOptions: { 5 | velocityType: "Velocity", 6 | position: "bottomleft", 7 | emptyString: "No velocity data" 8 | }, 9 | maxVelocity: 10, // used to align color scale 10 | colorScale: null, 11 | data: null 12 | }, 13 | 14 | _map: null, 15 | _canvasLayer: null, 16 | _windy: null, 17 | _context: null, 18 | _timer: 0, 19 | _mouseControl: null, 20 | 21 | initialize: function(options) { 22 | L.setOptions(this, options); 23 | }, 24 | 25 | onAdd: function(map) { 26 | // determine where to add the layer 27 | this._paneName = this.options.paneName || "overlayPane"; 28 | 29 | // fall back to overlayPane for leaflet < 1 30 | let pane = map._panes.overlayPane; 31 | if (map.getPane) { 32 | // attempt to get pane first to preserve parent (createPane voids this) 33 | pane = map.getPane(this._paneName); 34 | if (!pane) { 35 | pane = map.createPane(this._paneName); 36 | } 37 | } 38 | // create canvas, add to map pane 39 | this._canvasLayer = L.canvasLayer({ pane: pane }).delegate(this); 40 | this._canvasLayer.addTo(map); 41 | 42 | this._map = map; 43 | }, 44 | 45 | onRemove: function(map) { 46 | this._destroyWind(); 47 | }, 48 | 49 | setData: function(data) { 50 | this.options.data = data; 51 | if (this._windy) { 52 | this._windy.setData(data); 53 | this._clearAndRestart(); 54 | } 55 | this.fire("load"); 56 | }, 57 | 58 | setOpacity: function(opacity) { 59 | this._canvasLayer.setOpacity(opacity); 60 | }, 61 | 62 | setOptions: function(options) { 63 | this.options = Object.assign(this.options, options); 64 | if (options.hasOwnProperty("displayOptions")) { 65 | this.options.displayOptions = Object.assign( 66 | this.options.displayOptions, 67 | options.displayOptions 68 | ); 69 | this._initMouseHandler(true); 70 | } 71 | if (options.hasOwnProperty("data")) this.options.data = options.data; 72 | if (this._windy) { 73 | this._windy.setOptions(options); 74 | if (options.hasOwnProperty("data")) this._windy.setData(options.data); 75 | this._clearAndRestart(); 76 | } 77 | 78 | this.fire("load"); 79 | }, 80 | 81 | /*------------------------------------ PRIVATE ------------------------------------------*/ 82 | 83 | onDrawLayer: function(overlay, params) { 84 | var self = this; 85 | 86 | if (!this._windy) { 87 | this._initWindy(this); 88 | return; 89 | } 90 | 91 | if (!this.options.data) { 92 | return; 93 | } 94 | 95 | if (this._timer) clearTimeout(self._timer); 96 | 97 | this._timer = setTimeout(function() { 98 | self._startWindy(); 99 | }, 750); // showing velocity is delayed 100 | }, 101 | 102 | _startWindy: function() { 103 | var bounds = this._map.getBounds(); 104 | var size = this._map.getSize(); 105 | 106 | // bounds, width, height, extent 107 | this._windy.start( 108 | [ 109 | [0, 0], 110 | [size.x, size.y] 111 | ], 112 | size.x, 113 | size.y, 114 | [ 115 | [bounds._southWest.lng, bounds._southWest.lat], 116 | [bounds._northEast.lng, bounds._northEast.lat] 117 | ] 118 | ); 119 | }, 120 | 121 | _initWindy: function(self) { 122 | // windy object, copy options 123 | const options = Object.assign( 124 | { canvas: self._canvasLayer._canvas, map: this._map }, 125 | self.options 126 | ); 127 | this._windy = new Windy(options); 128 | 129 | // prepare context global var, start drawing 130 | this._context = this._canvasLayer._canvas.getContext("2d"); 131 | this._canvasLayer._canvas.classList.add("velocity-overlay"); 132 | this.onDrawLayer(); 133 | 134 | this._map.on("dragstart", self._windy.stop); 135 | this._map.on("dragend", self._clearAndRestart); 136 | this._map.on("zoomstart", self._windy.stop); 137 | this._map.on("zoomend", self._clearAndRestart); 138 | this._map.on("resize", self._clearWind); 139 | 140 | this._initMouseHandler(false); 141 | }, 142 | 143 | _initMouseHandler: function(voidPrevious) { 144 | if (voidPrevious) { 145 | this._map.removeControl(this._mouseControl); 146 | this._mouseControl = false; 147 | } 148 | if (!this._mouseControl && this.options.displayValues) { 149 | var options = this.options.displayOptions || {}; 150 | options["leafletVelocity"] = this; 151 | this._mouseControl = L.control.velocity(options).addTo(this._map); 152 | } 153 | }, 154 | 155 | _clearAndRestart: function() { 156 | if (this._context) this._context.clearRect(0, 0, 3000, 3000); 157 | if (this._windy) this._startWindy(); 158 | }, 159 | 160 | _clearWind: function() { 161 | if (this._windy) this._windy.stop(); 162 | if (this._context) this._context.clearRect(0, 0, 3000, 3000); 163 | }, 164 | 165 | _destroyWind: function() { 166 | if (this._timer) clearTimeout(this._timer); 167 | if (this._windy) this._windy.stop(); 168 | if (this._context) this._context.clearRect(0, 0, 3000, 3000); 169 | if (this._mouseControl) this._map.removeControl(this._mouseControl); 170 | this._mouseControl = null; 171 | this._windy = null; 172 | this._map.removeLayer(this._canvasLayer); 173 | } 174 | }); 175 | 176 | L.velocityLayer = function(options) { 177 | return new L.VelocityLayer(options); 178 | }; 179 | -------------------------------------------------------------------------------- /src/js/windy.js: -------------------------------------------------------------------------------- 1 | /* Global class for simulating the movement of particle through a 1km wind grid 2 | 3 | credit: All the credit for this work goes to: https://github.com/cambecc for creating the repo: 4 | https://github.com/cambecc/earth. The majority of this code is directly take nfrom there, since its awesome. 5 | 6 | This class takes a canvas element and an array of data (1km GFS from http://www.emc.ncep.noaa.gov/index.php?branch=GFS) 7 | and then uses a mercator (forward/reverse) projection to correctly map wind vectors in "map space". 8 | 9 | The "start" method takes the bounds of the map at its current extent and starts the whole gridding, 10 | interpolation and animation process. 11 | */ 12 | 13 | var Windy = function(params) { 14 | var MIN_VELOCITY_INTENSITY = params.minVelocity || 0; // velocity at which particle intensity is minimum (m/s) 15 | var MAX_VELOCITY_INTENSITY = params.maxVelocity || 10; // velocity at which particle intensity is maximum (m/s) 16 | var VELOCITY_SCALE = 17 | (params.velocityScale || 0.005) * 18 | (Math.pow(window.devicePixelRatio, 1 / 3) || 1); // scale for wind velocity (completely arbitrary--this value looks nice) 19 | var MAX_PARTICLE_AGE = params.particleAge || 90; // max number of frames a particle is drawn before regeneration 20 | var PARTICLE_LINE_WIDTH = params.lineWidth || 1; // line width of a drawn particle 21 | var PARTICLE_MULTIPLIER = params.particleMultiplier || 1 / 300; // particle count scalar (completely arbitrary--this values looks nice) 22 | var PARTICLE_REDUCTION = Math.pow(window.devicePixelRatio, 1 / 3) || 1.6; // multiply particle count for mobiles by this amount 23 | var FRAME_RATE = params.frameRate || 15; 24 | var FRAME_TIME = 1000 / FRAME_RATE; // desired frames per second 25 | var OPACITY = 0.97; 26 | 27 | var defaulColorScale = [ 28 | "rgb(36,104, 180)", 29 | "rgb(60,157, 194)", 30 | "rgb(128,205,193 )", 31 | "rgb(151,218,168 )", 32 | "rgb(198,231,181)", 33 | "rgb(238,247,217)", 34 | "rgb(255,238,159)", 35 | "rgb(252,217,125)", 36 | "rgb(255,182,100)", 37 | "rgb(252,150,75)", 38 | "rgb(250,112,52)", 39 | "rgb(245,64,32)", 40 | "rgb(237,45,28)", 41 | "rgb(220,24,32)", 42 | "rgb(180,0,35)" 43 | ]; 44 | 45 | const colorScale = params.colorScale || defaulColorScale; 46 | 47 | var NULL_WIND_VECTOR = [NaN, NaN, null]; // singleton for no wind in the form: [u, v, magnitude] 48 | 49 | var builder; 50 | var grid; 51 | var gridData = params.data; 52 | var date; 53 | var λ0, φ0, Δλ, Δφ, ni, nj; 54 | 55 | var setData = function(data) { 56 | gridData = data; 57 | }; 58 | 59 | var setOptions = function(options) { 60 | if (options.hasOwnProperty("minVelocity")) 61 | MIN_VELOCITY_INTENSITY = options.minVelocity; 62 | 63 | if (options.hasOwnProperty("maxVelocity")) 64 | MAX_VELOCITY_INTENSITY = options.maxVelocity; 65 | 66 | if (options.hasOwnProperty("velocityScale")) 67 | VELOCITY_SCALE = 68 | (options.velocityScale || 0.005) * 69 | (Math.pow(window.devicePixelRatio, 1 / 3) || 1); 70 | 71 | if (options.hasOwnProperty("particleAge")) 72 | MAX_PARTICLE_AGE = options.particleAge; 73 | 74 | if (options.hasOwnProperty("lineWidth")) 75 | PARTICLE_LINE_WIDTH = options.lineWidth; 76 | 77 | if (options.hasOwnProperty("particleMultiplier")) 78 | PARTICLE_MULTIPLIER = options.particleMultiplier; 79 | 80 | if (options.hasOwnProperty("opacity")) OPACITY = +options.opacity; 81 | 82 | if (options.hasOwnProperty("frameRate")) FRAME_RATE = options.frameRate; 83 | FRAME_TIME = 1000 / FRAME_RATE; 84 | }; 85 | 86 | // interpolation for vectors like wind (u,v,m) 87 | var bilinearInterpolateVector = function(x, y, g00, g10, g01, g11) { 88 | var rx = 1 - x; 89 | var ry = 1 - y; 90 | var a = rx * ry, 91 | b = x * ry, 92 | c = rx * y, 93 | d = x * y; 94 | var u = g00[0] * a + g10[0] * b + g01[0] * c + g11[0] * d; 95 | var v = g00[1] * a + g10[1] * b + g01[1] * c + g11[1] * d; 96 | return [u, v, Math.sqrt(u * u + v * v)]; 97 | }; 98 | 99 | var createWindBuilder = function(uComp, vComp) { 100 | var uData = uComp.data, 101 | vData = vComp.data; 102 | return { 103 | header: uComp.header, 104 | //recipe: recipeFor("wind-" + uComp.header.surface1Value), 105 | data: function(i) { 106 | return [uData[i], vData[i]]; 107 | }, 108 | interpolate: bilinearInterpolateVector 109 | }; 110 | }; 111 | 112 | var createBuilder = function(data) { 113 | var uComp = null, 114 | vComp = null, 115 | scalar = null; 116 | 117 | data.forEach(function(record) { 118 | switch ( 119 | record.header.parameterCategory + 120 | "," + 121 | record.header.parameterNumber 122 | ) { 123 | case "1,2": 124 | case "2,2": 125 | uComp = record; 126 | break; 127 | case "1,3": 128 | case "2,3": 129 | vComp = record; 130 | break; 131 | default: 132 | scalar = record; 133 | } 134 | }); 135 | 136 | return createWindBuilder(uComp, vComp); 137 | }; 138 | 139 | var buildGrid = function(data, callback) { 140 | var supported = true; 141 | 142 | if (data.length < 2 ) supported = false; 143 | if (!supported) console.log("Windy Error: data must have at least two components (u,v)"); 144 | 145 | builder = createBuilder(data); 146 | var header = builder.header; 147 | 148 | if (header.hasOwnProperty("gridDefinitionTemplate") && header.gridDefinitionTemplate != 0 ) supported = false; 149 | if (!supported) { 150 | console.log("Windy Error: Only data with Latitude_Longitude coordinates is supported"); 151 | } 152 | supported = true; // reset for futher checks 153 | 154 | λ0 = header.lo1; 155 | φ0 = header.la1; // the grid's origin (e.g., 0.0E, 90.0N) 156 | 157 | Δλ = header.dx; 158 | Δφ = header.dy; // distance between grid points (e.g., 2.5 deg lon, 2.5 deg lat) 159 | 160 | ni = header.nx; 161 | nj = header.ny; // number of grid points W-E and N-S (e.g., 144 x 73) 162 | 163 | if (header.hasOwnProperty("scanMode")) { 164 | var scanModeMask = header.scanMode.toString(2) 165 | scanModeMask = ('0'+scanModeMask).slice(-8); 166 | var scanModeMaskArray = scanModeMask.split('').map(Number).map(Boolean); 167 | 168 | if (scanModeMaskArray[0]) Δλ =-Δλ; 169 | if (scanModeMaskArray[1]) Δφ = -Δφ; 170 | if (scanModeMaskArray[2]) supported = false; 171 | if (scanModeMaskArray[3]) supported = false; 172 | if (scanModeMaskArray[4]) supported = false; 173 | if (scanModeMaskArray[5]) supported = false; 174 | if (scanModeMaskArray[6]) supported = false; 175 | if (scanModeMaskArray[7]) supported = false; 176 | if (!supported) console.log("Windy Error: Data with scanMode: "+header.scanMode+ " is not supported."); 177 | } 178 | date = new Date(header.refTime); 179 | date.setHours(date.getHours() + header.forecastTime); 180 | 181 | // Scan modes 0, 64 allowed. 182 | // http://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_table3-4.shtml 183 | grid = []; 184 | var p = 0; 185 | var isContinuous = Math.floor(ni * Δλ) >= 360; 186 | 187 | for (var j = 0; j < nj; j++) { 188 | var row = []; 189 | for (var i = 0; i < ni; i++, p++) { 190 | row[i] = builder.data(p); 191 | } 192 | if (isContinuous) { 193 | // For wrapped grids, duplicate first column as last column to simplify interpolation logic 194 | row.push(row[0]); 195 | } 196 | grid[j] = row; 197 | } 198 | 199 | callback({ 200 | date: date, 201 | interpolate: interpolate 202 | }); 203 | }; 204 | 205 | /** 206 | * Get interpolated grid value from Lon/Lat position 207 | * @param λ {Float} Longitude 208 | * @param φ {Float} Latitude 209 | * @returns {Object} 210 | */ 211 | var interpolate = function(λ, φ) { 212 | if (!grid) return null; 213 | 214 | var i = floorMod(λ - λ0, 360) / Δλ; // calculate longitude index in wrapped range [0, 360) 215 | var j = (φ0 - φ) / Δφ; // calculate latitude index in direction +90 to -90 216 | 217 | var fi = Math.floor(i), 218 | ci = fi + 1; 219 | var fj = Math.floor(j), 220 | cj = fj + 1; 221 | 222 | var row; 223 | if ((row = grid[fj])) { 224 | var g00 = row[fi]; 225 | var g10 = row[ci]; 226 | if (isValue(g00) && isValue(g10) && (row = grid[cj])) { 227 | var g01 = row[fi]; 228 | var g11 = row[ci]; 229 | if (isValue(g01) && isValue(g11)) { 230 | // All four points found, so interpolate the value. 231 | return builder.interpolate(i - fi, j - fj, g00, g10, g01, g11); 232 | } 233 | } 234 | } 235 | return null; 236 | }; 237 | 238 | /** 239 | * @returns {Boolean} true if the specified value is not null and not undefined. 240 | */ 241 | var isValue = function(x) { 242 | return x !== null && x !== undefined; 243 | }; 244 | 245 | /** 246 | * @returns {Number} returns remainder of floored division, i.e., floor(a / n). Useful for consistent modulo 247 | * of negative numbers. See http://en.wikipedia.org/wiki/Modulo_operation. 248 | */ 249 | var floorMod = function(a, n) { 250 | return a - n * Math.floor(a / n); 251 | }; 252 | 253 | /** 254 | * @returns {Number} the value x clamped to the range [low, high]. 255 | */ 256 | var clamp = function(x, range) { 257 | return Math.max(range[0], Math.min(x, range[1])); 258 | }; 259 | 260 | /** 261 | * @returns {Boolean} true if agent is probably a mobile device. Don't really care if this is accurate. 262 | */ 263 | var isMobile = function() { 264 | return /android|blackberry|iemobile|ipad|iphone|ipod|opera mini|webos/i.test( 265 | navigator.userAgent 266 | ); 267 | }; 268 | 269 | /** 270 | * Calculate distortion of the wind vector caused by the shape of the projection at point (x, y). The wind 271 | * vector is modified in place and returned by this function. 272 | */ 273 | var distort = function(projection, λ, φ, x, y, scale, wind) { 274 | var u = wind[0] * scale; 275 | var v = wind[1] * scale; 276 | var d = distortion(projection, λ, φ, x, y); 277 | 278 | // Scale distortion vectors by u and v, then add. 279 | wind[0] = d[0] * u + d[2] * v; 280 | wind[1] = d[1] * u + d[3] * v; 281 | return wind; 282 | }; 283 | 284 | var distortion = function(projection, λ, φ, x, y) { 285 | var τ = 2 * Math.PI; 286 | // var H = Math.pow(10, -5.2); // 0.00000630957344480193 287 | // var H = 0.0000360; // 0.0000360°φ ~= 4m (from https://github.com/cambecc/earth/blob/master/public/libs/earth/1.0.0/micro.js#L13) 288 | var H = 5; // ToDo: Why does this work? 289 | var hλ = λ < 0 ? H : -H; 290 | var hφ = φ < 0 ? H : -H; 291 | 292 | var pλ = project(φ, λ + hλ); 293 | var pφ = project(φ + hφ, λ); 294 | 295 | // Meridian scale factor (see Snyder, equation 4-3), where R = 1. This handles issue where length of 1º λ 296 | // changes depending on φ. Without this, there is a pinching effect at the poles. 297 | var k = Math.cos((φ / 360) * τ); 298 | return [ 299 | (pλ[0] - x) / hλ / k, 300 | (pλ[1] - y) / hλ / k, 301 | (pφ[0] - x) / hφ, 302 | (pφ[1] - y) / hφ 303 | ]; 304 | }; 305 | 306 | var createField = function(columns, bounds, callback) { 307 | /** 308 | * @returns {Array} wind vector [u, v, magnitude] at the point (x, y), or [NaN, NaN, null] if wind 309 | * is undefined at that point. 310 | */ 311 | function field(x, y) { 312 | var column = columns[Math.round(x)]; 313 | return (column && column[Math.round(y)]) || NULL_WIND_VECTOR; 314 | } 315 | 316 | // Frees the massive "columns" array for GC. Without this, the array is leaked (in Chrome) each time a new 317 | // field is interpolated because the field closure's context is leaked, for reasons that defy explanation. 318 | field.release = function() { 319 | columns = []; 320 | }; 321 | 322 | field.randomize = function(o) { 323 | // UNDONE: this method is terrible 324 | var x, y; 325 | var safetyNet = 0; 326 | do { 327 | x = Math.round(Math.floor(Math.random() * bounds.width) + bounds.x); 328 | y = Math.round(Math.floor(Math.random() * bounds.height) + bounds.y); 329 | } while (field(x, y)[2] === null && safetyNet++ < 30); 330 | o.x = x; 331 | o.y = y; 332 | return o; 333 | }; 334 | 335 | callback(bounds, field); 336 | }; 337 | 338 | var buildBounds = function(bounds, width, height) { 339 | var upperLeft = bounds[0]; 340 | var lowerRight = bounds[1]; 341 | var x = Math.round(upperLeft[0]); //Math.max(Math.floor(upperLeft[0], 0), 0); 342 | var y = Math.max(Math.floor(upperLeft[1], 0), 0); 343 | var xMax = Math.min(Math.ceil(lowerRight[0], width), width - 1); 344 | var yMax = Math.min(Math.ceil(lowerRight[1], height), height - 1); 345 | return { 346 | x: x, 347 | y: y, 348 | xMax: width, 349 | yMax: yMax, 350 | width: width, 351 | height: height 352 | }; 353 | }; 354 | 355 | var deg2rad = function(deg) { 356 | return (deg / 180) * Math.PI; 357 | }; 358 | 359 | var invert = function(x, y, windy) { 360 | var latlon = params.map.containerPointToLatLng(L.point(x, y)); 361 | return [latlon.lng, latlon.lat]; 362 | }; 363 | 364 | var project = function(lat, lon, windy) { 365 | var xy = params.map.latLngToContainerPoint(L.latLng(lat, lon)); 366 | return [xy.x, xy.y]; 367 | }; 368 | 369 | var interpolateField = function(grid, bounds, extent, callback) { 370 | var projection = {}; // map.crs used instead 371 | var mapArea = (extent.south - extent.north) * (extent.west - extent.east); 372 | var velocityScale = VELOCITY_SCALE * Math.pow(mapArea, 0.4); 373 | 374 | var columns = []; 375 | var x = bounds.x; 376 | 377 | function interpolateColumn(x) { 378 | var column = []; 379 | for (var y = bounds.y; y <= bounds.yMax; y += 2) { 380 | var coord = invert(x, y); 381 | if (coord) { 382 | var λ = coord[0], 383 | φ = coord[1]; 384 | if (isFinite(λ)) { 385 | var wind = grid.interpolate(λ, φ); 386 | if (wind) { 387 | wind = distort(projection, λ, φ, x, y, velocityScale, wind); 388 | column[y + 1] = column[y] = wind; 389 | } 390 | } 391 | } 392 | } 393 | columns[x + 1] = columns[x] = column; 394 | } 395 | 396 | (function batchInterpolate() { 397 | var start = Date.now(); 398 | while (x < bounds.width) { 399 | interpolateColumn(x); 400 | x += 2; 401 | if (Date.now() - start > 1000) { 402 | //MAX_TASK_TIME) { 403 | setTimeout(batchInterpolate, 25); 404 | return; 405 | } 406 | } 407 | createField(columns, bounds, callback); 408 | })(); 409 | }; 410 | 411 | var animationLoop; 412 | var animate = function(bounds, field) { 413 | function windIntensityColorScale(min, max) { 414 | colorScale.indexFor = function(m) { 415 | // map velocity speed to a style 416 | return Math.max( 417 | 0, 418 | Math.min( 419 | colorScale.length - 1, 420 | Math.round(((m - min) / (max - min)) * (colorScale.length - 1)) 421 | ) 422 | ); 423 | }; 424 | 425 | return colorScale; 426 | } 427 | 428 | var colorStyles = windIntensityColorScale( 429 | MIN_VELOCITY_INTENSITY, 430 | MAX_VELOCITY_INTENSITY 431 | ); 432 | var buckets = colorStyles.map(function() { 433 | return []; 434 | }); 435 | 436 | var particleCount = Math.round( 437 | bounds.width * bounds.height * PARTICLE_MULTIPLIER 438 | ); 439 | if (isMobile()) { 440 | particleCount *= PARTICLE_REDUCTION; 441 | } 442 | 443 | var fadeFillStyle = `rgba(0, 0, 0, ${OPACITY})`; 444 | 445 | var particles = []; 446 | for (var i = 0; i < particleCount; i++) { 447 | particles.push( 448 | field.randomize({ 449 | age: Math.floor(Math.random() * MAX_PARTICLE_AGE) + 0 450 | }) 451 | ); 452 | } 453 | 454 | function evolve() { 455 | buckets.forEach(function(bucket) { 456 | bucket.length = 0; 457 | }); 458 | particles.forEach(function(particle) { 459 | if (particle.age > MAX_PARTICLE_AGE) { 460 | field.randomize(particle).age = 0; 461 | } 462 | var x = particle.x; 463 | var y = particle.y; 464 | var v = field(x, y); // vector at current position 465 | var m = v[2]; 466 | if (m === null) { 467 | particle.age = MAX_PARTICLE_AGE; // particle has escaped the grid, never to return... 468 | } else { 469 | var xt = x + v[0]; 470 | var yt = y + v[1]; 471 | if (field(xt, yt)[2] !== null) { 472 | // Path from (x,y) to (xt,yt) is visible, so add this particle to the appropriate draw bucket. 473 | particle.xt = xt; 474 | particle.yt = yt; 475 | buckets[colorStyles.indexFor(m)].push(particle); 476 | } else { 477 | // Particle isn't visible, but it still moves through the field. 478 | particle.x = xt; 479 | particle.y = yt; 480 | } 481 | } 482 | particle.age += 1; 483 | }); 484 | } 485 | 486 | var g = params.canvas.getContext("2d"); 487 | g.lineWidth = PARTICLE_LINE_WIDTH; 488 | g.fillStyle = fadeFillStyle; 489 | g.globalAlpha = 0.6; 490 | 491 | function draw() { 492 | // Fade existing particle trails. 493 | var prev = "lighter"; 494 | g.globalCompositeOperation = "destination-in"; 495 | g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); 496 | g.globalCompositeOperation = prev; 497 | g.globalAlpha = OPACITY === 0 ? 0 : OPACITY * 0.9; 498 | 499 | // Draw new particle trails. 500 | buckets.forEach(function(bucket, i) { 501 | if (bucket.length > 0) { 502 | g.beginPath(); 503 | g.strokeStyle = colorStyles[i]; 504 | bucket.forEach(function(particle) { 505 | g.moveTo(particle.x, particle.y); 506 | g.lineTo(particle.xt, particle.yt); 507 | particle.x = particle.xt; 508 | particle.y = particle.yt; 509 | }); 510 | g.stroke(); 511 | } 512 | }); 513 | } 514 | 515 | var then = Date.now(); 516 | (function frame() { 517 | animationLoop = requestAnimationFrame(frame); 518 | var now = Date.now(); 519 | var delta = now - then; 520 | if (delta > FRAME_TIME) { 521 | then = now - (delta % FRAME_TIME); 522 | evolve(); 523 | draw(); 524 | } 525 | })(); 526 | }; 527 | 528 | var start = function(bounds, width, height, extent) { 529 | var mapBounds = { 530 | south: deg2rad(extent[0][1]), 531 | north: deg2rad(extent[1][1]), 532 | east: deg2rad(extent[1][0]), 533 | west: deg2rad(extent[0][0]), 534 | width: width, 535 | height: height 536 | }; 537 | 538 | stop(); 539 | 540 | // build grid 541 | buildGrid(gridData, function(grid) { 542 | // interpolateField 543 | interpolateField( 544 | grid, 545 | buildBounds(bounds, width, height), 546 | mapBounds, 547 | function(bounds, field) { 548 | // animate the canvas with random points 549 | windy.field = field; 550 | animate(bounds, field); 551 | } 552 | ); 553 | }); 554 | }; 555 | 556 | var stop = function() { 557 | if (windy.field) windy.field.release(); 558 | if (animationLoop) cancelAnimationFrame(animationLoop); 559 | }; 560 | 561 | var windy = { 562 | params: params, 563 | start: start, 564 | stop: stop, 565 | createField: createField, 566 | interpolatePoint: interpolate, 567 | setData: setData, 568 | setOptions: setOptions 569 | }; 570 | 571 | return windy; 572 | }; 573 | 574 | if (!window.cancelAnimationFrame) { 575 | window.cancelAnimationFrame = function(id) { 576 | clearTimeout(id); 577 | }; 578 | } 579 | --------------------------------------------------------------------------------