├── aviation ├── aviation-tracker-screenshot.png ├── index.html ├── main.css ├── libs │ └── Leaflet.Marker.Parallax.js └── script.js ├── esri-leaflet └── dynamic-chart-screenshot.png ├── lerc-landcover ├── lerc-landcover-screenshot.png ├── index.html └── script.js ├── night-and-day ├── night-and-day-screenshot.png ├── main.css ├── index.html └── script.js ├── .eslintrc.json ├── LICENSE.md └── README.md /aviation/aviation-tracker-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwasilgeo/leaflet-experiments/HEAD/aviation/aviation-tracker-screenshot.png -------------------------------------------------------------------------------- /esri-leaflet/dynamic-chart-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwasilgeo/leaflet-experiments/HEAD/esri-leaflet/dynamic-chart-screenshot.png -------------------------------------------------------------------------------- /lerc-landcover/lerc-landcover-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwasilgeo/leaflet-experiments/HEAD/lerc-landcover/lerc-landcover-screenshot.png -------------------------------------------------------------------------------- /night-and-day/night-and-day-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwasilgeo/leaflet-experiments/HEAD/night-and-day/night-and-day-screenshot.png -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": false, 5 | "es6": true, 6 | "node": false 7 | }, 8 | "rules": { 9 | "no-const-assign": "warn", 10 | "no-this-before-super": "warn", 11 | "no-undef": "warn", 12 | "no-unreachable": "warn", 13 | "no-unused-vars": "warn", 14 | "constructor-super": "warn", 15 | "valid-typeof": "warn", 16 | // "indent":"off" 17 | "no-console": "warn", 18 | "semi": "warn" 19 | }, 20 | "globals": { 21 | "L": false, 22 | "turf": false, 23 | "SunCalc": false, 24 | "spacetime": false, 25 | "spacetimeGeo": false, 26 | "$": false, 27 | "Lerc": false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jacob Wasilkowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lerc-landcover/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Lerc Imagery Land Cover Layer 10 | 11 | 12 | 13 | 36 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /night-and-day/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | #map { 7 | position: absolute; 8 | top: 0; 9 | bottom: 0; 10 | right: 0; 11 | left: 0; 12 | background-color: black; 13 | } 14 | 15 | .crosshairs { 16 | position: relative; 17 | width: 100%; 18 | height: 100%; 19 | z-index: 1000; 20 | display: -webkit-box; 21 | display: -ms-flexbox; 22 | display: -webkit-flex; 23 | display: flex; 24 | -webkit-justify-content: center; 25 | justify-content: center; 26 | -webkit-align-items: center; 27 | align-items: center; 28 | font-size: 5em; 29 | font-weight: bold; 30 | line-height: 0; 31 | color: white; 32 | text-shadow: 2px 2px 2px black, 2px 2px 2px black, 2px 2px 2px black; 33 | } 34 | 35 | #solarInfo { 36 | position: absolute; 37 | top: 10px; 38 | width: 100%; 39 | z-index: 2000; 40 | font-family: Courier New, Courier, monospace; 41 | text-align: center; 42 | color: white; 43 | text-shadow: 2px 2px 2px black, 2px 2px 2px black, 2px 2px 2px black; 44 | font-size: 1.3em; 45 | font-weight: bold; 46 | } 47 | 48 | #solarInfo h1 { 49 | margin: 0 0 0.15em; 50 | font-size: 1.6em; 51 | font-weight: normal; 52 | } 53 | 54 | .author-credit { 55 | font-size: 1.4em; 56 | font-weight: bold; 57 | } 58 | 59 | .author-credit a { 60 | color: #FF005E !important; 61 | } 62 | -------------------------------------------------------------------------------- /night-and-day/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Night and Day 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |
+
21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /aviation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Aviation Tracker 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |

27 | 28 | 29 | Searching for aircraft around the world 30 | 31 | 32 |

33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /aviation/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body, div, input { 7 | font-family: 'Lato', sans-serif; 8 | } 9 | 10 | #map { 11 | position: absolute; 12 | top: 0; 13 | bottom: 0; 14 | right: 0; 15 | left: 0; 16 | } 17 | 18 | .info { 19 | position: absolute; 20 | bottom: 35px; 21 | right: 0; 22 | margin: 0 10px; 23 | z-index: 400; 24 | min-width: 185px; 25 | font-size: 1.1em; 26 | text-align: center; 27 | user-select: none; 28 | cursor: default; 29 | padding: 0 10px; 30 | background-color: rgba(255, 255, 255, 0.9) 31 | } 32 | 33 | .info p { 34 | margin: 10px 0; 35 | } 36 | 37 | .instructions { 38 | font-style: italic; 39 | } 40 | 41 | .esri-truncated-attribution:hover { 42 | white-space: normal; 43 | background-color: whitesmoke; 44 | } 45 | 46 | .shadow { 47 | color: rgba(0, 0, 0, 0); 48 | text-shadow: 0 0 5px rgba(31, 16, 96, 0.2); 49 | } 50 | 51 | .radar { 52 | position: absolute; 53 | bottom: calc(50% - 75px); 54 | left: calc(50% - 75px); 55 | z-index: 1000; 56 | width: 0; 57 | height: 0; 58 | border-radius: 50%; 59 | border: 75px solid rgba(0, 0, 0, 0); 60 | border-top-color: deepskyblue; 61 | animation: radar-rotate 1.5s ease-in-out infinite; 62 | } 63 | 64 | @keyframes radar-rotate { 65 | 0% { 66 | opacity: 0.8; 67 | } 68 | 50% { 69 | opacity: 0.3; 70 | } 71 | 100% { 72 | transform: rotate(360deg); 73 | opacity: 0.8; 74 | } 75 | } 76 | 77 | .off { 78 | display: none; 79 | } 80 | 81 | .leaflet-touch .geocoder-control.geocoder-control-expanded { 82 | width: 220px; 83 | } 84 | 85 | .geocoder-control-suggestions .geocoder-control-suggestion.geocoder-control-selected, 86 | .geocoder-control-suggestions .geocoder-control-suggestion:hover { 87 | background: deepskyblue; 88 | border-color: deepskyblue 89 | } 90 | 91 | .custom-credit { 92 | font-size: 1.4em; 93 | font-weight: bold; 94 | } 95 | 96 | .author-credit { 97 | color: #FF005E !important; 98 | text-decoration: underline !important; 99 | } 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # leaflet-experiments 2 | 3 | Web mapping experiments using [LeafletJS](https://leafletjs.com/). 4 | 5 | Also check out my [Esri experiments](https://github.com/jwasilgeo/esri-experiments) and other data visualizations on [CodePen](https://codepen.io/jwasilgeo/). 6 | 7 | ## [Aviation Tracker](https://jwasilgeo.github.io/leaflet-experiments/aviation/) 8 | 9 | ### Read more about it at [**FlowingData: Aviation tracker with depth**](https://flowingdata.com/2017/09/15/aviation-tracker-with-depth/) 10 | 11 | [![screenshot of Aviation Tracker](https://raw.githubusercontent.com/jwasilgeo/leaflet-experiments/master/aviation/aviation-tracker-screenshot.png)](https://jwasilgeo.github.io/leaflet-experiments/aviation/) 12 | 13 | - Aircraft tracking data provided by **The OpenSky Network, ** 14 | 15 | - [Leaflet.ParallaxMarker (jwasilgeo fork)](https://github.com/jwasilgeo/Leaflet.ParallaxMarker): parallax layer originating from [@dagjomar's](https://github.com/dagjomar) awesome [Leaflet.ParallaxMarker](https://github.com/dagjomar/Leaflet.ParallaxMarker) 16 | 17 | - [Leaflet.Terminator](https://github.com/joergdietrich/Leaflet.Terminator): solar terminator layer 18 | 19 | - [Esri Leaflet](https://esri.github.io/esri-leaflet/): Esri gray basemap layer and geocoder widget 20 | 21 | - [Font Awesome](https://fontawesome.com/): "plane" icon used for the parallax layer symbol 22 | 23 | ## [Night and Day](https://jwasilgeo.github.io/leaflet-experiments/night-and-day/) 24 | 25 | [![screenshot of Night and Day](https://raw.githubusercontent.com/jwasilgeo/leaflet-experiments/master/night-and-day/night-and-day-screenshot.png)](https://jwasilgeo.github.io/leaflet-experiments/night-and-day/) 26 | 27 | - The Suomi-NPP VIIRS "Earth at Night" layer is clipped by using [Leaflet.Terminator](https://github.com/joergdietrich/Leaflet.Terminator) in combination with [TileLayer.BoundaryCanvas](https://github.com/aparshin/leaflet-boundary-canvas). 28 | 29 | - [SunCalc](https://github.com/mourner/suncalc), [Turf.js](http://turfjs.org/), [spacetime](https://spencermountain.github.io/spacetime/), and [spacetime-geo](https://spencermountain.github.io/spacetime/) are used for reporting the local time and sun positions. 30 | 31 | ## [Lerc Imagery Land Cover Layer](https://jwasilgeo.github.io/leaflet-experiments/lerc-landcover/) 32 | 33 | [![screenshot of Lerc Imagery Land Cover Layer](https://raw.githubusercontent.com/jwasilgeo/leaflet-experiments/master/lerc-landcover/lerc-landcover-screenshot.png)](https://jwasilgeo.github.io/leaflet-experiments/lerc-landcover/) 34 | 35 | - Demonstration of rendering a [2020 global land cover map](https://www.arcgis.com/home/item.html?id=d6642f8a4f6d4685a24ae2dc0c73d4ac) (produced by Impact Observatory for Esri) hosted as an ArcGIS ImageServer in tiled LERC format as 8bit color-coded pixels. 36 | 37 | - **Inspired heavily by [@jgravois's](https://github.com/jgravois)** earlier research on tying together LERC decoding within a LeafletJS `GridLayer`. 38 | 39 | - LERC imagery tile data are read with Esri's [Limited Error Raster Compression](https://github.com/Esri/lerc/) JavaScript decoder. 40 | 41 | - Check out this [Esri Community thread](https://community.esri.com/t5/esri-leaflet-questions/how-to-diagnose-error-rendering-esri-2020-land/m-p/1079790) and [GitHub `esri-leaflet` discussion](https://github.com/Esri/esri-leaflet/issues/726) for more info. 42 | 43 | ## [Esri-Leaflet and Chart.js](https://developers.arcgis.com/esri-leaflet/samples/dynamic-chart/) 44 | 45 | 46 | 47 | > Plot feature attributes on a dynamic chart that updates as users pan and zoom, and respond to chart interactions by modifying feature layer contents. 48 | 49 | [![screenshot of Esri-Leaflet and Chart.js](https://raw.githubusercontent.com/jwasilgeo/leaflet-experiments/master/esri-leaflet/dynamic-chart-screenshot.png)](https://developers.arcgis.com/esri-leaflet/samples/dynamic-chart/) 50 | 51 | ## Licensing 52 | 53 | A copy of the license is available in the repository's [LICENSE](./LICENSE.md) file. 54 | -------------------------------------------------------------------------------- /night-and-day/script.js: -------------------------------------------------------------------------------- 1 | var solarInfoNode = document.querySelector('#solarInfo'); 2 | 3 | var daylightLayer = L.esri.basemapLayer('Imagery'); 4 | 5 | spacetime.extend(spacetimeGeo); 6 | 7 | var terminator = L.terminator(); 8 | 9 | var nighttimeLayer = generateNighttimeLayer(terminator); 10 | 11 | var map = L.map('map', { 12 | center: [27.5, 90.5], 13 | zoom: 2, 14 | minZoom: 1, 15 | maxZoom: 10, 16 | worldCopyJump: true, 17 | layers: [ 18 | daylightLayer, 19 | nighttimeLayer 20 | ] 21 | }) 22 | .on('layeradd', updateSolarInfo) 23 | .on('move', updateSolarInfo); 24 | 25 | map.zoomControl.setPosition('bottomleft'); 26 | 27 | map.attributionControl.setPrefix( 28 | '@JWasilGeo | ' + 29 | map.attributionControl.options.prefix 30 | ); 31 | 32 | // top-most labels tile layer in a custom map pane 33 | map.createPane('labels'); 34 | map.getPane('labels').style.pointerEvents = 'none'; 35 | L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_only_labels/{z}/{x}/{y}.png', { 36 | attribution: '© OpenStreetMap, © CARTO', 37 | subdomains: ['a', 'b', 'c', 'd'], 38 | pane: 'labels' 39 | }).addTo(map); 40 | 41 | // update the terminator and nighttime layer every 10 seconds 42 | setInterval(function() { 43 | terminator = updateTerminator(terminator); 44 | nighttimeLayer = updateNighttimeLayer(terminator, nighttimeLayer); 45 | }, 10000); 46 | 47 | function updateTerminator(terminator) { 48 | var newTerminator = L.terminator(); 49 | terminator.setLatLngs(newTerminator.getLatLngs()); 50 | terminator.redraw(); 51 | return terminator; 52 | } 53 | 54 | function updateNighttimeLayer(terminator, previousNighttimeLayer) { 55 | var nextNighttimeLayer = generateNighttimeLayer(terminator).addTo(map); 56 | // sorta funky, but visually effective way to remove the previous nighttime layer 57 | setTimeout(function() { 58 | previousNighttimeLayer.remove(); 59 | }, 5000); 60 | return nextNighttimeLayer; 61 | } 62 | 63 | function generateNighttimeLayer(terminator) { 64 | return L.TileLayer.boundaryCanvas('https://gibs-{s}.earthdata.nasa.gov/wmts/epsg3857/best/VIIRS_Black_Marble/default/2016-01-01/GoogleMapsCompatible_Level8/{z}/{y}/{x}.png', { 65 | attribution: 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.', 66 | boundary: terminator.toGeoJSON(), 67 | minNativeZoom: 1, 68 | maxNativeZoom: 8 69 | }); 70 | } 71 | 72 | function updateSolarInfo() { 73 | var latLngCoordinates = map.getCenter().wrap(); 74 | 75 | var date = Date.now(); 76 | 77 | // calculate sun times for the given date 78 | // we're interested in "nadir" and "solarNoon" 79 | var sunTimes = SunCalc.getTimes(date, latLngCoordinates.lat, latLngCoordinates.lng); 80 | 81 | // create a spacetime moment for the given date but at the map's center point location 82 | var d = spacetime(date) 83 | .in({ 84 | lat: latLngCoordinates.lat, 85 | lon: latLngCoordinates.lng 86 | }); 87 | 88 | var currentLocalTime = [ 89 | d.time(), 90 | 'in', 91 | d.timezone().name, 92 | ].join(' '); 93 | 94 | // find out if the map's center point location falls in day or night 95 | // by checking for the point being contained in the terminator polygon 96 | var isNight = turf.booleanContains( 97 | L.terminator().toGeoJSON(), 98 | turf.point([latLngCoordinates.lng, latLngCoordinates.lat]) 99 | ); 100 | 101 | // update the html display text 102 | if (isNight) { 103 | solarInfoNode.innerHTML = [ 104 | '

Night and Day

', 105 | currentLocalTime, 106 | '
Night is darkest at ', 107 | spacetime(sunTimes.nadir).goto(d.timezone().name).time(), 108 | '
' 109 | ].join(''); 110 | } else { 111 | solarInfoNode.innerHTML = [ 112 | '

Day and Night

', 113 | currentLocalTime, 114 | '
Sun is highest at ', 115 | spacetime(sunTimes.solarNoon).goto(d.timezone().name).time(), 116 | '
' 117 | ].join(''); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /aviation/libs/Leaflet.Marker.Parallax.js: -------------------------------------------------------------------------------- 1 | L.Marker.Parallax = L.Marker.extend({ 2 | _initIcon: function() { 3 | L.Marker.prototype._initIcon.call(this); 4 | var anchor = this.options.icon.options.iconAnchor ? L.point(this.options.icon.options.iconAnchor) : L.point([0, 0]); 5 | this.options.icon._originalOffset = L.point(-anchor.x, -anchor.y); 6 | }, 7 | 8 | onAdd: function(e) { 9 | L.Marker.prototype.onAdd.call(this, e); 10 | this._map.on('move', this._onMapMove, this); 11 | if (this._map.options.zoomAnimation && L.Browser.any3d) { 12 | this._map.on('zoomanim', this._animateZoom, this); 13 | } 14 | this._onMapMove(); 15 | }, 16 | 17 | onRemove: function(e) { 18 | this._map.off('move', this._onMapMove, this); 19 | if (this._map.options.zoomAnimation) { 20 | this._map.off('zoomanim', this._animateZoom, this); 21 | } 22 | L.Marker.prototype.onRemove.call(this, e); 23 | }, 24 | 25 | _onMapMove: function() { 26 | var offsets = this._calculateOffsetFromOrigin(this._map.getCenter()); 27 | 28 | if (this._icon) { 29 | this._updateIconOffset(offsets.centerOffset); 30 | } 31 | }, 32 | 33 | _animateZoom: function(e) { 34 | L.Marker.prototype._animateZoom.call(this, e); 35 | 36 | // calculate the "future" offset and parallax based on the 37 | // _animateZoom's info on the map's next center and zoom 38 | var offset = this._calculateOffsetFromOrigin(e.center); 39 | var parallax = this._calculateParallaxFromOffset(e.zoom, offset); 40 | 41 | this._icon.style.marginLeft = parallax.x + 'px'; 42 | this._icon.style.marginTop = parallax.y + 'px'; 43 | }, 44 | 45 | _calcLatLngFromOffset: function() { 46 | var offsets = this._calculateOffsetFromOrigin(this._map.getCenter()); 47 | var parallax = this._calculateParallaxFromOffset(this._map.getZoom(), offsets.centerOffset); 48 | 49 | var containerPoint = offsets.containerPoint.add(parallax); 50 | var markerLatLng = this._map.containerPointToLatLng(containerPoint); 51 | 52 | // console.log('@ containerPoint: ', containerPoint); 53 | // console.log('@ got markerLatLng', markerLatLng); 54 | 55 | return markerLatLng; 56 | }, 57 | 58 | _updateIconOffset: function(offset) { 59 | if (!offset || !this._icon) { return; } 60 | 61 | var parallax = this._calculateParallaxFromOffset(this._map.getZoom(), offset); 62 | var originalOffset = this.options.icon._originalOffset; 63 | 64 | var newOffset = originalOffset.add(parallax); 65 | 66 | this._icon.style.marginLeft = newOffset.x + 'px'; 67 | this._icon.style.marginTop = newOffset.y + 'px'; 68 | }, 69 | 70 | //Find how much from the center of the map the marker is currently located 71 | _calculateOffsetFromOrigin: function(center) { 72 | if (!this._map) { return; } 73 | 74 | var latlng = this.getLatLng(); 75 | var markerPoint = this._map.latLngToContainerPoint(latlng); 76 | // var centerPoint = this._map.getSize().divideBy(2); 77 | var centerPoint = this._map.latLngToContainerPoint(center); 78 | //User centerPoint and markerPoint to calculate the distance from center 79 | 80 | var deltaX = (markerPoint.x - centerPoint.x); 81 | var deltaY = (markerPoint.y - centerPoint.y); 82 | 83 | var offset = { x: deltaX, y: deltaY }; 84 | var containerPoint = markerPoint.add(offset); 85 | 86 | return { containerPoint: containerPoint, centerOffset: offset }; 87 | // targetPoint = centerPoint.subtract([overlayWidth, 0]), 88 | // targetLatLng = map.containerPointToLatLng(centerPoint); 89 | }, 90 | 91 | _calculateParallaxFromOffset: function(zoom, offset) { 92 | var parallax = L.point([0, 0]); 93 | 94 | if (!this.options.parallaxZoffset) { 95 | return parallax; 96 | } 97 | 98 | //Multiplies the delta x with a factor depending on the map z. 99 | var constFactor = this.options.parallaxZoffset * 0.000001; 100 | var moveFactor = constFactor * Math.pow(2, zoom); 101 | 102 | parallax.x = offset.x * moveFactor; 103 | parallax.y = offset.y * moveFactor; 104 | 105 | return parallax; 106 | } 107 | }); 108 | 109 | L.Marker.parallax = function(latlng, opts) { return new L.Marker.Parallax(latlng, opts); }; 110 | -------------------------------------------------------------------------------- /lerc-landcover/script.js: -------------------------------------------------------------------------------- 1 | // INSPIRED HEAVILY BY https://github.com/jgravois/lerc-leaflet 2 | 3 | // create a custom layer type extending from the LeafletJS GridLayer 4 | const Lerc8bitColorLayer = L.GridLayer.extend({ 5 | createTile: function (coords, done) { 6 | let tileError; 7 | let tile = L.DomUtil.create("canvas", "leaflet-tile"); 8 | tile.width = this.options.tileSize; 9 | tile.height = this.options.tileSize; 10 | 11 | const tileUrl = `${this.options.url}/tile/${coords.z}/${coords.y}/${coords.x}`; 12 | 13 | fetch(tileUrl, { method: "GET" }) 14 | .then((response) => response.arrayBuffer()) 15 | .then((arrayBuffer) => { 16 | try { 17 | // decode the response's arrayBuffer (Lerc global comes from an imported script) 18 | tile.decodedPixels = Lerc.decode(arrayBuffer); 19 | 20 | // display newly decoded pixel data as canvas context image data 21 | this.draw.call(this, tile); 22 | } catch (error) { 23 | console.error(error); 24 | // displaying error text in the canvas tile is for debugging/demo purposes 25 | // we could instead call `this.draw.call(this, tile);` to bring less visual attention to any errors 26 | this.drawError(tile); 27 | } 28 | done(tileError, tile); 29 | }) 30 | .catch((error) => { 31 | console.error(error); 32 | // displaying error text in the canvas tile is for debugging/demo purposes 33 | // we could instead call `this.draw.call(this, tile);` to bring less visual attention to any errors 34 | this.drawError(tile); 35 | done(tileError, tile); 36 | }); 37 | 38 | return tile; 39 | }, 40 | 41 | draw: function (tile) { 42 | const width = tile.decodedPixels.width; 43 | const height = tile.decodedPixels.height; 44 | const pixels = tile.decodedPixels.pixels[0]; // get pixels from the first band (only 1 band when 8bit RGB) 45 | const mask = tile.decodedPixels.maskData; 46 | const rasterAttributeTableFeatures = 47 | this.options.rasterAttributeTable.features; 48 | 49 | // write new canvas context image data by working with the decoded pixel array and mask array 50 | const ctx = tile.getContext("2d"); // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D 51 | const imageData = ctx.createImageData(width, height); 52 | const data = imageData.data; 53 | 54 | for (let i = 0; i < width * height; i++) { 55 | // look up RGB colormap attributes in the raster attribute table for the decoded pixel value 56 | const pixelValue = pixels[i]; 57 | const attributes = rasterAttributeTableFeatures.find( 58 | (info) => info.attributes.Value === pixelValue 59 | ).attributes; 60 | 61 | // set RGB values in the pixel array 62 | data[i * 4] = attributes.Red; 63 | data[i * 4 + 1] = attributes.Green; 64 | data[i * 4 + 2] = attributes.Blue; 65 | 66 | // make the pixel transparent when either missing data exists for the decoded mask value 67 | // or for this particular ImageServer when the ClassName raster attribute is "No Data" 68 | if ((mask && !mask[i]) || attributes.ClassName === "No Data") { 69 | data[i * 4 + 3] = 0; 70 | } else { 71 | data[i * 4 + 3] = 255; 72 | } 73 | } 74 | 75 | ctx.putImageData(imageData, 0, 0); 76 | }, 77 | 78 | drawError: function (tile) { 79 | const width = tile.width; 80 | const height = tile.height; 81 | const ctx = tile.getContext("2d"); 82 | ctx.font = "italic 12px sans-serif"; 83 | ctx.fillStyle = "darkred"; 84 | ctx.textAlign = "center"; 85 | ctx.textBaseline = "middle"; 86 | ctx.fillText( 87 | "Error decoding data or tile may not exist here.", 88 | width / 2, 89 | height / 2, 90 | width - 10 91 | ); 92 | }, 93 | }); 94 | 95 | // create a LeafletJS map in WKID 4326 96 | const map = L.map("map", { 97 | crs: L.CRS.EPSG4326, 98 | maxZoom: 13, 99 | }).setView([35, 73], 3); 100 | 101 | map.attributionControl.setPrefix( 102 | '@JWasilGeo | ' + 103 | '2020 global land cover map (produced by Impact Observatory for Esri) | ' + 104 | 'Learn more at https://github.com/jwasilgeo/leaflet-experiments | ' + 105 | map.attributionControl.options.prefix 106 | ); 107 | 108 | const landcoverImageServerUrl = 109 | "https://tiledimageservices.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/Esri_2020_Land_Cover_V2/ImageServer"; 110 | 111 | // before creating an instance of the layer and adding it to the map, first get the raster attribute table 112 | // from the ImageServer because we need to assign RGB colors to land cover pixel categories 113 | fetch(`${landcoverImageServerUrl}/rasterattributetable?f=json`, { 114 | method: "GET", 115 | }) 116 | .then((response) => response.json()) 117 | .then((rasterAttributeTable) => { 118 | // create an instance of the custom "Lerc8bitColorLayer" defined above 119 | const landcoverLayer = new Lerc8bitColorLayer({ 120 | url: landcoverImageServerUrl, 121 | rasterAttributeTable: rasterAttributeTable, 122 | tileSize: 256, 123 | }); 124 | 125 | // and finally add it to the map 126 | landcoverLayer.addTo(map); 127 | }) 128 | .catch((error) => { 129 | console.error("Error loading ImageServer raster attribute table", error); 130 | }); 131 | -------------------------------------------------------------------------------- /aviation/script.js: -------------------------------------------------------------------------------- 1 | var aircraftNode = document.querySelector('#aircraft'), 2 | aircraftSummaryNode = document.querySelector('#aircraftSummary'), 3 | aggregateSummaryStatsHTML, 4 | localSummaryStatsHTML, 5 | radarNode = document.querySelector('.radar'), 6 | currentAjax = null, 7 | currentAircraftMarkers = { 8 | parallax: [], 9 | shadow: [] 10 | }; 11 | 12 | var terminator = L.terminator({ 13 | stroke: false, 14 | fillOpacity: 0.4, 15 | interactive: false 16 | }); 17 | 18 | // aircraft marker group layers 19 | var aircraftParallaxGroupLayer = L.featureGroup() 20 | .on('click mouseover', function(e) { 21 | aircraftParallaxGroupLayer.eachLayer(function(layer) { 22 | if (map.hasLayer(layer)) { 23 | layer.getElement().style.color = ''; 24 | } 25 | }); 26 | 27 | e.layer.getElement().style.color = 'deepskyblue'; 28 | 29 | aircraftNode.innerHTML = [ 30 | '

', 31 | e.layer._aircraft[1] + ' ' + e.layer._aircraft[2], 32 | '

', 33 | e.layer._aircraft[13] || '---', 34 | ' ft


' 35 | ].join(''); 36 | }); 37 | 38 | var aircraftShadowGroupLayer = L.featureGroup(); 39 | 40 | var worldwideAircraftGroupLayer = L.featureGroup(); 41 | 42 | var oldZoom = null; 43 | 44 | var map = L.map('map', { 45 | center: [0, 0], 46 | zoom: 2, 47 | minZoom: 1, 48 | maxBounds: [ 49 | [89, -250], 50 | [-89, 250] 51 | ], 52 | worldCopyJump: true, 53 | layers: [ 54 | L.esri.basemapLayer('Gray'), 55 | L.esri.basemapLayer('GrayLabels'), 56 | terminator, 57 | worldwideAircraftGroupLayer 58 | ], 59 | preferCanvas: true 60 | }) 61 | .on('zoomstart', function() { 62 | oldZoom = map.getZoom(); 63 | }) 64 | .on('zoom', function() { 65 | var newZoom = map.getZoom(); 66 | toggleWorldwideLayer(oldZoom, newZoom); 67 | updateParallaxZOffset(oldZoom, newZoom); 68 | }) 69 | .on('moveend', function() { 70 | wrapMarkers(worldwideAircraftGroupLayer); 71 | filterParallaxAircraftAtCurrentMapBounds(); 72 | }); 73 | 74 | map.attributionControl.setPrefix( 75 | '@JWasilGeo | ' + 76 | 'Aircraft data provided by The OpenSky Network https://www.opensky-network.org | ' + 77 | map.attributionControl.options.prefix 78 | ); 79 | 80 | L.esri.Geocoding.geosearch({ 81 | placeholder: 'Search for an airport', 82 | title: 'Airport location search', 83 | position: 'topright', 84 | expanded: true, 85 | collapseAfterResult: false, 86 | useMapBounds: false, 87 | zoomToResult: false, 88 | providers: [ 89 | L.esri.Geocoding.arcgisOnlineProvider({ 90 | categories: 'Airport' 91 | }) 92 | ] 93 | }) 94 | .on('results', function(data) { 95 | if (data.results.length) { 96 | map.fitBounds(data.results[0].bounds.pad(5)); 97 | } 98 | }) 99 | .addTo(map); 100 | 101 | // initially display aircraft reporting their location around the world 102 | generateAircraftWorldwide(); 103 | 104 | function toggleWorldwideLayer(oldZoom, newZoom) { 105 | var thresholdZoom = 7; 106 | if (oldZoom < newZoom && newZoom >= thresholdZoom) { 107 | // zooming in and past a threshold 108 | // - hide worldwide layer 109 | // - show aircraft related layers 110 | if (map.hasLayer(worldwideAircraftGroupLayer)) { 111 | worldwideAircraftGroupLayer.remove(); 112 | } 113 | 114 | if (!map.hasLayer(aircraftParallaxGroupLayer)) { 115 | aircraftParallaxGroupLayer.addTo(map); 116 | aircraftShadowGroupLayer.addTo(map); 117 | } 118 | 119 | aircraftNode.innerHTML = '

Interact with aircraft to learn more.


'; 120 | 121 | aircraftSummaryNode.innerHTML = localSummaryStatsHTML || aggregateSummaryStatsHTML; 122 | } else if (oldZoom > newZoom && newZoom <= thresholdZoom) { 123 | // zooming out and past a threshold 124 | // - show worldwide layer 125 | // - hide aircraft related layers 126 | if (!map.hasLayer(worldwideAircraftGroupLayer)) { 127 | worldwideAircraftGroupLayer.addTo(map); 128 | } 129 | 130 | if (map.hasLayer(aircraftParallaxGroupLayer)) { 131 | aircraftParallaxGroupLayer.remove(); 132 | aircraftShadowGroupLayer.remove(); 133 | L.DomUtil.empty(aircraftNode); 134 | } 135 | 136 | aircraftSummaryNode.innerHTML = aggregateSummaryStatsHTML; 137 | } 138 | } 139 | 140 | function updateParallaxZOffset(oldZoom, newZoom) { 141 | var thresholdZoom = 10; 142 | if (oldZoom < newZoom && newZoom >= thresholdZoom) { 143 | // zooming in and past a threshold: 144 | // - when the map's current zoom level is going to be greater than or equal to 10 145 | // use a smaller parallaxZoffset (aircraft altitude divided by 90) 146 | aircraftParallaxGroupLayer.eachLayer(function(layer) { 147 | layer.options.parallaxZoffset = layer._aircraft[13] / 90; 148 | }); 149 | } else if (oldZoom > newZoom && newZoom <= thresholdZoom) { 150 | // zooming out and past a threshold: 151 | // - when the map's current zoom level is going to be less than 10 152 | // revert to the original parallaxZoffset (aircraft altitude divided by 10) 153 | aircraftParallaxGroupLayer.eachLayer(function(layer) { 154 | layer.options.parallaxZoffset = layer._aircraft[13] / 10; 155 | }); 156 | } 157 | } 158 | 159 | function generateAircraftWorldwide() { 160 | radarNode.classList.remove('off'); 161 | 162 | // remove all the previous aircraft from the map 163 | worldwideAircraftGroupLayer.clearLayers(); 164 | aircraftParallaxGroupLayer.clearLayers(); 165 | aircraftShadowGroupLayer.clearLayers(); 166 | 167 | if (currentAjax) { 168 | currentAjax.abort('stopped early'); 169 | currentAjax = null; 170 | } 171 | 172 | currentAjax = $.ajax({ 173 | url: 'https://opensky-network.org/api/states/all', 174 | dataType: 'json' 175 | }) 176 | .done(function(response) { 177 | if (currentAjax) { 178 | currentAjax = null; 179 | } 180 | 181 | // TODO: repeat with interval? 182 | // setTimeout(generateAircraftWorldwide, 10000); 183 | 184 | radarNode.classList.add('off'); 185 | 186 | updateTerminator(terminator); 187 | 188 | if (!response.states) { 189 | return; 190 | } 191 | 192 | var aircraftList = response.states 193 | .filter(function(aircraft) { 194 | // ignore aircraft that are reporting themselves to be on the ground [8] 195 | // or are missing important attributes such as an altitude [13], longitude [5], and latitude [6] 196 | if ( 197 | !aircraft[8] && 198 | aircraft[13] && 199 | (aircraft[5] && aircraft[6]) 200 | ) { 201 | // convert meters to feet 202 | aircraft[13] = Math.round(aircraft[13] * 3.28084); 203 | 204 | return aircraft; 205 | } 206 | }) 207 | .sort(function(aircraftA, aircraftB) { 208 | // sort ascending by altitude 209 | return aircraftA[13] - aircraftB[13]; 210 | }); 211 | 212 | aggregateSummaryStatsHTML = [ 213 | '

', 214 | aircraftList.length, 215 | ' aircraft around the world are currently reporting their position.

', 216 | '

Zoom in or search for an airport.

' 217 | ].join(''); 218 | 219 | aircraftSummaryNode.innerHTML = aggregateSummaryStatsHTML; 220 | 221 | aircraftList.forEach(function(aircraft) { 222 | var simpleCircleMarker = L.circleMarker([aircraft[6], aircraft[5]], { 223 | radius: 2, // pixels, 224 | interactive: false, 225 | stroke: false, 226 | fillOpacity: 0.3, 227 | fillColor: 'deepskyblue' 228 | }); 229 | 230 | // use Font Awesome's "fa-plane" icon 231 | // https://fontawesome.com/icons/plane?style=solid 232 | 233 | // when zoomed in, show the aircraft in the sky using the parallax plugin 234 | var parallaxMarker = L.Marker.parallax( 235 | { 236 | lat: aircraft[6], 237 | lng: aircraft[5] 238 | }, { 239 | parallaxZoffset: aircraft[13] / 10, // use the altitude for the parallax z-offset value 240 | icon: L.divIcon({ 241 | className: 'leaflet-marker-icon leaflet-zoom-animated leaflet-interactive', 242 | html: '' 243 | }) 244 | } 245 | ); 246 | 247 | // hold onto the aircraft info for later usage 248 | parallaxMarker._aircraft = aircraft; 249 | 250 | // also when zoomed in, show the "shadow" of the aircraft at its reported coordinates on the ground 251 | var shadowMarker = L.marker( 252 | { 253 | lat: aircraft[6], 254 | lng: aircraft[5] 255 | }, { 256 | icon: L.divIcon({ 257 | className: 'leaflet-marker-icon leaflet-zoom-animated', 258 | html: '' 259 | }), 260 | interactive: false, 261 | pane: 'shadowPane' 262 | } 263 | ); 264 | 265 | worldwideAircraftGroupLayer.addLayer(simpleCircleMarker); 266 | 267 | currentAircraftMarkers.parallax.push(parallaxMarker); 268 | currentAircraftMarkers.shadow.push(shadowMarker); 269 | }); 270 | 271 | filterParallaxAircraftAtCurrentMapBounds(); 272 | }) 273 | .fail(function(error) { 274 | if (currentAjax) { 275 | currentAjax = null; 276 | } 277 | 278 | // TODO: repeat with interval? 279 | // setTimeout(generateAircraftWorldwide, 10000); 280 | 281 | if (error.statusText === 'stopped early') { 282 | return; 283 | } 284 | 285 | radarNode.classList.add('off'); 286 | console.error(error); 287 | }); 288 | } 289 | 290 | function filterParallaxAircraftAtCurrentMapBounds() { 291 | if (map.hasLayer(worldwideAircraftGroupLayer)) { 292 | return; 293 | } 294 | 295 | aircraftParallaxGroupLayer.clearLayers(); 296 | aircraftShadowGroupLayer.clearLayers(); 297 | 298 | var mapBounds = map.getBounds(); 299 | 300 | currentAircraftMarkers.parallax.forEach(function(parallaxMarker, index) { 301 | if (mapBounds.contains(parallaxMarker.getLatLng())) { 302 | aircraftParallaxGroupLayer.addLayer(parallaxMarker); 303 | aircraftShadowGroupLayer.addLayer(currentAircraftMarkers.shadow[index]); 304 | } 305 | }); 306 | 307 | var aircraftCount = aircraftParallaxGroupLayer.getLayers().length; 308 | 309 | var highestAltitude = aircraftParallaxGroupLayer.getLayers() 310 | .map(function(layer) { 311 | return layer._aircraft[13]; 312 | }) 313 | .reduce(function(previousValue, currentValue) { 314 | return Math.max(previousValue, currentValue); 315 | }, 0); 316 | 317 | aircraftNode.innerHTML = '

Interact with aircraft to learn more.


'; 318 | 319 | if (!aircraftCount) { 320 | localSummaryStatsHTML = [ 321 | '

No aircraft here.

' 322 | ].join(''); 323 | } else if (aircraftCount === 1) { 324 | localSummaryStatsHTML = [ 325 | '

The 1 aircraft here is flying at ', 326 | highestAltitude, 327 | ' ft.

' 328 | ].join(''); 329 | } else { 330 | localSummaryStatsHTML = [ 331 | '

Of the ', 332 | aircraftCount, 333 | ' aircraft here, the highest is at ', 334 | highestAltitude, 335 | ' ft.

' 336 | ].join(''); 337 | } 338 | 339 | aircraftSummaryNode.innerHTML = localSummaryStatsHTML; 340 | } 341 | 342 | function updateTerminator(terminator) { 343 | var newTerminator = L.terminator(); 344 | terminator.setLatLngs(newTerminator.getLatLngs()); 345 | terminator.redraw(); 346 | return terminator; 347 | } 348 | 349 | function wrapMarkers(groupLayer) { 350 | // ensure that the point features will be drawn beyond +/-180 longitude 351 | groupLayer.eachLayer(function(layer) { 352 | var wrappedLatLng = wrapAroundLatLng(layer.getLatLng()); 353 | layer.setLatLng(wrappedLatLng); 354 | }); 355 | } 356 | 357 | function wrapAroundLatLng(latLng) { 358 | var wrappedLatLng = latLng.clone(); 359 | var mapCenterLng = map.getCenter().lng; 360 | var wrapAroundDiff = mapCenterLng - wrappedLatLng.lng; 361 | if (wrapAroundDiff < -180 || wrapAroundDiff > 180) { 362 | wrappedLatLng.lng += (Math.round(wrapAroundDiff / 360) * 360); 363 | } 364 | return wrappedLatLng; 365 | } 366 | --------------------------------------------------------------------------------