Mapbox GL JS Updating Source of Image Overlay - javascript

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.

Related

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.

TimeDimension with Leaflet and Python backend

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).

Display data from JSON on hover of SVG path?

I am working on a SVG map of the US and I would like to display a small info window pointing to each state when you hover over it that shows the population, similar to the info windows on Google Maps. Unfortunately I'm not very good with JSON so I'm stuck at this point.
Currently I have the SVG map of the US up and the fill color changes on hover via CSS. I have a sample group of JSON data and wondering how I can incorporate it into my map so that it displays the population on hover.
Here's an example of the JSON data:
[
{
"id":"AL",
"population":"253045"
},
{
"id":"AR",
"population":"1529923"
},
{
"id":"AZ",
"population":"352416t"
}
]
There's quite a bit of HTML due to the SVG paths, so I won't paste that here, but you can see what I have so far on this JSFiddle
To get info about the hovered state, first put your info in an array, so it can be accessed:
var info = [
{
"id":"AL",
"population":"253045"
},
{
"id":"AR",
"population":"1529923"
},
{
"id":"AZ",
"population":"352416t"
}
]
Then you can get info from your array by filtering it by the id key using filter:
info.filter(function(state) {return state.id === currState});
Here is an example where you get the population of the hovered state:
$('path').hover(function() {
var currState = $(this).attr('id');
var currStateInfo = info.filter(function(x) {return x.id == currState});
if (currInfo[0])
console.log(currInfo[0].population);
});
Here is a working example: https://jsfiddle.net/4tasLo6k/

Data from json file for use with CHAPS timeline

I'm trying to use the CHAP links library timeline (http://almende.github.io/chap-links-library/timeline.html).
Example17 is using JSON, but it's in the html file itself. I'd like to use an external JSON file sitting on the web server instead.
Here's my example.json:
{"timeline":[
{
"start":"2013,7,26",
"end":"2013,7,26",
"content": "Bleah1"
},
{
"start":"2013,7,26",
"end":"2013,8,2",
"content": "Bleah2"
},
{
"start":"2013,7,26",
"end":"2013,8,2",
"content": "Bleah3"
},
{
"start":"2013,7,26",
"end":"2013,8,2",
"content": "Bleah4"
}
]}
I added this:
<script src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
And here's the modified function:
// Called when the Visualization API is loaded.
function drawVisualization() {
// Create a JSON data table
$.getJSON('example.json', function(jsondata) {
data = jsondata.timeline;
});
// specify options
var options = {
'width': '100%',
'height': '300px',
'editable': true, // enable dragging and editing events
'style': 'box'
};
// Instantiate our timeline object.
timeline = new links.Timeline(document.getElementById('mytimeline'));
function onRangeChanged(properties) {
document.getElementById('info').innerHTML += 'rangechanged ' +
properties.start + ' - ' + properties.end + '<br>';
}
// attach an event listener using the links events handler
links.events.addListener(timeline, 'rangechanged', onRangeChanged);
// Draw our timeline with the created data and options
timeline.draw(data, options);
}
Anyone who can tell me what I'm doing wrong gets a cookie! :-)
Update: I should specify that it's rendering the timeline div correctly, I'm just getting no data showing up.
Your start and end dates need to be parsed as Date objects for use in the timeline
I stumbled on this post as I was implementing similar functionality.
In version 2.6.1 of timeline.js, around line 3439 where the function links.Timeline.Item is declared, you'll notice a comment relating to implementing parseJSONDate.
/* TODO: use parseJSONDate as soon as it is tested and working (in two directions)
this.start = links.Timeline.parseJSONDate(data.start);
this.end = links.Timeline.parseJSONDate(data.end);
*/
I enabled the suggested code and it all works!* (go to the parseJSONDate function to see which formats are accepted)
*(works insofar as dates appear on the timeline.. I'm not using therefore not testing any selection/removal features, images, or anything like that..)

openlayers vector features z-indexing

Im having hard time trying to understand the z-indexing of the vector features.
When i was searching the web for info i found these links:
http://openlayers.org/dev/examples/ordering.html
http://osgeo-org.1803224.n2.nabble.com/Bug-in-using-graphicZIndex-td2648665.html
and
http://osgeo-org.1803224.n2.nabble.com/graphicZIndex-of-vector-features-td3919627.html
What i did, was setting up styles like they are shown on first link:
this.vectorsLayer = new OpenLayers.Layer.Vector("Vectors", {
styleMap: new OpenLayers.StyleMap({
"default": {
'strokeColor': "#ff9933",
'strokeWidth': 5
},
"select": {
'strokeColor': "#3399ff"
}
})
}
);
this.carsLayer = new OpenLayers.Layer.Vector("Cars", {'rendererOptions': {yOrdering: false, zIndexing: true}});
this.startIconStyle = {'externalGraphic':this.startIconUrl};
this.parkIconStyle = {'externalGraphic':this.parkIconUrl};
this.endIconStyle = {'externalGraphic':this.endIconUrl};
this.defaultStyles = {
//'label':getLabel(),
'graphicZIndex':745,
'graphicXOffset':-13,
'graphicYOffset':-41,
'graphicWidth':26,
'graphicHeight':41,
'strokeLinecap':'round',
'strokeColor':"#000000",
'strokeWidth':2,
'strokeOpacity':1,
'fillOpacity':1}
//style of path that car has used
this.drivedStyle = {
'strokeWidth': 3,
'strokeOpacity': 1,
'strokeColor': "#3399ff",
'strokeDashstyle': "dash"
}
And when i create markers, i do it like that:
var routePoint = this.points[this.routePos].clone();
var newstyleSite = OpenLayers.Util.extend(this.startIconStyle, this.defaultStyles ,OpenLayers.Feature.Vector.style['default']);
this.routePointFeature = new OpenLayers.Feature.Vector(routePoint, {}, newstyleSite);
this.vectorsLayer.addFeatures(this.routePointFeature);
And when i look at the z-index of that marker - z-index is set to auto not 745, which is in this.defaultStyles.
This brings us to 3rd link... which i cant understand at all, cause setting styles anywhere else gives exactly as much, as im getting right now.
Neither does
this.routePointFeature.attributes.zIndex = 745;
change anything at all. Even though it apparently works on that first link/example.
How is this z-indexing supposed to work? And why doesnt it work in my case? What am i doing wrong? Or is something bugged there?
Edit: Alot of people have viewed this question and upvoted the answer. Yet i have had to deal with other stuff and have not worked with opelayers for a while now. Can some people who have seen this answer confirm that the answer works so i can accept it?
Alan
You have to enable z-indexing for the vector layer.
this.vectorsLayer = new OpenLayers.Layer.Vector("Vectors", {
styleMap: <your style map>,
rendererOptions: { zIndexing: true }
});
Additionally, OpenLayers.Util.extend only takes two parameters, and the first parameter is the destination (i.e., the second parameter, source, will be combined into it). If you want to combine multiple objects, you can use jQuery.extend or multiple calls to OpenLayers.Util.extend:
OpenLayers.Util.extend(this.startIconStyle, OpenLayers.Feature.Vector.style['default'] );
OpenLayers.Util.extend( this.startIconStyle, this.defaultStyles );
or
jQuery.extend( this.startIconStyle, OpenLayers.Feature.Vector.style['default'], this.defaultStyles );
Both of these will result in this.startIconStyle containing the union of this.startIconStyle, OpenLayers.Feature.Vector.style['default'], and this.defaultStyles.
What you may really want is:
var newstyleSite = {};
jQuery.extend( newstyleSite, OpenLayers.Feature.Vector.style['default'], this.defaultStyles, this.startIconStyle );

Categories