-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Option to keep certain styles and layers on setStyle() #4006
Comments
Thanks for this proposal @indus! Separate from the
While it's true that using a reactive paradigm and immutable objects could mostly the ergonomic and performance issues (respectively), we can't assume that this is necessarily a valid option for all/most users. Thus, I think it makes sense to provide a means for developers to short-circuit the diff for particular sources. |
Another design that may address the same problem without requiring the reintroduction of imperative concepts is something akin to React's |
As I understand it, the |
A goal of the "smart" Renaming the property to Having GL JS able to decide whether to preserve or replace a source without the user explicitly having to make this performance decision is even better. |
If we want {
sources: {
myVectorSouce: { ... },
// there used to be a geojson source here, but now it's gone
}
} to sometimes mean "make the style like this -- i.e. if there are any sources that aren't Adding something like React's
🤔 |
Whats the status now this issue? |
Any possible updates on this item? It would be very helpful to have an option to save layer states on style updates within reactive environments. |
in case it helps anyone the workaround I'm using at the moment is below. It constructs a new Style object retaining the sources and layers you want copied across to the new style. import { json } from 'd3-request';
function swapStyle(styleID) {
var currentStyle = map.getStyle();
json(`https://api.mapbox.com/styles/v1/mapbox/${styleID}?access_token=${mapboxgl.accessToken}`, (newStyle) => {
newStyle.sources = Object.assign({}, currentStyle.sources, newStyle.sources); // ensure any sources from the current style are copied across to the new style
var labelIndex = newStyle.layers.findIndex((el) => { // find the index of where to insert our layers to retain in the new style
return el.id == 'waterway-label';
});
var appLayers = currentStyle.layers.filter((el) => { // app layers are the layers to retain, and these are any layers which have a different source set
return (el.source && el.source != "mapbox://mapbox.satellite" && el.source != "composite");
});
appLayers.reverse(); // reverse to retain the correct layer order
appLayers.forEach((layer) => {
newStyle.layers.splice(labelIndex, 0, layer); // inset these layers to retain into the new style
});
map.setStyle(newStyle); // now setStyle
});
} |
I'm using a factory function to recreate a complete stylesheet everytime the configuration of layers is supposed to change; something like this: let getStyle = function (map_class) {
let layers = [];
//...
if (map_class.basemap)
layers = [...layers_base]; // from a official stylesheet
if (map_class.elevation && map_class.hillshade)
layers.push({
"id": "elevation_hillshade",
"source": "elevation_hillshade",
"type": "raster"
})
else {
map_class.elevation && layers.push({
"id": "elevation",
"source": "elevation",
"type": "raster"
})
map_class.hillshade && layers.push({
"id": "hillshade",
"source": "hillshade",
"type": "raster"
})
}
// ... 30-50 more if-statements
let style = {
version: 8,
glyphs: "http://localhost:8084/fonts/{fontstack}/{range}.pbf",
sources: sources, // from a global var
layers: layers
};
return style;
}
this.on("state_changed", function (state) {
map.setStyle(getStyle(state.map_class))
}) When I compare it to the class based system we had I still think its ugly because the stylesheet is no longer a plain JSON object but a function (and you wouldn't be able to transfer it easily to a native library). It also may be less performant (?), but at least it works for me after the deprecation and until there is some progress towards an incremental/partial setStyle |
Alternative proposal at #6701 |
the style object for Mapbox Streets has a 'sprite' property with value "mapbox://sprites/mapbox/streets-v11" My understanding is that these two different sprite values will force setStyle() to rebuild the style from scratch, since it cannot perform a diff as the underlying setSprite() is not implemented. Should one simply accept that when switching from streets to satellite a complete rebuild of the style is required when using setStyle() or is there a way to reconcile the two different sprite values? |
@ggerber One approach (out of a few) that I use is in Mapbox Studio I'd download a Streets style, and download a Satellite style, then manually concatenate the layers list and combine the sources from the style JSON, then upload that as a Style to Mapbox Studio and then upload both sets of icons to that style, so you end up with a style containing both streets and satellite but in one style with one spritesheet, then just toggle layers on/off when switching "styles". |
Unfortunatly this workaround do not work for me. Is there an other way ? |
I was able to get a work around from stack overflow working here In addition, if you've loaded any custom images such as marker/symbol images you'll have to add them back as well. It took me a few, but I figured out that you'd have to do this in the 'style.load' event, making sure you try/catch and gulp the exception for map.removeImage followed by map.addImage. I don't prefer try/catch gulping like that, but there wasn't a method like getImage that I could find to check if it exists or not in the map already. |
I had to tweak this slightly to get it to work - looks like the name of the Satellite source has changed to I also tweaked a few other things for readability. // styleID should be in the form "mapbox/satellite-v9"
async function switchBasemap(map, styleID) {
const { data: newStyle } = await axios.get(
`https://api.mapbox.com/styles/v1/${styleID}?access_token=${mapboxgl.accessToken}`
);
const currentStyle = map.getStyle();
// ensure any sources from the current style are copied across to the new style
newStyle.sources = Object.assign(
{},
currentStyle.sources,
newStyle.sources
);
// find the index of where to insert our layers to retain in the new style
let labelIndex = newStyle.layers.findIndex((el) => {
return el.id == 'waterway-label';
});
// default to on top
if (labelIndex === -1) {
labelIndex = newStyle.layers.length;
}
const appLayers = currentStyle.layers.filter((el) => {
// app layers are the layers to retain, and these are any layers which have a different source set
return (
el.source &&
el.source != 'mapbox://mapbox.satellite' &&
el.source != 'mapbox' &&
el.source != 'composite'
);
});
newStyle.layers = [
...newStyle.layers.slice(0, labelIndex),
...appLayers,
...newStyle.layers.slice(labelIndex, -1),
];
map.setStyle(newStyle);
} |
…n on load: this is more general and would allow to retain (actually readd) markers when changing map style during project editing (currently, map.setStyle(.) removes markers... mapbox/mapbox-gl-js#4006)
@stevage Do you know if your method maintains cached layers? I have tried the method were I save the source/layers from the old source and then add them to the map after the style is updated. Im wondering if by keeping the existing soruce/layers in the new style if mapbox is able to keep the layer cache. |
It seems to - the preserved layers stay visible on the map, they don't even flicker or disappear. |
Thanks for this code sample. It inspired me to get a closer to what I need where the layers keep their cache when the base map is changed. The piece you are missing is that if the sprite value is modified, mapbox will do a hard refresh on all the layers. In all my layers I dont have custom sprites, I just grab one to set as the default. Then each time I make the fetch call, I set newStyle.sprite = to the default sprite url. Im in clojure and have some helper functions but heres the gist:
|
Ah, in my particular case I have hacked the two style sheets to use the same spritesheet, using mbsprite |
Thanks @stevage for the workaround. I would suggest moving the Because axios get is awaited, there is some delay (however small) due to the external request, this introduces the possibility that the output of map.getStyle() changes before the function is finished. There is still a (extremely) small chance that an external function would modify map while the switchBasemap function is running, but for this to happen the external function needs to be called in the micro/nano seconds between the end of the axios await and |
That's a great point, thanks - updated the code. (Since I suspect people come here from Google and use that code...) |
Another thing I discovered when doing style changes is that feature-state isn't retained using the approach outlined by @stevage and @andrewharvey . So I came up with the following addition which works reasonably well, although admittedly it's a bit hacky reaching into private properties but it works.
|
Using // styleID should be in the form "mapbox/satellite-v9"
async function switchBaseMap(map, styleID) {
const response = await fetch(
`https://api.mapbox.com/styles/v1/${styleID}?access_token=${mapboxgl.accessToken}`
);
const responseJson = await response.json();
const newStyle = responseJson;
const currentStyle = map.getStyle();
// ensure any sources from the current style are copied across to the new style
newStyle.sources = Object.assign({},
currentStyle.sources,
newStyle.sources
);
// find the index of where to insert our layers to retain in the new style
let labelIndex = newStyle.layers.findIndex((el) => {
return el.id == 'waterway-label';
});
// default to on top
if (labelIndex === -1) {
labelIndex = newStyle.layers.length;
}
const appLayers = currentStyle.layers.filter((el) => {
// app layers are the layers to retain, and these are any layers which have a different source set
return (
el.source &&
el.source != 'mapbox://mapbox.satellite' &&
el.source != 'mapbox' &&
el.source != 'composite'
);
});
newStyle.layers = [
...newStyle.layers.slice(0, labelIndex),
...appLayers,
...newStyle.layers.slice(labelIndex, -1),
];
map.setStyle(newStyle);
} |
I recently extended this plugin that adds a base style switcher to automatically preserve user-added layers. It does so by keeping a dictionary of the layers that mapbox adds by default for any base style (roads, satellite, etc), and also tracking the layers/sources that are present in a style before switching, and re-added any styles that mapbox didn't add. Not sure it's the best strategy, but it really highlights the pain of user-added layers not being preserved when switching the map style- |
I really dont understand how is that not by design? This is ridiculous to handle the layers you added to the map since there could be hundreds of them and in some complicated projects its not easy to do that. At least they could provide the flag to keep the layers or discard. |
So after doing some research I came up with this solution. It works well but obviously its a workaround.
Now for each
If someone is interested in seeing it a working demo, let me know so I can add it to the codepen. |
@niZaliznyak You know you can comment here ant tag people? You dont need to make new issues on any repos 😄 |
Sorry. I didn't know 🥲 |
Here is the solution in the official documentation: https://docs.mapbox.com/mapbox-gl-js/example/style-switch/ |
The example is not a proper way to do it because they just give you an option to redraw the same layer. That not really keeping the layers created. In many cases you will have many layers with various states which might have changed during the usage and redrawing them is not an option. Also for the sake of performance is better to keep the layers you have and snap them in place instead of redrawing them all, as for example, if you have 100k markers, 10k polygons, lines etc, to redraw them it will take long time. |
@stevage |
Any updates on this one? Im having a hard time to make the symbol layers appear after switching the style, Im using the following code snippet, and my map style is a custom one, made with mapbox studio if that matters
|
@ricardohsweiss You have to add listener on style download.
|
@niZaliznyak I just found out that actually the issue is regarding the image of the symbols, not the layers itself. When we set the new style mapbox complains about missing images (that i previously loaded with loadImage and addImage functions). Anyone has an idea of how can I readd the images from one style to the other without having to add them again manually? I have images that are in the local bundle, but also some that are loaded through url. This is scattered through several places in the application, would be really helpful to just "migrate" them to the new style |
So after some research I realized that was not possible/straightforward to keep the images in the map sprite after changing the map style via setStyle, so my solution was to store all imgs in cdn, and keep track of an "imageCollection" global object containing the id and url of the images in the map. Also made some changes to the function of adding the sources and layers again in the map, as I use custom mapbox styles created in mapbox studio, I was not happy by doing a fetch or axios call to get the map style, i rather let mapbox resolve the url and fetch for me as those can change in the future. Here is my updated code with typescript if anyone is interested:
Hope this saves some time for those who had the same issue 😃 |
Motivation
Design Alternatives
#3979 current approach
#4000 alternative approach (draw-back: need for predefined nested styles)
Design
I would like to suggest an option that would allow the user to copy some layers/sources over to a new style to keep them after setting a new style.
Mock-Up
OR
Concepts
It would work like a normal setStyle but after the new style is loaded the copying from the old to the new style would take place before the map gets updated.
Implementation
the copying-code likely could get called at the end of the style-diffing procedure and could look something like that lodash-pseudocode:
The text was updated successfully, but these errors were encountered: