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

Feature Request: SVG symbols (via addImage) #5529

Closed
krabbypattified opened this issue Oct 26, 2017 · 13 comments
Closed

Feature Request: SVG symbols (via addImage) #5529

krabbypattified opened this issue Oct 26, 2017 · 13 comments

Comments

@krabbypattified
Copy link

Motivation

Use with Webpack (for modern workflows)

import mySVG from './foo.svg'
map.addImage(mySvg) // or similar

This isn't a great solution:
Can we use svg symbols? #1577

And this isn't either (yuck, callbacks).

mapbox-gl-js version: v0.41.0

@krabbypattified
Copy link
Author

Also I'd like to note that the docs say addImage accepts an HTMLImageElement, but I haven't been able to get this to work.

I get this error: Cannot read property 'sdfIcons' of undefined

@pathmapper
Copy link
Contributor

pathmapper commented Oct 26, 2017

@krabbypattified did you try mapboxgl.Marker() ?

https://www.mapbox.com/mapbox-gl-js/api/#marker

The following tutorial works also with SVG:
https://www.mapbox.com/help/building-a-store-locator/#add-custom-markers

@krabbypattified
Copy link
Author

@pathmapper yes I have tried Markers. I need to animate it so I even jerry-rigged an animation mechanism for it. The problem is that on mobile it gets slow and the markers lag behind the rest of the map. Basically all I’m trying to accomplish is this demo:
https://www.mapbox.com/mapbox-gl-js/example/animate-point-along-route/
Except with my own SVGs, not an airplane.

@Scarysize
Copy link

You need to rasterize your SVG so it can be used as an icon-image. You can do this by rendering the SVG into a canvas:
https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Drawing_DOM_objects_into_a_canvas

Then you should be able to get the image data from the canvas 2d context:
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getImageData

map.addImage allows you to pass this ImageData instead of an HTMLImageElement.

@jfirebaugh
Copy link
Contributor

jfirebaugh commented Oct 26, 2017

Passing an HTMLImageElement (or ImageData) is the API we're likely to support here. In fact, it should already work. Are you sure you're not using a build from source more recent than the official v0.41.0 release? The error you're getting looks like #5516, which was introduced after 0.41.0.

@krabbypattified
Copy link
Author

Thanks @jfirebaugh! There was something srewy with my version (i was using a fork).
Now it's 4 lines of code and voila:

import mySVG from 'foo.svg'
let img = new Image(20,20)
img.onload = ()=>map.addImage('bus', img)
img.src = mySVG

@gabimoncha
Copy link

gabimoncha commented Jun 4, 2018

@krabbypattified how do you add the image to the layer?
I tried your solution different ways but I get the same error message:

index.js:2178 Error: An image with this name already exists.
    at i.addImage (mapbox-gl.js:32)
    at n.addImage (mapbox-gl.js:32)
    at Image.img.onload (Component.jsx:11)

And the layers don't show.

Here is my code:

let img = new Image(15, 15);
img.onload = () => map.addImage("girls", img);
img.src = girlsIcon;

map.addLayer({
     id: id,
     type: "symbol",
     source: {
        type: type,
        url: url
     },
     "source-layer": sourceLayer,
     layout: {
        visibility: visibility,
        "icon-image": "girls", <I also tried with "girlsIcon" and "img.src">
        "icon-allow-overlap": true,
        "icon-size": 0.95
     }
})

@bdirito
Copy link

bdirito commented Nov 5, 2018

Ive come across the behavior as described by @gabrielmoncea (especially in chrome) and think ive figured out whats going on.

The issue is that image.src is fundamentally async. Thus by the time you try to run map.addImage the svg may (or may not be) rastered. The solution is to wait on the addImage until the image is actually there. My workaround looks something like

import { BehaviorSubject } from 'rxjs'
import { first } from 'rxjs/operators'

const setupMarkers = async () => {
    const image = new Image(50, 55)
    const imageLoaded = new BehaviorSubject(false);
    image.addEventListener('load', () => { imageLoaded.next(true) }, { once: true })
    image.src = svgData
    await imageLoaded.pipe(first()).toPromise()

    map.addImage('exampleSvg', image)
}

@bdirito
Copy link

bdirito commented Nov 5, 2018

#6231 may be related

@theprojectsomething
Copy link

Had a similar issue on Firefox v65.

Turns out Firefox currently requires both a width and height to be explicitly defined on the SVG in order for it to render correctly with ctx.drawImage (used internally by map.addImage) ... updating any SVGs with the dimensions of the image (on the fly or otherwise) resolves the issue.

Perhaps a little bit overkill, but Mapbox-gl could attempt to resolve this internally by type-checking the src of a passed HTMLImageElement and updating with width & height attributes where appropriate. Or warning when the resultant image data is fully transparent, e.g.

const isTransparent = (data /* canvasImageData */) => {
  const len = data.length / 4; // insert fancy accuracy factor here
  for(let i = 0; i < len; i += 1) {
    // insert fancy inside-out iterator here
    if (data[(i * 4) + 3]) { // check opacity is larger than zero
      return false;
      break;
    }
  }
  return true;
};
if (isTransparent(imgData)) console.warn('Your symbol is invisible!');

@kalzoo
Copy link

kalzoo commented Jun 20, 2019

I modified @bdirito's excellent solution to remove the dependency on rxjs, and it's working well for me. The first method generates the ImageData from an external path, and the second generates the string for an inline image src.

export const svgPathToImage = ({ path, width, height }: { path: string; width: number; height: number }) =>
  new Promise(resolve => {
    const image = new Image(width, height);
    image.addEventListener('load', () => resolve(image));
    image.src = path;
  });

// This does a lookup of the symbol's name to find its string value
export const symbolAsInlineImage = ({ name, width, height }: { name: string; width: number; height: number }) =>
  svgPathToImage({ path: 'data:image/svg+xml;charset=utf-8;base64,' + btoa(symbols[name]), width, height });

I'm using react-mapbox-gl, and having this promise resolve once the image is loaded lets me delay rendering of the layer until that's complete.

@YenHub
Copy link

YenHub commented Jan 11, 2021

Thanks @jfirebaugh! There was something srewy with my version (i was using a fork).
Now it's 4 lines of code and voila:

import mySVG from 'foo.svg'
let img = new Image(20,20)
img.onload = ()=>map.addImage('bus', img)
img.src = mySVG

SOLVED:- This is the best solution I have found - thanks so much for sharing the outcome of your journey!

In my instance, I needed to load "many" svgs, so simply created a map of them and ran this snippet in a loop:-

// CustomIcons.js
import CustSVG from './CustSVG';
import CustBlueSVG from './CustBlueSVG';
const CustomIcons = [
    {
        src: CustSVG,
        name: 'custom-svg'
    },
    {
        src: CustBlueSVG,
        name: 'custom-blue-svg'
    }
];

export default CustomIcons;

Then I import these, and use them within the useEffect hook of my Component:

// MapComponent.js
import CustomIcons from './CustomIcons';

function MapComponent() {

    // ...

    useEffect( () => {
        map.on('load', function () {
    
            CustomIcons.forEach(icon => {
                const customIcon = new Image(24, 24);
                customIcon.onload = () => map.addImage(icon.name, customIcon)
                customIcon.src = icon.src;
            });
    
            // ... the rest of your map code that uses the SVGs
        });
    }, []);

}

Thanks again, super helpful!

@maxy4u
Copy link

maxy4u commented Sep 27, 2022

I modified @bdirito's excellent solution to remove the dependency on rxjs, and it's working well for me. The first method generates the ImageData from an external path, and the second generates the string for an inline image src.

export const svgPathToImage = ({ path, width, height }: { path: string; width: number; height: number }) =>
  new Promise(resolve => {
    const image = new Image(width, height);
    image.addEventListener('load', () => resolve(image));
    image.src = path;
  });

// This does a lookup of the symbol's name to find its string value
export const symbolAsInlineImage = ({ name, width, height }: { name: string; width: number; height: number }) =>
  svgPathToImage({ path: 'data:image/svg+xml;charset=utf-8;base64,' + btoa(symbols[name]), width, height });

I'm using react-mapbox-gl, and having this promise resolve once the image is loaded lets me delay rendering of the layer until that's complete.
Hi @kalzoo Could you please share the link to your code i would like to see the usage. In my case i am getting svg from material ui and would like to use it as a image layer in my react map gl.

Thanks in advance.

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

10 participants