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 | [](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 |
--------------------------------------------------------------------------------