Open matthieugouel opened 4 years ago
For the heatmap, this is technically impossible since the heatmap layer gets colorized after the accumulation stage (when all features are rendered into one grayscale texture), using a single gradient as lookup.
For the line gradient though, this might be possible — needs investigation.
Ok. I thought I was the same "engine" underneath since they share common API but, yeah, I'm mostly interested in line-gradient
anyway.
Is there a some kind of "standard procedure" in order to implement a data driven property ? I'm trying to investigate on my own but I'm not familiar with the code so it's not very efficient 😄
I've been using something like this:
const stops = [
0, 'green',
0.2, 'cyan',
0.6, 'orange',
0.9, 'green',
1, 'cyan',
];
map.addLayer({
type: 'line',
source: 'line',
id: 'line',
paint: {
'line-width': 14,
'line-gradient': [
'interpolate',
['linear'],
['line-progress'],
...stops
]
},
});
Seems to work well, but ideally I want there to be a hard line between the colour transitions. But I can't seem to figure out how to do that.
Hello, I have a need to put several animated lines with different gradients on the map. Trying to read gradient values from a line's properties (in the same manner as matthieugouel did) ended up with this error:
Error: layers.line-animation.paint.line-gradient: data expressions not supported
Will it be fixed sometime? It looks like the only way to do my task is to put lines in different layers which will hit performance, and I would rather not to do that.
any update?
I'm also very much interested in having such functionality. Is there a way to give a hand to help implementing it?
Adding another request for this feature!
This feature would be great, thanks :)
Also it would be great if the gradient values (not colors) were to be taken from a property or an array that matches the line points array. There's a nice feature in the Oruxmaps app that shows the slope using gradient colors - i.e. when the slope is "hard" the color is red and when the slope is "easy" the color is green. This would be a very nice addition to this framework to be able to do it. I have looked at the code to understand where the data from line progress is coming from but couldn't fully understand, mainly I guess because it's tiled base...? If someone can help me better understand the code I might be able to help here... IF you want me to open a new issue since it's not exactly the same let me know...
Hi. I found a solution for this, maybe. Example. We need set gradient to route by speed data. 0 - green, 50 - yellow, 100 -red
map.addSource('route', {
'type': 'geojson',
'lineMetrics': true,
'data': {
'type': 'FeatureCollection',
'features': [{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": values.map(item => item.location)
}
}]
}
});
map.addLayer({
id: "route",
type: "line",
source: "route",
paint: {
"line-width": 5,
'line-gradient': [
'interpolate',
['linear'],
['line-progress'],
...getLineBackground()
]
}
})
const getLineBackground = () => {
const range = chroma.scale('green', 'yellow', 'red').domain(0, 50, 100); // use chroma.js
const colorsData = [];
const totalDistance = values.reduce((total, currentPoint, index) => {
if (index !== this.props.values?.length - 1) {
return total + getDistanceFromLatLng(currentPoint.location[0], currentPoint.location[1], this.props.values[index + 1].location[0], this.props.values[index + 1].location[1]);
}
return total;
}, 0); // distance between first and past point
// next calculate percentage one line to all distance
const lengthBetweenPoints = values?.map((item, index) => {
if (index === 0) {
return {
...item,
weight: 0
};
}
if (index === values?.length - 1) {
return {
...item,
weight: 1
};
}
return {
...item,
weight: getDistanceFromLatLng(item.location[0], item.location[1], values[index + 1].location[0], values[index + 1].location[1]) / totalDistance
};
});
let weight = 0;
// next fill colorsData for **line-progress** property
lengthBetweenPoints.forEach((item, index) => {
if (item.weight || index === 0) {
if (index !== lengthBetweenPoints.length - 1) {
weight += item.weight;
colorsData.push(weight);
} else {
colorsData.push(item.weight);
}
colorsData.push(range(item.speed).hex());
}
});
return colorsData;
};
In final, we have
PS: Code for getDistanceFromLatLng
const getDistanceFromLatLng = (lon1,lat1,lon2,lat2) => {
var R = 6371; // Radius of the earth in km
var dLat = deg2rad(lat2-lat1); // deg2rad below
var dLon = deg2rad(lon2-lon1);
var a =
Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2)
;
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
var d = R * c; // Distance in km
return d;
};
function deg2rad(deg) {
return deg * (Math.PI/180);
}
@EarlOld this only works for a single feature. Ideally, we would be able to set data-driven line-gradients for tons of lines in the same layer (such as a road network visualizing traffic). One way we could do that is by keeping line-gradient
static (so there's one texture for color lookup), but introducing a new line-gradient-progress
property that would map line-progress
to the final gradient key value, e.g.:
'line-gradient-progress': [
'interpolate', 'linear', ['line-progress'],
0, ['get', 'start_speed'],
1, ['get', 'end_speed']
]
Any updates? Would love to have this feature to track altitude above the ground for a flight tracking app.
Also very interested in this feature. We have a lot of line segments that we want to dynamically aplly a gradient to, and currently the only way to achieve this is to split them into individual layers, which comes with a huge performance hit (we have hundreds of them). Having an ability to use data driven properties to control gradient would be extremely useful!
Adding my support here - this would be perfect for visualizing a vehicle's speed along a route (collection of LineString
s, each of which has an initialVelocity
and finalVelocity
). In fact, the use case for line-gradient
as it currently exists is extremely narrow, as you'll rarely know your stop outputs ahead of time unless you're visualizing something super simple where the outputs are completely unrelated to each feature itself. As mentioned, the only workaround is a complete anti-pattern and abuse of the library - introducing N layers for N styled features.
Ultimately we've decided to implement a custom mapbox layer with three.js (via threebox) and some custom gradient logic. Now we have one layer with hundreds of lines and we can change the gradient dynamicly on a per-line basis. The performance with this approach is really nice, we notice virtually no difference compared to the vanilla implementation. Since it's a custom layer for mapbox, this means all the higher-level logic related to mapbox required very little changes - most of the effort was spent building this custom layer itself.
This is not the ideal solution since it introduces some extra dependencies and extra complexity, but if the dynamic gradients are a key feature for your particular use-case (as it is for us), I would recommend looking into supplementing mapbox with three.js.
@eslavnov Would you mind sharing a small working example of your solution? I don't have much experience with three.js so that would be helpful. Thanks!
As mentioned, the only workaround is a complete anti-pattern and abuse of the library - introducing N layers for N styled features.
@nickfaughey @eslavnov @dalbani There's actually another way to achieve this that scales with only the numbers of colors in your ramp. Instead of adding your line geometry and varying the gradient stops, we can tweak the source geometry such that it's representable by a static gradient scheme.
Here's how it would work, with a color ramp that goes Red - Orange - Yellow - Green
:
In the style, we add a line layer for every pairwise colors in the ramp sequence: RedToOrange
, OrangeToYellow
, YellowToGreen
. This strategy requires N-1
layers, where N is the number of colors in the ramp.
In the geometry, we figure out where each stop would be, and chop up the line at those points (this is easy with a utility like turf.lineSliceAlong), so that the original line is now up to N-1
shorter segments. This can be done at runtime if you wrap it in a function.
We use layer RedToOrange
to render the first segment of each original line, OrangeToYellow
to render the second segments, and YellowToGreen
to render the thirds (we can make it easy to filter by tagging each segment with a segmentIndex
property). This works because even with variable gradient stops, the gradient between stops is always linear.
R-O O-Y Y-G
|-----||-------||-------|
R-O O-Y Y-G
|---------||----||---------|
The ideal solution is probably data-driven gradient stops, but this should be viable until then.
Here is an implementation with the Mapbox custom layer and Three.js (via ThreeBox-plugin). It only uses one layer, but you can define different lines with different start and end colors. You can also extend it easily in order to support more-points gradient. Here is a JSFiddle where you can check the implementation: https://jsfiddle.net/rqdz0ufL/4/ Before run you should put your Mapbox token into the code!
Hope this will help!
Any update on this? It would be great to have data-driven gradient stops. Thanks
Adding my interest in this feature, especially considering threebox is now archived. Would be great to paint different gradients in the same layer as opposed to sorting features by color and then painting them to different layers.
Has anyone tried splitting the line into features and adding a layer for each feature? Hard, medium, easy Red, yellow, green
Should work, no idea at which amount of layers it might break though 🙈
@LunicLynx Can you please share your solution code as soon as possible ?
Has anyone tried splitting the line into features and adding a layer for each feature? Hard, medium, easy Red, yellow, green
@GadhiyaRiddhi there is no code. I just think it should be possible.
We would also be very interested in this.
Also expressing interest in this!
Hi. I found a solution for this, maybe. Example. We need set gradient to route by speed data. 0 - green, 50 - yellow, 100 -red
- Create source with all points
map.addSource('route', { 'type': 'geojson', 'lineMetrics': true, 'data': { 'type': 'FeatureCollection', 'features': [{ "type": "Feature", "geometry": { "type": "LineString", "coordinates": values.map(item => item.location) } }] } });
- Create layer
map.addLayer({ id: "route", type: "line", source: "route", paint: { "line-width": 5, 'line-gradient': [ 'interpolate', ['linear'], ['line-progress'], ...getLineBackground() ] } })
- Create getLineBackground function.
const getLineBackground = () => { const range = chroma.scale('green', 'yellow', 'red').domain(0, 50, 100); // use chroma.js const colorsData = []; const totalDistance = values.reduce((total, currentPoint, index) => { if (index !== this.props.values?.length - 1) { return total + getDistanceFromLatLng(currentPoint.location[0], currentPoint.location[1], this.props.values[index + 1].location[0], this.props.values[index + 1].location[1]); } return total; }, 0); // distance between first and past point // next calculate percentage one line to all distance const lengthBetweenPoints = values?.map((item, index) => { if (index === 0) { return { ...item, weight: 0 }; } if (index === values?.length - 1) { return { ...item, weight: 1 }; } return { ...item, weight: getDistanceFromLatLng(item.location[0], item.location[1], values[index + 1].location[0], values[index + 1].location[1]) / totalDistance }; }); let weight = 0; // next fill colorsData for **line-progress** property lengthBetweenPoints.forEach((item, index) => { if (item.weight || index === 0) { if (index !== lengthBetweenPoints.length - 1) { weight += item.weight; colorsData.push(weight); } else { colorsData.push(item.weight); } colorsData.push(range(item.speed).hex()); } }); return colorsData; };
In final, we have
PS: Code for getDistanceFromLatLng
const getDistanceFromLatLng = (lon1,lat1,lon2,lat2) => { var R = 6371; // Radius of the earth in km var dLat = deg2rad(lat2-lat1); // deg2rad below var dLon = deg2rad(lon2-lon1); var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon/2) * Math.sin(dLon/2) ; var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); var d = R * c; // Distance in km return d; }; function deg2rad(deg) { return deg * (Math.PI/180); }
Thanks. That approach works great
I implemented it this way now:
map.on('load', function() {
// Get the upper and lower bounds for the speed variable
const lowerBound = lineFeature.properties.myProperty[0];
const upperBound = lineFeature.properties.myProperty[1];
const midpoint = (lowerBound + upperBound) / 2;
map.addLayer({
'id': 'line',
'type': 'line',
'source': {
'type': 'geojson',
'data': lineFeature,
'lineMetrics': true
},
'layout': {
'line-join': 'round',
'line-cap': 'round'
},
'paint': {
'line-width': 8,
'line-gradient': [
'interpolate',
['linear'],
['line-progress'],
0, ['rgba', ...getRGBGradientGreenYellowRed(lowerBound, 0, 10), 1],
0.5, ['rgba', ...getRGBGradientGreenYellowRed(midpoint, 0, 10), 1],
1, ['rgba', ...getRGBGradientGreenYellowRed(upperBound, 0, 10), 1]
]
}
})
});
This colors the line in a gradient green to yellow to red depending on the value of myProperty. myProperty has two values, one for the starting point and one for the end point. Everything <= 0 is green and >=10 is red. So if I have myProperty=[2.5, 7.5] the start is yellow-green, the end is orange and the colors in between a smooth gradient.
Here is the function to get the RGB tuple based on an input number (myProperty) and the upper and lower bounds (here 0 and 10):
function getRGBGradientGreenYellowRed(input: number, rangeStart: number = 0.0, rangeEnd: number = 1.0, convert255 = true): [number, number, number] {
const distance: number = rangeEnd - rangeStart;
const r: number = 2.0 * ((input - rangeStart) / distance);
const g: number = 2.0 * (1.0 - ((input - rangeStart) / distance));
const rgb: [number, number, number] = [r > 1.0 ? 1.0 : r / 1.0, g > 1.0 ? 1.0 : g / 1.0, 0.0];
if (convert255) {
return [rgb[0] * 255.0, rgb[1] * 255.0, rgb[2] * 255.0];
} else {
return rgb;
}
}
Motivation
As far as I tested for know, it seems not possible to use data driven property to set the color associated with the percentages in the line-gradient / heatmap paint property. For instance see this API example below :
Cheers, Matthieu.