Skip to content
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

.getStyle is broken for Standard style #13160

Open
korywka opened this issue Apr 19, 2024 · 16 comments
Open

.getStyle is broken for Standard style #13160

korywka opened this issue Apr 19, 2024 · 16 comments

Comments

@korywka
Copy link
Contributor

korywka commented Apr 19, 2024

According to MapBox JS API docs, getStyle method returns Style JSON:

image

There is not any mention or hint about Standard style, for which this method returns different:

image

This makes harder to retrieve current style name or url property.
Some developers even think that Standard style doesn't have name at all: korywka/mapbox-controls#55.

Current fix looks like this:

getCurrentStyleName() {
	if (!this.map) throw Error('map is undefined');
	/** @type {string} */
	let name;
	/** @type {any} mapbox standard style doesn't return JSON object */
	const style = this.map.getStyle();
	if (Array.isArray(style.imports) && style.imports.length) {
		// mapbox standard style
		name = style.imports[0].data.name;
		console.log(style);
	} else {
		// classic style
		name = style.name;
	}
	if (!name) throw Error('style must have name');
	return name;
}

My proposal:

  • fix documentation, add information about Standard style
  • fix JSDoc for .getStyle() documentation
  • add method to get real current style source
image

let it be string for url like mapbox://styles/mapbox/satellite-streets-v12 or mapbox://styles/mapbox/standard and Object for plain JSON style.

Or suggest please another way to identify loaded style among others.

@joshkautz
Copy link

Adding a +1. This is a well-documented report. I agree that this new Standard Style introduces some breaking changes/inconsistencies. The workaround above was helpful and I'm good to go moving forward, but I expect this to cause more people some headaches.

@stepankuzmin
Copy link
Contributor

Hi everyone 👋

Thanks for reaching out and sorry for any confusion. I understand the frustration when things don't work as expected. With the new Mapbox Standard style, we're introducing a different approach to handling map styles. When you use this style in your application, we take care of updating the basemap with the latest features automatically, so your users always see the most up-to-date version without any extra effort from your side.

For the Mapbox Standard style or any "fragment" style (those with fragment: true or a schema property defined), map.getStyle() returns an empty style with no layers or sources. The original style is encapsulated in an import with the ID basemap and isn't meant to be accessed directly. This design ensures your application logic isn't dependent on style internals, which allows us to provide seamless and consistent updates.

I’ll update the related documentation to clarify this. We don't plan to add an API for accessing import internals directly. However, I'd love to understand more about your use case. @korywka, could you please share why you need to get the import's name from the style JSON?

@korywka
Copy link
Contributor Author

korywka commented May 17, 2024

Hi.
It is necessary for Style Switcher control. The control should not only to switch style, but to indicate the current selected style.
Please give a hint If it is possible to do in other way.
The code of current implementation is here: https://github.com/korywka/mapbox-controls/blob/master/packages/styles/src/index.js

@alexwohlbruck
Copy link

@stepankuzmin My use case is to be able to click on POIs on the map, and open details about them. This is impossible with the standard style at the moment because map.queryRenderedFeatures() requires a layer ID, which according to the new API, there are no layers on my map. How can we work around this?

@TimBarham
Copy link

@stepankuzmin I have the same issue as @alexwohlbruck ... map.queryRenderedFeatures() now returns nothing, which means we can't detect clicks on POIs on the map.

@Dylansc22
Copy link

@stepankuzmin My use case is to be able to click on POIs on the map, and open details about them. This is impossible with the standard style at the moment because map.queryRenderedFeatures() requires a layer ID, which according to the new API, there are no layers on my map. How can we work around this?

Exact same functionality I'm trying to achieve @alexwohlbruck and @TimBarham. Hoping for a fix.

If I understand correctly, you can't really do anything with imported layers found in map.getStyle().imports[0].data.layers

For example 👇

const someImportedPOILayer = map.getStyle().imports[0].data.layers[3]
map.removeLayer(someImportedLayer.id)
// returns error -- "The layer does not exist in the map's style." as it seems to be pointing to the empty array of map.getStyle().layers

I was thinking I could hack a solution by taking the imported layer and doing a map.addSource(...) & map.addLayer(...) to re-add the imported layer again and hopefully have it populate in the map.getStyle().sources and map.getStyle().layers
I think that would enable map.queryRenderedFeatures(...) to work, but I haven't quite figured out the code for it.

Still somewhat confused on "import layers", "composite sources" "sourceLayers" and "sources" through all of this... 😵

@alexwohlbruck
Copy link

alexwohlbruck commented Jul 26, 2024

^this actually does work. you can add only the POI layer from the standard style after map loads, then it can be made clickable:

map.addSource('composite', {
  type: 'vector',
  url: 'mapbox://mapbox.mapbox-bathymetry-v2,mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2,mapbox.mapbox-models-v1',
})
map.addLayer({
  "id": "poi-label",
  "type": "symbol",
  "source": "composite",
  "source-layer": "poi_label",
  "minzoom": 6,
  "filter": [
      {truncated for clarity, find the actual code in the mapbox standard style json or my edit history}
    ]
  }
})

map.on('click', e => {
  const selectedFeatures = map.queryRenderedFeatures(e.point)
  if (selectedFeatures.length) {
    alert(`You clicked ${selectedFeatures[0].properties?.name}`)
  }
})

of course, this means any updates to that layer made by mapbox will have to be manually replaced. also, there will be two duplicate POI layers rendered, but visually you can't tell any difference

another solution which is a one-liner, but breaks all the map config options such as POI toggling and dynamic lighting:

map.style._isInternalStyle = () => false

neither of these options are ideal, and I would really appreciate a proper API for accessing this data

@Dylansc22
Copy link

Dylansc22 commented Jul 28, 2024

^this actually does work. you can add only the POI layer from the standard style after map loads, then it can be made clickable:

It does work (almost)! but all my custom icon's are missing. (you or someone else knows how to resolve 🤞)

This is because even though addSource(...) and addLayer(...) has added added the POI layer appropriately (ie. map.queryRenderFeatures(...) now works for that layer), the icon-image property for that layer doesn't show my custom icons. I believe this is due to sprites currently being referenced from map.getStyle().imports[0].data.sprite but they need to be added and referenced via map.getStyle().sprites (which is currently just undefined)

I tried the following which loads the sprite images, but reloading the style like this just breaks other things in the standard style👇

const style = map.getStyle();
style.sprite = 'mapbox://sprites/<my_user_name>/<my_sprite_style_url>'
map.setStyle(style);

neither of these options are ideal, and I would really appreciate a proper API for accessing this data

Agreed. Love the new standard style features.(3d / lighting / shadows / trees / etc...) but it has made basic tasks challenging.

@alexwohlbruck
Copy link

Sprites might have to be loaded separately, I don't totally know how they work. Download the style.json from your network panel and you can see how it is all working

@Dylansc22
Copy link

Dylansc22 commented Jul 28, 2024

Sprites might have to be loaded separately, I don't totally know how they work. Download the style.json from your network panel and you can see how it is all working

Yea that's the plan, thank you! staying optimistic 😐 👍

Edit: solved it (or well "hacked it" into working)
In order to use the sprite images (which currently only exist on the import) you need to add the default (non-imported) reference location on the imageManager so any map layers added via map.addLayer(...) know where to find the spirte images:
map.style.imageManager.images[""] = map.style.imageManager.images["basemap"]

@Dylansc22
Copy link

Dylansc22 commented Jul 29, 2024

Solved it! (or well "hacked it" into working). Hoping for a better longterm solution from the Mapbox Team though

To summarize for others

Issue: When displaying a map using the new Standard Style (ie. style: "mapbox://styles/mapbox/standard"), you cannot access individual layers of the map the way you could in their classic map styles. This makes many mapbog-gl.js API methods not functional (eg. map.removeLayer('some_layer') or map.queryRenderedFeatures(e.point, ...) as discussed above)

why issue is happening: As I understand via my own project - this is because the new Standard Style Map relies on loading the standard style via Imports to reference all map layers. This is done for backend reasons (I believe keeping all data such as POIs and roads up to date). But because the the layers are not stored in map.getStyle().layers but map.getStyle().imports[0].data.layers the layers are "not available through runtime APIs". Another reason they do it this way is because the new Standard style uses Schemas which setup various preset colorschemes (?) but also make it where you "cannot access individual layers". This is evident since source: {} and layers: [] are empty as illustrated in the initial post. I'm sure there is more nuance to my above summary - but finding this information is not abundantly clear in the docs.

hack solution to get map.queryRenderedFeatures(...) to work on Standard Style by re-adding the layer again so that API methods can access it. This works but you are just redrawing the layer on-top of itself layer, but this one is no longer in imports.

// Before re-adding the source/layer, fix the sprite images' reference 
// so any layer that is added can use the icons that come from the imported map style
// Without this, symbol-layers will not properly show `icon-image` if that layout property is set 
map.style.imageManager.images[""] = map.style.imageManager.images.basemap

// Re-add the source 
map.addSource('composite', {
  type: 'vector',
  url: 'mapbox.mapbox-streets-v8' // because I need poi-label layer
  // url: 'mapbox://mapbox.mapbox-bathymetry-v2,mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2,mapbox.mapbox-models-v1',
})

// Re-add the layer
map.addLayer({
  "id": "poi-label",
  "type": "symbol",
  "source": "composite",
  "source-layer": "poi_label",
  "minzoom": 6,
  "filter": [
      {truncated for clarity, find the actual code in the mapbox standard style json or my edit history}
    ]
  }
})

// add click behavior to the new layer
map.on('click', e => {
  const selectedFeatures = map.queryRenderedFeatures(e.point)
  if (selectedFeatures.length) {
    alert(`You clicked ${selectedFeatures[0].properties?.name}`)
  }
})

@chunlampang
Copy link

Here is another hack solution to control the Standard style's layers
#12950 (comment)

@Dylansc22
Copy link

Here is another hack solution to control the Standard style's layers #12950 (comment)

Nice! Yes! Your solution seems similar to my work-around for updating layers in the Standard style. =]

let style = map.getStyle()
style.imports[0].data.layers.find(e => e.id === "some-layer-id").layout["text-field"] = "I'm the Updated Text!"
map.setStyle(style)

Not sure if there are benifits to method vs mine! 🤔

@chunlampang
Copy link

chunlampang commented Aug 8, 2024

Here is another hack solution to control the Standard style's layers #12950 (comment)

Nice! Yes! Your solution seems similar to my work-around for updating layers in the Standard style. =]

let style = map.getStyle()
style.imports[0].data.layers.find(e => e.id === "some-layer-id").layout["text-field"] = "I'm the Updated Text!"
map.setStyle(style)

Not sure if there are benifits to method vs mine! 🤔

My method will not trigger style.load, which means it will not reload everything.

@RikidWai
Copy link

Hi all, I was trying to make the labels on standard style map clickable and trying to follow the solution provided here. It was really frustrated that I can't get this simple queryRenderedFeatures function to work. Unfortunately, I got this error when copying your solution. May I have if you have encountered similar error? I tried to change composite but seems no luck. Any help is appreciated.

Unhandled Runtime Error
Error: There is already a source with ID "composite".
'use client';

import 'mapbox-gl/dist/mapbox-gl.css';

import React, { useEffect, useRef, useState } from 'react';
import type { MapRef } from 'react-map-gl';
import MAP, { FullscreenControl, GeolocateControl, NavigationControl, ScaleControl } from 'react-map-gl';

import { or } from 'drizzle-orm';
import mapbox from 'react-map-gl/dist/esm/mapbox/mapbox';
import { json } from 'stream/consumers';
import style from 'styled-jsx/style';

const MAPBOX_STYLE = 'mapbox://styles/mapbox/standard';

const MapboxMap: React.FC = () => {
  const mapRef = useRef<MapRef>(null);
  const map = mapRef.current?.getMap();
  const geoControlRef = useRef<mapboxgl.GeolocateControl>(null);
  const [viewport, setViewport] = useState({
    latitude: 22.3193,
    longitude: 114.1694,
    zoom: 3,
    width: '100%',
    height: '100vh',
  });

  useEffect(() => {
    if ('geolocation' in navigator) {
      navigator.geolocation.getCurrentPosition(
        (position) => {
          setViewport(prevViewport => ({
            ...prevViewport,
            latitude: position.coords.latitude,
            longitude: position.coords.longitude,
          }));
        },
        error => console.error('Error getting location:', error),
        { enableHighAccuracy: true },
      );
    }
  });

  // map.style.imageManager.images[""] = map.style.imageManager.images.basemap

  // Re-add the source
  if (map) {
    map.addSource('composite', {
      type: 'vector',
      url: 'mapbox.mapbox-streets-v8', // because I need poi-label layer
      // url: 'mapbox://mapbox.mapbox-bathymetry-v2,mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2,mapbox.mapbox-models-v1',
    });

    // Re-add the layer
    map.addLayer({
      "id": "poi-label",
      "type": "symbol",
      "source": "composite",
      "source-layer": "poi_label",
      "minzoom": 6,
      "filter": [
          {truncated for clarity, find the actual code in the mapbox standard style json or my edit history}
        ]
      }
    });

    // add click behavior to the new layer
    map.on('click', (e) => {
      const selectedFeatures = map.queryRenderedFeatures(e.point);
      if (selectedFeatures.length) {
        alert(`You clicked ${selectedFeatures[0].properties?.name}`);
      }
    });
  }

  return (
    <div className="h-screen w-screen">
      <MAP
        initialViewState={viewport}
        mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN}
        mapStyle={MAPBOX_STYLE}
        ref={mapRef}
        attributionControl={false}
        projection={{
          name: 'globe',
        }}
        onLoad={() => {
          geoControlRef.current?.trigger();
        }}
      >
        <GeolocateControl
          position="bottom-right"
          ref={geoControlRef}
          showUserLocation
          fitBoundsOptions={{ maxZoom: 8.5 }}
        />
        <FullscreenControl position="bottom-right" />
        <NavigationControl position="bottom-right" />
        <ScaleControl />
        <CustomMarker
          latitude={22.3193}
          longitude={114.1694}
          category="restaurant"
        />
      </MAP>
    </div>
  );
};

export default MapboxMap;

@alexwohlbruck
Copy link

looks like there is some hope for those of us who are looking to add click interactions to the built in standard style layers!

#13332 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants