Related
I want to highlight fill-extrusion features when hovered over them.
The styling related to this is straight-forward using expressions and feature state, but I am having trouble retrieving the correct features.
There is code available online to change the feature state when hovered over, and it seems straight-forward enough, so I adapted it:
var hover_id = null;
const feature_state = { hover: true }
map.on('mousemove', '3d-buildings', (e) => {
// Get features under cursor, following render order
const features = map.queryRenderedFeatures(e.point);
// Check that features are not empty
if (features.length > 0) {
// Clean up previously hovered feature
if (hover_id) {
map.removeFeatureState({source: "composite", sourceLayer: 'building', id: hover_id});
}
// Set feature state of the new hovered feature
hover_id = features[0].id;
map.setFeatureState({source: 'composite', sourceLayer: 'building', id: hover_id}, feature_state);
console.log(hover_id)
}
});
While this works well initially, it stops working as soon as I tilt the camera using the right mouse button. After tilting, the foremost element no longer gets selected (something else seems to get selected and an ID gets printed out, but nothing shows up on the map and no error is thrown).
On a related note, the correct feature only gets selected after zooming in quite far - there is a large zoom range where the buildings already get rendered to the screen, but seem to not get picked up by queryRenderedFeatures. Is this expected behaviour?
Expected behaviour:
map.queryRenderedFeatures(...)[0] selects the foremost feature, independent of the camera tilt.
What could be a possible reason for the camera tilt influencing the feature selection? Is this a bug or am I misusing the API?
I think the issue you’re facing has nothing to do with tilt but with the fact that you’re adding and removing the state instead of changing the value of the state. The state must be declared in the layer definition, change the color with a expression, and then you only need to change the value of the state.
Here you have a fiddle I have created to show how to change color of fill extrusions on mouse over/out
Relevant code is this:
let mapConfig = {
NYC: {
origin: [-74.044514, 40.689259, 39],
center: [-74.0137, 40.70346, 0],
zoom: 16.2,
pitch: 60,
bearing: 35
}
}
mapboxgl.accessToken = 'PUT YOUR TOKEN HERE';
let point = mapConfig.NYC;
var map = new mapboxgl.Map({
style: 'mapbox://styles/mapbox/streets-v11',
center: point.center,
zoom: point.zoom,
pitch: point.pitch,
bearing: point.bearing,
container: 'map',
antialias: true,
hash: true
});
map.on('style.load', function() {
if (map.getSource('composite')) {
map.addLayer({
'id': '3d-buildings',
'source': 'composite',
'source-layer': 'building',
'type': 'fill-extrusion',
'minzoom': 14,
'paint': {
'fill-extrusion-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
'#ff0000',
'#ddd'
],
'fill-extrusion-height': ["number", ["get", "height"], 5],
'fill-extrusion-base': ["number", ["get", "min_height"], 0],
'fill-extrusion-opacity': 1
}
}, 'road-label');
}
let fHover;
map.on('mousemove', function(e) {
//157001066
var features = map.queryRenderedFeatures(e.point, {
layers: ['3d-buildings']
});
if (features[0]) {
mouseout();
mouseover(features[0]);
} else {
mouseout();
}
});
map.on('mouseout', function(e) {
mouseout();
});
function mouseout() {
if (!fHover) return;
map.getCanvasContainer().style.cursor = 'default';
map.setFeatureState({
source: fHover.source,
sourceLayer: fHover.sourceLayer,
id: fHover.id
}, {
hover: false
});
}
function mouseover(feature) {
fHover = feature;
map.getCanvasContainer().style.cursor = 'pointer';
map.setFeatureState({
source: fHover.source,
sourceLayer: fHover.sourceLayer,
id: fHover.id
}, {
hover: true
});
}
});
If this answer solves your question, please mark it as answer accepted in that way it will also help other users to know it was the right solution.
#jscastro This works fine: My requirement is I need to change the color of few buildings with lat and lng. I have achieved getting buildings id's from lat and lng by using below API
https://api.mapbox.com/v4/mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2/tilequery/55.26365875255766,25.188400365955193.json?radius=30&limit=10&dedupe&access_token=.
I am facing one issue here, The colors are changing only after zoom level 17. I want to change the color on zoom level 15.
map.on("style.load", function () {
if (map.getSource("composite")) {
const layers = map.getStyle().layers;
const labelLayerId = layers.find(
(layer) => layer.type === "symbol" && layer.layout["text-field"]
).id;
map.addLayer(
{
id: "3d-buildings",
source: "composite",
"source-layer": "building",
filter: ["==", "extrude", "true"],
type: "fill-extrusion",
minzoom: 15,
zoom: 15,
pitch: 60,
bearing: -60,
layout: {
// Make the layer visible by default.
visibility: "visible",
},
paint: {
"fill-extrusion-color": [
"case",
["boolean", ["feature-state", "hover"], false],
"#00ff00",
"#AED0EC",
],
"fill-extrusion-height": [
"interpolate",
["linear"],
["zoom"],
15,
0,
15.5,
["get", "height"],
],
"fill-extrusion-base": [
"interpolate",
["linear"],
["zoom"],
15,
0,
15.05,
["get", "min_height"],
],
"fill-extrusion-opacity": 1,
},
},
labelLayerId
);
}
map.getCanvasContainer().style.cursor = "pointer";
map.setFeatureState(
{
source: "composite",
sourceLayer: "building",
id: "4411722601841895",
},
{
hover: true,
}
);
map.getCanvasContainer().style.cursor = "pointer";
map.setFeatureState(
{
source: "composite",
sourceLayer: "building",
id: "1315660041727095",
},
{
hover: true,
}
);
map.getCanvasContainer().style.cursor = "pointer";
map.setFeatureState(
{
source: "composite",
minzoom: 15,
sourceLayer: "building",
id: "3957345234349675",
},
{
hover: true,
}
);
map.getCanvasContainer().style.cursor = "pointer";
map.setFeatureState(
{
source: "composite",
sourceLayer: "building",
id: "5328485811",
},
{
hover: true,
}
);
});
I get some information from a AJAX and I create a GeoJson looking like
var waypointGeojson = {
'type': 'FeatureCollection',
'features': [{
'type': 'Feature',
'properties': {
'id': waypoint.poi_id,
'name': waypoint.name,
'iconSize': [100, 100]
},
'geometry': {
'type': 'Point',
'coordinates': [waypoint.longitude, waypoint.latitude],
}, {
'type': 'Feature',
'properties': {
'id': waypoint.poi_id,
'name': waypoint.name,
'iconSize': [25,25]
}, {
'geometry': {
'type': 'Point',
'coordinates': [waypoint.longitude, waypoint.latitude],
}, 'type': 'Feature',
'properties': {
'id': waypoint.poi_id,
'name': waypoint.name,
'iconSize': [35,35]
},
'geometry': {
'type': 'Point',
'coordinates': [waypoint.longitude, waypoint.latitude],
},
]
};
Then I add the source to the map like this :
map.addSource('waypoints', {
'type': 'geojson',
'data': waypointGeojson,
});
Then I loop around the Features to get the data and show the markers on my map.
waypointGeojson.features.forEach(function(marker) {
var radius = Number(marker.properties.iconSize[0]);
var latitude = Number(marker.geometry.coordinates[1]);
var el = document.createElement('div');
el.className = 'marker';
el.style.backgroundColor = 'rgba(230, 56, 18, 0.5)' ;
el.style.width = radius + 'px';
el.style.height = radius + 'px';
new mapboxgl.Marker(el)
.setLngLat(marker.geometry.coordinates)
.addTo(map);
}
At this step my map looks like :
Map at Zoom 15.32
However I want the Circle to be adjusted while I zoom in/zoom out.
For instance, if I zoom in :
Map at Zoom 17.32 The circle radius has not been adjusted (obviously!)
If you have any idea how I could do that with MapBox GL JS ?
I did try to use the formula (from here), with no success :
const metersToPixelsAtMaxZoom = (meters, latitude) =>
meters / 0.075 / Math.cos(latitude * Math.PI / 180)
If I use the method described here, then I put the
waypointGeojson.features.forEach(function(marker) {
var id = Number(marker.properties.id);
var radius = Number(marker.properties.iconSize[0]);
var latitude = Number(marker.geometry.coordinates[1])
map.addLayer({
"id": "circle"+id,
"type": "circle",
"source": "waypoints",
// "layout": {
// "visibility": "none"
// },
"paint": {
"circle-radius": {
stops: [
[0, 0],
[20, metersToPixelsAtMaxZoom(radius, latitude)]
],
base: 2
},
"circle-color": "red",
"circle-opacity": 0.4
}
});
});
I get 3 circles per point see here. This method is good because zoom in/out doesn't alter the size, but how to only have the corresponding circle on my point?
This is what my dataset looks like:
Seattle Crime Dataset
What I want to do is change the extrusion height based on the frequency column. I can successfully display these as points but I'm struggling with it whenever I use fill-extrusion. Can you help point me in the right direction?
map.addLayer({
'id': 'total',
'type': 'circle',
'source': {
type: 'vector',
url: 'mapbox://askakdagr8.9tklrr8g'
},
'source-layer': 'GroupedOutput-9i6ink',
'paint': {
// make circles larger as the user zooms from z12 to z22
'circle-radius': {
'base': 1.75,
'stops': [
[12, 2],
[22, 180]
]
},
'circle-color': '#ff7770'
}
});
Since the mapbox-gl-js does not currently have functionality for extruding a circle, you need to replace the points with a polygon, and interpolating the circle, for example, by a function turf.circle:
map.on('sourcedata', function(e) {
if (e.sourceId !== 'total') return
if (e.isSourceLoaded !== true) return
var data = {
"type": "FeatureCollection",
"features": []
}
e.source.data.features.forEach(function(f) {
var object = turf.centerOfMass(f)
var center = object.geometry.coordinates
var radius = 10;
var options = {
steps: 16,
units: 'meters',
properties: object.properties
};
data.features.push(turf.circle(center, radius, options))
})
map.getSource('extrusion').setData(data);
})
[ http://jsfiddle.net/zjLek40n/ ]
I want to show the marker as well as the number of points available in a particular latitude and longitude.
I expect something Similar as above.
But i have added another 2 layers to show different color and number on it. If I do this way, I am getting "undefined" error while showing popup. Since the data takes from the other layer. If it takes from layer "locations" it works as expected. But when we have multiple occurences popup content shows "undefined". Below is my implementation output and code
map.on('load', function () {
map.addSource("place", {
type: "geojson",
data: liveData,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
map.addLayer({
"id": "locations",
"type": "circle",
"source": "location",
"paint": {
"circle-radius": 7,
"circle-color": "#FFF000",
"circle-stroke-width": 4,
"circle-stroke-color": "#FFFFFF"
}
});
map.addLayer({
id: "clusters",
type: "circle",
source: "location",
filter: ["has", "point_count"],
paint: {
"circle-color": {
property: "point_count",
type: "interval",
stops: [
[0, "#FFF000"],
[2, "#DC143C"],
]
},
"circle-radius": {
property: "point_count",
type: "interval",
stops: [
[0, 7],
[2, 7],
]
}
}
});
map.addLayer({
id: "cluster-count",
type: "symbol",
source: "location",
filter: ["has", "point_count"],
layout: {
"text-field": "{point_count_abbreviated}",
"text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"],
"text-size": 12,
"text-offset": [0, 5]
}
});
map.on('click', 'locations', function (e) {
let htmlString = "";
for (let i = 0; i < e.features.length; i++) {
htmlString = htmlString + e.features[i].properties.description;
if (i != e.features.length - 1) {
htmlString = htmlString + "</br>";
}
}
new mapboxgl.Popup()
.setLngLat(e.features[0].geometry.coordinates)
.setHTML(htmlString)
.addTo(map);
});
}
My working fiddle
I want to achieve this as first picture or popup should work in my approach?
As far as I know getting all features that get combined in the cluster is currently not possible via mapbox's cluster feature.
See here: https://github.com/mapbox/mapbox-gl-js/issues/3318
I'm using mapbox-gl-js to animate many image from one coordinates to another on the map.
It become slow frame rate when I try to add up to 15 image and above.
Doing performance profile on chrome give me the hint to method Actor.receive, which cost the most computational time.
/**
* Util.js
*/
var linesDataSource = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"properties": {},
"geometry": {
"type": "LineString",
"coordinates": [
[
151.15684390068054, -33.89568424317427
],
[
151.15808844566345, -33.89606717952166
],
[
151.15779876708984, -33.89680633086413
],
[
151.15740180015564, -33.897794824453406
],
[
151.1582601070404, -33.8980085512904
],
[
151.1609423160553, -33.89863191817193
],
[
151.16222977638245, -33.89621857248702
],
[
151.16639256477356, -33.89771467675142
],
[
151.1694610118866, -33.898916884371395
],
[
151.17089867591858, -33.896298721595166
],
[
151.17217540740964, -33.899014841282515
],
[
151.1714780330658, -33.899192944468965
],
[
151.17132782936093, -33.89878330658397
],
[
151.1719822883606, -33.8985784869035
],
[
151.17339849472046, -33.89839147720036
],
[
151.17376327514648, -33.89825789858986
],
[
151.17332339286804, -33.897269410368615
],
[
151.1732053756714, -33.89697553328233
],
[
151.17341995239258, -33.89662822269281
],
[
151.17295861244202, -33.896263099778615
],
[
151.17225050926208, -33.89589797530112
],
[
151.17136001586914, -33.89561299901295
],
[
151.17184281349182, -33.894758064434605
],
[
151.17200374603271, -33.89455323508587
],
[
151.17257237434387, -33.89148073582115
],
[
151.17042660713196, -33.89132042847356
],
[
151.17168188095093, -33.88838140703873
],
[
151.1716067790985, -33.887606557247125
],
[
151.16321682929993, -33.888274531623864
],
[
151.16029858589172, -33.88777577791726
],
[
151.1591076850891, -33.88790937294604
],
[
151.15857124328613, -33.8892809364742
],
[
151.1584746837616, -33.89006467716016
],
[
151.15894675254822, -33.89009139546571
],
[
151.15893602371216, -33.889806399775104
]
]
}
}]
}
var PI = Math.PI;
var TWO_PI = Math.PI * 2;
function rotation(start, end) {
var dx = end[0] - start[0];
var dy = end[1] - start[1];
return -Math.atan2(dy, dx) * (180 / PI);
};
function lerp(v0, v1, t) {
return v0 * (1 - t) + v1 * t
}
function interpolateAngle(fromAngle, toAngle, t) {
fromAngle = fromAngle * (PI / 180);
toAngle = toAngle * (PI / 180);
fromAngle = (fromAngle + TWO_PI) % TWO_PI;
toAngle = (toAngle + TWO_PI) % TWO_PI;
var diff = Math.abs(fromAngle - toAngle);
if (diff < PI) {
return lerp(fromAngle, toAngle, t) * (180 / PI);
} else {
if (fromAngle > toAngle) {
fromAngle = fromAngle - TWO_PI;
return lerp(fromAngle, toAngle, t) * (180 / PI);
} else if (toAngle > fromAngle) {
toAngle = toAngle - TWO_PI;
return lerp(fromAngle, toAngle, t) * (180 / PI);
}
}
}
/**
* Car.js
*/
function Car(name, map, path) {
this.name = name;
this.map = map;
this.path = path;
this.speed = 90; // 30 km/h
this.accumulatedDistance = 0;
this.previousPos = this.path.features[0].geometry.coordinates[0];
this.previousAngle = 0;
this.animate = function(frameInfo) {
this.accumulatedDistance += ((frameInfo.deltaTime / 3600) * this.speed);
var point = turf.along(this.path.features[0], this.accumulatedDistance, 'kilometers');
this.map.getSource(this.name).setData(point);
var newAngle = rotation(this.previousPos, point.geometry.coordinates);
var rotate = interpolateAngle(this.previousAngle, newAngle, 0.1);
this.map.setLayoutProperty(this.name, 'icon-rotate', rotate);
this.previousAngle = rotate;
this.previousPos = point.geometry.coordinates;
};
this.init = function() {
this.map.addSource(this.name, {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": this.previousPos
}
}]
}
});
this.map.addLayer({
"id": this.name,
"type": "symbol",
"source": this.name,
"layout": {
"icon-image": "car",
"icon-size": 1,
"icon-rotate": 0,
"icon-rotation-alignment": "map"
}
});
};
}
/**
* MapBoxTest.js
*/
var destination = {};
var cars = [];
var style = 'mapbox://styles/mapbox/streets-v9'; //'/TestEmptyProject/mapbox-gl-styles-master/styles/basic-v8.json';
//'http://localhost:8080/styles/osm-bright.json'; // 'http://localhost:8080/styles/fiord-color-gl.json'
mapboxgl.accessToken = 'pk.eyJ1IjoiZW1wZXJvcjE0MTIiLCJhIjoiY2ozYTYxdXFlMDM3dzJyczRsa2M5ZjE3aCJ9.9zQGtkSsjOw6npohN6ba3w';
var map = new mapboxgl.Map({
container: 'map',
style: style,
center: [132.133333, -23.116667],
zoom: 3
});
// Used to increment the value of the point measurement against the linesData.
var counter = 0;
var linesData = {};
function addCar() {
var car = new Car("Car_" + counter, map, linesData);
car.init();
cars.push(car);
++counter;
}
var previousTimeStamp = 0;
// Add a source and layer displaying a point which will be animated in a circle.
function animate(timeStamp) {
if (timeStamp <= previousTimeStamp) {
console.log("Wrong timeStamp, now: " + timeStamp + "\t previous: " + previousTimeStamp);
return;
}
var i;
var frameInfo = {
"timeStamp": timeStamp,
"previousTimeStamp": previousTimeStamp,
"deltaTime": (timeStamp - previousTimeStamp) / 1000
};
previousTimeStamp = timeStamp;
for (i = 0; i < cars.length; ++i) {
var car = cars[i];
car.animate(frameInfo);
}
requestAnimationFrame(animate);
}
map.on('load', function() {
console.log("map load");
map.loadImage('https://maxcdn.icons8.com/office/PNG/40/Transport/railroad_car-40.png', function(error, image) {
if (error) throw error;
map.addImage('car', image);
});
//fetch('./lines.geojson', {
//method: 'get'
//}).then(function(response) {
// return response.json();
//}).then(function(data) {
linesData = linesDataSource;
var coordinates = linesData.features[0].geometry.coordinates;
var bounds = coordinates.reduce(function(bounds, coord) {
return bounds.extend(coord);
}, new mapboxgl.LngLatBounds(coordinates[0], coordinates[0]));
map.fitBounds(bounds, {
padding: 20,
duration: 2000
});
map.addSource('lines', {
"type": "geojson",
"data": linesData
});
map.addLayer({
"id": "route",
"source": "lines",
"type": "line",
"paint": {
"line-width": 2,
"line-color": "#007cbf"
}
});
// }).catch(function(err) {
//console.log("error: " + err);
//});
document.getElementById('addCar').addEventListener('click', function() {
addCar();
});
});
requestAnimationFrame(animate);
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
.overlay {
position: absolute;
top: 10px;
left: 10px;
}
.overlay button {
font: 600 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
background-color: #3386c0;
color: #fff;
display: inline-block;
margin: 0;
padding: 10px 20px;
border: none;
cursor: pointer;
border-radius: 3px;
}
.overlay button:hover {
background-color: #4ea0da;
}
<script src="https://master.fieldtec.com/vendor/custom-component-modules/car_tracking_animation/scripts/turf.min.js"></script>
<link href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.css" rel="stylesheet"/>
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.js"></script>
<body>
<div id='map'></div>
<div class='overlay'>
<button id='addCar'>Add Car</button>
</div>
</body>
Do all of your animation with one source. Update the source each frame with setData(). Render one layer from the source using data-driven styles. This will use your GPU to render animations.
This will improve performance considerably by reducing the number of layers and setData() calls.
Example code of animating in GL JS with one layer and one source: https://bl.ocks.org/ryanbaumann/9b9b52e09ff86d1ce8346fb76b681427
To animate hundreds of icons, it is more efficient to do the animation in the shaders rather than in the javascript. This allows you to leverage the power of the GPU. Here is a demo : http://misterfresh.github.io/mapbox-animation/