TimeDimension with Leaflet and Python backend - javascript

I am working on showing congestion through a freeway system.
Having difficulties so added a Bounty.
I have created an example I am working through with JS bin here ( https://jsbin.com/lebeqam/edit?html,js,output )
I have created a leaflet map with the detectors lat long and id numbers.
I also have a csv with occupancy data, a important traffic value, over time for each detector.
I am wondering how I should go about creating a heatmap with this data that shows on the map. I would like to be able to change the time and even play the time forward or backward to get an understanding of congestion and how to stop it at it's root.
This is html and jscript for the page currently with some parts removed
<div id="mapid" style="height:1858px; border-right: 1px solid #d7d7d7; position: fixed; top: 0px;width: 67%;z-index: 0;cursor: -webkit-grab;cursor: -moz-grab;background: #fff;
color: #404040;color: rgba(0,0,0,.75);outline: 0;overflow: hidden;-ms-touch-action: none;
box-sizing: border-box;"></div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
<script src="https://use.fontawesome.com/releases/v5.0.8/js/all.js"></script>
</body>
<script>
var myIcon = L.divIcon({
html: '<i class="fas fa-map-pin"></i>',
iconSize: [20, 20],
className: 'dummy' // We don't want to use the default class
});
var mymap = L.map('mapid').setView([-37.735018, 144.894947], 13);
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
maxZoom: 18,
attribution: 'Map data © OpenStreetMap contributors, ' +
'CC-BY-SA, ' +
'Imagery © Mapbox',
id: 'mapbox.streets'
}).addTo(mymap);
L.geoJSON(myGeojsonData, {
pointToLayer: function (getJsonPoint, latlng) {
return L.marker(latlng, { icon: myIcon });
}
}).bindPopup(function(layer) {
return 'ID #: ' + layer.feature.properties.IDnumber + '<br />Area: ' + layer.feature.properties.Area;
}).addTo(mymap);
var circle = L.circle([-37.735018, 144.894947], {
color: 'red',
fillColor: '#f03',
fillOpacity: 0.5,
radius: 50
}).addTo(mymap);
</script>
This is part of the geoJson (the entire file is huge but you'll get the picture)
var myGeojsonData =
{
"features": [
{
"geometry": {
"coordinates": [
144.829434,
-37.825233
],
"type": "Point"
},
"properties": {
"Area": "Combined Entry MVT on Grieve Pde, West Gate Fwy North Ramps, Grieve Pde Byp Start EB between Grieve ",
"IDnumber": "2541EL_P0"
},
"type": "Feature"
},...etc
And this is the CSV with the traffic data (also only a part of it for the sake of space.)
I have tried to simplify this a bit by using these two json files to just get going (the time series file is in the jsbin as it's too large for stackoverflow.
var myGeojsonData =
{
"features": [
{
"geometry": {
"coordinates": [
144.827465,
-37.82572
],
"type": "Point"
},
"properties": {
"Area": "Freeway MVT on West Gate Fwy WB between Grieve Pde Off Ramp (ob) & Split To PFW and WRR",
"IDnumber": "7859OB_L_P0"
},
"type": "Feature"
},
],
"type": "FeatureCollection"
}
;
If anyone could show me how they would approach this that would be brilliant.
Thank you,

I think you have a fair bit of work to do to make a functional application but I've had a quick go at evolving your jsbin to do most of the "bits" of what you need.
I made up some additional fake data and included it at "7859OB_L_P1" in the jsbin.
Essentially I have:
restructured the example data to be a little more useful for building the heatmaps (though...it might actually be better to use these to prepare the data in the format expected by the heatmap library)
i.e. [[lat, lng, value], ...] rather than {'Time':[...], 'Data':{...}}
Used the nouislider library recommended by istopopoki
Used an existing leaflet plugin to draw the heatmaps: leaflet.heat
Most of the new "work" is done in the slider update call. You can see it here: https://jsbin.com/jeguwif/edit?html,js,output
// Register an update handler on the slider which:
// - Updates the "timeSelected" element
// - Calculates the new data for the time
// - sets the values into the heatmap and updates the "max" value
//
// NB: if there is LOTS of data, it might be too slow to recalculate these
// on every change, in which case perhaps building the [lat, lng, val] arrays
// for each time up front might be a better idea
slider.noUiSlider.on('update', function (values, handle) {
var index = Number(values[0])
timeSelected.value = tabular['Time'][index]
var dataRow = tabular['Data'][index]
// play with this val for scaling, i.e. depends how zoomed you are etc
var max = Math.max.apply(null, dataRow) * 1.0
var heatValues = tabular['Locations'].map((loc, idx) => {
return [].concat(locationCoords[loc]).concat(dataRow[idx])
})
heat.setOptions({max: max})
heat.setLatLngs(heatValues.concat(heatValues).concat(heatValues))
});
Additionally, I have added the script inclusions in the HTML head section.

Try the following :
create a function taking a date as argument that will display the data on your map only for that date. Or two arguments (startDate, enDate), and your function will display the data between those two dates. This function has to filter the data, and display it.
create a function that clears all the data on the map.
Next to the map, add a slider. Or date pickers, or anything that gives the possibility to choose a start and end date. You can for example use nouislider. When the user changes the dates range, you can bind something to that event. What you will bind is a function that first clears the map (ie call the function of step 2) and display the new data (ie call the function of step 1).

Related

Bind more popups to the same marker or merge popups content

I have a JSON in which there are places with their coordinates and their textual content to be inserted in the relative marker's popup.
If in the JSON there is 2 times the same place (with the same coordinates), I have to bind 2 popups with their respective contents on the same marker (or at most I have to update the popup with the new content while keeping the old one).
<html>
<head>
<!-- Libraries leaflet/jquery for may project-->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.0/jquery.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.3.1/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet#1.3.1/dist/leaflet.js"></script>
</head>
<body>
<div id="map" style="width:100%; height: 100%"></div>
<script>
// my json data
var data = [
{
"name" : "Location A",
"lat" :"27",
"long" : "29",
"popupContent" : "content 1"
},
{
"name" : "Location B",
"lat" :"51",
"long" : "12",
"popupContent" : "content 2"
},
{
"name" : "Location A",
"lat" :"27",
"long" : "29",
"popupContent" : "content 3"
}
]
//init leaflet map
var map = new L.Map('map');
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 18
}).addTo(map);
var italy = new L.LatLng(42.504154,12.646361);
map.setView(italy, 6);
//iterate my json data and create markers with popups
for(let key in data){
L.marker([data[key].lat,data[key].long]).bindPopup(data[key].popupContent).addTo(map)
}
</script>
</body>
</html>
With this code, the third place overrides the first, and I have a single marker and a single popup with written "content 3".
I would like 2 popups (one with written "content 1" and one with "content 3") or one single popup with all two contents.
The easiest way to address similar use case is simply to use a clustering plugin, typically Leaflet.markercluster, so that it separates your Markers which are on the same position or very close (actually your 3rd place does not "override" the first, in the sense of replacing, it just sits on top of it, in the sense of overlapping).
The added advantage is that it naturally separates Markers which are very close one to each other, but still at slightly different positions, which below heuristics will not catch.
var mcg = L.markerClusterGroup();
//iterate my json data and create markers with popups
for(let key in data){
L.marker(latLng).addTo(mcg) // Add into the MCG instead of directly to the map.
}
mcg.addTo(map);
Demo: https://plnkr.co/edit/B0XF5SSpQ27paWt1
Now in your case, you may not be wary of closeby Markers, but really have data that apply to same places (in your data, name and coordinates of items 1 and 3 are identical).
In that case, a solution could simply be to rework your data first (possibly in runtime) to merge the popup content of all items that have the same name and/or coordinates (depending on how exactly you can identify identical items).
For example using Lodash groupBy:
var groupedData = _.groupBy(data, "name"); // Depends on how you identify identical items
//iterate my json data and create markers with popups
for(let key in groupedData){
var items = groupedData[key];
// Coordinates of first item, all items of this group are supposed to be on same place
var latLng = [items[0].lat, items[0].long];
// Merge all popup contents
var popupContent = items.map(item => item.popupContent).join("<br/>")
L.marker(latLng).bindPopup(popupContent).addTo(map)
}
Demo: https://plnkr.co/edit/D7TzdaBVRvJr2sid

How to color LineString segments differently in Mapbox GL JS animations

I'm making an animation that shows where wolves go, based on some historical GPS-collar data I've got.
The code is based on this Mapbox example:
https://docs.mapbox.com/mapbox-gl-js/example/live-update-feature/
I would like to color the line segments based on whether it was daytime or nighttime, blue for night and red for daytime. Like this:
In time period 1, the wolf moves east-northeast; it's nighttime, so the line segment is blue. In time period 2, the wolf moves northeast; it's daytime, so the line segment is red. In time period 3, the wolf moves east-northeast again; it's nighttime, so the line segment is blue again.
But I can't seem to get the different coloring to work. I've got some toy/example data:
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": {
"type": "LineString", "coordinates" : [[-112.191833, 57.073668],
[-112.181833, 57.083668],
[-112.181833, 57.073668],
[-112.201833, 57.075668]]} } ],
"properties": {"daytime" : [0, 1, 1, 0] }}
There are 4 time periods and the middle two are daytime (set to 1).
Here's my code. (You'll need to paste in your mapbox key for it to work):
mapboxgl.accessToken = 'INSERT YOUR MAPBOX KEY HERE';
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/satellite-v9',
zoom: 0
});
map.on('load', function() {
// We use D3 to fetch the JSON here to parse and use it separately
// from GL JS's use in the added source. You can use any request method
// that you want.
d3.json(
"https://raw.githubusercontent.com/pete-rodrigue/wolves_of_alberta/data_store/wolves_geojson_example.geojson?token=ACEMB42EH5NKZSF24MHPQSS6JFTMU",
function(err, data) {
if (err) throw err;
// save full coordinate list for later
var coordinates = data.features[0].geometry.coordinates;
// save 1's and 0's for later
var daynight = data.properties.daytime
// start by showing just the first coordinate
data.features[0].geometry.coordinates = [coordinates[0]];
// THIS NEXT LINE IS ONE PART OF MY FAILED APPROACH:
data.properties.daytime = ['blue']; // set initial color value to blue
// add it to the map
map.addSource('trace', { type: 'geojson', data: data });
map.addLayer({
'id': 'trace',
'type': 'line',
'source': 'trace',
'paint': {
// THIS WILL WORK FINE
'line-color': 'orange',
// 'line-color': ['to-string', ['get', 'daytime']], // DOES NOT WORK
'line-width': 8
},
layout: {'line-cap': 'round', 'line-join': 'round'}
});
// setup the viewport
map.jumpTo({ 'center': coordinates[0], 'zoom': 13 });
map.setPitch(30);
// on a regular basis, add more coords from the saved list to update map
var i = 0;
var timer = window.setInterval(function() {
if (i < coordinates.length) {
data.features[0].geometry.coordinates.push(
coordinates[i]
);
// if it's daytime, append blue; if it's nighttime, append red
if (daynight[i] == 0) {
data.properties.daytime.push(['blue']);
} else {data.properties.daytime.push(['red']);}
map.getSource('trace').setData(data);
map.panTo(coordinates[i]);
i++;
} else {
window.clearInterval(timer);
}
}, 150);
}
);
});
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
<head>
<meta charset="utf-8" />
<title>Wolves GPS collar example data</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<script src="https://api.mapbox.com/mapbox-gl-js/v1.7.0/mapbox-gl.js"></script>
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v1.7.0/mapbox-gl.css" rel="stylesheet" />
<link rel="stylesheet" href="main_css.css">
</head>
<body>
<div id="map"></div>
<script type="text/javascript" src='main_js.js'></script>
</body>
Also here:
https://codepen.io/pete-rodrigue/pen/XWbJOpK
I've commented out the part that wasn't working and left a note.
Basically, I'm trying to do this:
'line-color': ['to-string', ['get', 'daytime']]
in the paint part of map.addLayer(), where the daytime property is an array of strings that say 'blue' or 'red', which I push new elements onto as the animation progresses--that's this part of the code:
if (daynight[i] == 0) {
data.properties.daytime.push(['blue']); // if it's daytime, append blue
} else {
data.properties.daytime.push(['red']); // if it's nighttime, append red
}
I'm sure there's an obvious reason why this doesn't work. But I'm new at this and can't fathom it.
Any help and explanation of the fundamentals would be much appreciated.
When you specify:
'line-color': ['to-string', ['get', 'daytime']]
in the 'trace' layer with Map#addLayer, the map renderer is being told to use '[0, 1, 1, 0]' (in the case of the example data you provided) as the color for the lines in the source with id 'trace'. It does not wait for you to set up the timer, and even if it did, you'd likely run into some problems with JavaScript's asynchronicity. As noted in Mapbox's style specification, 'line-color' expects a color in the form of HTML-style hex values, RGB, RGBA, HSL, or HSLA, so you are likely running into errors since a stringified array is none of those forms.
Instead, I'd recommend breaking up each time period into its own LineString where the 'line-color' is specified by its corresponding value in the original color array, and adding these LineStrings to the map as separate layers. You can then create the animation effect by setting up some intervals to specify the order in which the layers should be added and animated as done in the live update feature example.

Mapbox GL JS Updating Source of Image Overlay

I am working on a project where I have 4 maps on the screen at once. I have a barebones menu where the user selects an overlay, and it displays on the correct map.
This works, however the image sources for the image overlays I am using update server-side every 2 minutes. I would like to just have a simple function to re-fetch the source automatically so it's guaranteed to be updated every 2 minutes. I have added an image to show how this works.
I have found Mapbox API documentation on updating GeoJSON files in this manner, but I cannot figure out how to update image sources automatically. I have looked to no avail.
Here is my screenshot with actual source and layer, and below that I will write pseudocode for what I am looking to do.
Here is the Source and Layer:
topleftmapbox.on('load', function() {
topleftmapbox.addSource("source_KEWX_L2_REFLECTIVITY", {
"type": "image",
"url": "images/KEWX_L2_REFLECTIVITY.gif",
"coordinates": [
[-103.009641, 33.911],
[-94.009641, 33.911],
[-94.009641, 24.911],
[-103.009641, 24.911]
]
})
var layers = topleftmapbox.getStyle().layers;
// Find the index of the first symbol layer in the map style
var firstSymbolId;
for (var i = 0; i < layers.length; i++) {
if (layers[i].type === 'symbol') {
firstSymbolId = layers[i].id;
break;
}
}
topleftmapbox.addLayer({
"id": "overlay_KEWX_L2_REFLECTIVITY",
"source": "source_KEWX_L2_REFLECTIVITY",
"type": "raster",
"raster-opacity": 0.5,
"layout": {
"visibility": "none"
},
}, firstSymbolId)
});
Pseudocode for what I would like to do:
On Map load() {
start timer for every 2 minutes
Get Source "source_KEWX_L2_REFLECTIVITY"
Refresh the source with same URL ("images/KEWX_L2_REFLECTIVITY.gif") to make sure its live.
keep doing this every 2 minutes
}
There is (now?) an updateImage() method so you can do:
map.getSource('source_KEWX_L2_REFLECTIVITY').updateImage({
url: "images/KEWX_L2_REFLECTIVITY.gif?" + counter++,
coordinates': [
[-103.009641, 33.911],
[-94.009641, 33.911],
[-94.009641, 24.911],
[-103.009641, 24.911]
]
});
Including an incrementing counter will ensure you're not displaying a cached image.

JQuery .when .done seems to cause code to run before script is done loading

After replacing hard-coded <script> with JQuery promises, I've been frequently getting these errors:
Reproducing the problem is inconsistent. Occasionally, the page will load without the error, which seems to happen if I keep pressing the refresh button, rather than re-loading the page from a new tab.
Here is a minimal version of the code that demonstrates the problem:
<!DOCTYPE html>
<html>
<head>
<style>
#map {
width: 100%;
height: 100vh;
}
</style>
</head>
<body>
<div id="map"></div>
<script src=//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js></script>
<script>
$(makemap());
var pins = [
{
"lng": -79.9133742,
"lat": 43.2594723,
"id": 544
},
{
"lng": -79.9239563,
"lat": 43.2585329,
"id": 545
},
{
"lng": -79.92670809999998,
"lat": 43.2580113,
"id": 546
},
];
function makemap() {
$.when(
$('<link/>', {
rel: 'stylesheet',
href: '//cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.css'
}).appendTo('head'),
$('<link/>', {
rel: 'stylesheet',
href: '//cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/MarkerCluster.Default.css'
}).appendTo('head'),
$.getScript("//cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.js"),
$.getScript("//cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/leaflet.markercluster-src.js"),
$.Deferred(function (deferred) {
$(deferred.resolve);
})
).done(function () {
var tiles = L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '© <a href=http://osm.org/copyright>OpenStreetMap</a> contributors'
}),
latlng = L.latLng(43.26, -79.92);
var map = L.map('map', {center: latlng, zoom: 14, layers: [tiles]});
var markers = L.markerClusterGroup();
var markerList = [];
for (var i of pins) {
var marker = L.marker(L.latLng(i.lat, i.lng), {title: i.id});
marker.placeid = i.id;
markers.addLayer(marker);
markerList.push(marker);
}
map.addLayer(markers);
})
}
</script>
</body>
</html>
What am I doing wrong? If this is a limitation of JQuery, are there any alternative methods to accomplish what I want to do here (preferably using native ES6 or below)?
Attemp #2 with Mike's code:
<!DOCTYPE html>
<html>
<head>
<link rel=stylesheet href=//cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.css>
<link rel=stylesheet href=//cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/MarkerCluster.Default.css>
<style>
#map {
width: 100%;
height: 100vh;
}
</style>
</head>
<body>
<div id="map"></div>
<script src=//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js></script>
<script>
var pins = [
{
"lng": -79.9133742,
"lat": 43.2594723,
"id": 544
},
{
"lng": -79.9239563,
"lat": 43.2585329,
"id": 545
},
{
"lng": -79.92670809999998,
"lat": 43.2580113,
"id": 546
},
];
var scripts = [
"//cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.js",
"//cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/leaflet.markercluster-src.js"
];
var loaded = scripts.length;
function checkDone() {
loaded = loaded - 1;
if (loaded === 0) {
var tiles = L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '© <a href=http://osm.org/copyright>OpenStreetMap</a> contributors'
}),
latlng = L.latLng(43.26, -79.92);
var map = L.map('map', {center: latlng, zoom: 14, layers: [tiles]});
var markers = L.markerClusterGroup();
var markerList = [];
for (var i of pins) {
var marker = L.marker(L.latLng(i.lat, i.lng), {title: i.id});
marker.placeid = i.id;
markers.addLayer(marker);
markerList.push(marker);
}
map.addLayer(markers);
}
}
while (scripts.length) {
var head = scripts.splice(0, 1)[0];
$.getScript(head, checkDone);
}
</script>
</body>
</html>
As you have discovered, the heart of problem with jQuery.getScript() is that :
it reliably fires its callback (or chained .then()) to indicate that the script has loaded, but
there's no guarantee that the loaded script has executed
According to this answer the issue should be fixed in jQuery 2.1.0+, however, from what you say, that would appear not to be the case.
To give the scripts greater chance to have executed, execution of the map/marker code needs to be pushed later by some unknown small amount.
Here are some things to try - no better than that I'm afraid.
First give the seconmd script more time to load by splitting checkDone() into two functions as follows :
function makeMap() {
var map = L.map('map', {
center: L.latLng(43.26, -79.92),
zoom: 14,
layers: [L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '© <a href=http://osm.org/copyright>OpenStreetMap</a> contributors'
})]
});
setTimeout(makeMarkers.bind(map), 0);
}
function makeMarkers() {
var markers = L.markerClusterGroup();
pins.forEach(function(pin) {
var marker = L.marker(L.latLng(pin.lat, pin.lng), { title: pin.id });
marker.placeid = pin.id;
markers.addLayer(marker);
});
this.addLayer(markers);
}
Note that makeMarkers() is called from makeMap() via a timeout. Even with a timeout of 0, the call will occur in a later event thread.
Now, with those two functions in palce, progressively replace the while (scripts.length) {...} loop with :
$.when.apply(null, scripts.map(function(url) {
return $.getScript(url);
})).then(makeMap);
then with :
scripts.reduce(function(promise, url) {
return promise.then(function() {
return $.getScript(url);
});
}, $.when()).then(makeMap);
then with :
scripts.reduce(function(promise, url) {
return promise.then(function() {
return $.getScript(url);
});
}, $.when()).then(function() {
setTimeout(makeMap, 0);
});
then with :
scripts.reduce(function(promise, url) {
return promise.then(function() {
return $.getScript(url);
});
}, $.when()).then(function() {
setTimeout(makeMap, 1000);
});
At some stage, you should hopefully find that one of these processes (or maybe the last version with even more delay) fires makeMap late enough to be reliable - on your browser/computer under prevailing conditions.
You still have uncertainty over other browsers and other computers ....
Ultimately, you may need to revert to hard-coced script tags and accept the greater page-load time. What would be nice is for the <SCRIPT> tag's defer attribute to be reliably supported in all browsers.
Two things:
you're missing a master $( function doThisOnDocumentReady() { ... }) wrapper around your code. Right now it'll kick in immediately rather than waiting for the DOM to be done first. That's not super critical here, but it's good practice and a good habit to get into (you can pretty much ignore this advice if you just want this problem solved though)
getScript itself does not wait for resources linked in those nodes to finish "all the way" before returning. You can give it a callback to trigger after it finishes loading the script, but that still doesn't guarantee the scrip's finished executing, too. As such, right now your code runs before we are technically guaranteed that the things you need are actually available.
So, instead of hoping they're reading, let's explicitly make sure they're ready before we run the code that relies on having leaflet etc. properly set up:
function doStuffKnowingItllWork() {
// ... your actual code ...
}
$.when() {
// ... load scripts etc here ...
).done(function () {
// ensure we have everything available
(function ensureDependencies() {
if (window.L) {
return setTimeout(doStuffKnowingItllWork,0);
}
setTimeout(ensureDependencies, 200);
}());
});
This sets up a function that checks whether we have what we need to do the work we need to do (in the case of leaflet, a global L variable) and if it's not available, we schedule a call to the same function (at least) 200 milliseconds from now.
If we do have all the dependencies we need, we schedule a call to the "actually run the code you need", with a fresh execution context and callstack by scheduling it on the next tick, using setTimeout 0 - you could just return doStuffKnowingItllWork();, but then if there's an error your stack trace is going to include the dependency check and jquery wrapping.
Also note that this is a function that immediately gets called after declaring, and that there is some syntax freedom: both (function x(){})() and (function x(){}()) do the same; where you put that execution operator (the () part that runs a function) is mostly up to you and what JS style you want to follow. Also note it's functionally equivalent to function x(){}; x(); so if you want to use that for clarity: also up to you.
Call Jquery before leaflet.js
and $.when( in document.ready

Basemap Reference Layers (text) ignore Opacity on Selection-Change?

I am trying to keep my basemap layer opacity at a constant between different selections (and can be controlled by the user with a slider). Any basemap layers that don't have a related 'reference' layer behave as expected (i.e. if topo is at 25% before changing to imagery, it will update to 25% on change). If a user selects a basemap that also has a reference layer (imagery with labels; light gray canvas, etc), the reference layer ignores the opacity setting when loaded and will only change AFTER the user tries to move the slider. Thoughts?
Fun tidbit... Basemap layer 'Terrain with Labels' ignores this completely on both the imagery and the text when swapping. It almost looks like it refreshes after it loads.
Here is the working example in JSFiddle (http://jsfiddle.net/disuse/ez6mN/) and the dojo code that I am using to replicate my issue. Using the latest Esri ArcGIS Javascript 3.7.
Codeblock
var baseMap_Opacity;
var baseOpacity = 0.25;
require([
"esri/map",
"esri/dijit/BasemapGallery",
"dijit/form/HorizontalSlider",
"dijit/form/HorizontalRule",
"dijit/form/HorizontalRuleLabels",
"dojo/parser",
"dojo/dom",
"dojo/on",
"dojo/ready",
"dojo/domReady!"
], function(
Map,
BasemapGallery,
HorizontalSlider,
HorizontalRule,
HorizontalRuleLabels,
parser,
dom,
on,
ready
) {
ready(function() {
map = new Map("map", {
center: [-121.569, 39.00],
zoom: 7,
optimizePanAnimation: true,
basemap: "topo"
});
var basemapGallery = new BasemapGallery({
showArcGISBasemaps: true,
map: map
}, "basemaps");
basemapGallery.startup();
basemap = map.getLayer("layer0");
basemap.setOpacity(baseOpacity);
on(basemapGallery, "selection-change", function() {
changeBasemapOpacity(baseOpacity);
});
createHorzSlider();
});
function createHorzSlider() {
baseMap_Opacity = dom.byId("baseMap_Opacity");
baseMap_Opacity.innerHTML = Math.round(baseOpacity*100) + "%";
var horzSlider = new HorizontalSlider({
minimum: 0,
maximum: 1,
value: baseOpacity,
intermediateChanges: true,
showButtons: true,
discreteValues: 101,
style: "width: 300px; margin-left: 25px;",
onChange: function(value) {
changeBasemapOpacity(value);
}
}, "horzSlider");
horzSlider.startup();
var horzSliderRule = new HorizontalRule({
container: "bottomDecoration",
count: 2 ,
style: "height: 5px; width: 288px; margin-top: 5px; margin-left: 32px;"
}, "horzSliderRule");
horzSliderRule.startup();
var horzSliderLabels = new HorizontalRuleLabels({
container: "bottomDecoration",
labels: ["0", "100"],
style: "width: 288px; margin-left: 32px;",
labelStyle: "font-style: normal; font-weight: bold; font-size: 14px;"
}, "horzSliderLabels");
horzSliderLabels.startup();
}
function changeBasemapOpacity(value) {
baseOpacity = value;
baseMap_Opacity.innerHTML = Math.round(baseOpacity*100) + "%";
var esriURL = "http://services.arcgisonline.com";
var layers = map.getLayersVisibleAtScale();
for (var i = 0; i < layers.length; i++) {
var lyr = map.getLayer(layers[i].id);
if ((lyr._basemapGalleryLayerType) || (lyr.id == "layer0") || ((lyr.url) && (lyr.url.indexOf(esriURL) == 0))) {
lyr.setOpacity(baseOpacity);
}
}
}
});
The basemap gallery's selection-change event fires after the newly selected basemap is in the map. This fires before reference layers are added and is the intended design, the idea being that you wouldn't want to manipulate reference layers. In your case, that's not what you want so using selection-change is out.
To accomplish what you want, use the map's layer-add-result event and check if layer._basemapGalleryLayerType is truthy. If it is, you know a layer used by the basemap gallery was added to the map and you should update its opacity. Here's a code snippet:
map.on("layer-add-result", function(e) {
if ( e.layer._basemapGalleryLayerType ) {
e.layer.setOpacity(baseOpacity);
}
});
Regarding the issue with the Terrain with Labels basemap, things are working as expected. Because that basemap's reference layer includes labels as well as political boundaries and major roads, it looks like opacity isn't being applied when in fact it is. Using the code above will set opacity on both the layer that represents the terrain basemap as well as the reference layer.
Here's a modified version of your page that I think accomplishes what you want: http://jsbin.com/IyixAPa/1/edit

Categories