Skip to content

Ionic Angular Leaflet – performant rendering of many svg markers

I want to render many custom (svg) markers on my map. After my initial research I found a couple of approaches, but none of these seem to be applicable in my case. I’m using ionic/angular 5.0.0 and leaflet 1.7.1.

This is what I have:

displayMarkers(foos: Foo[]) {
    // Transform foos into clickable markers
    this.markers = foos.map((foo) => {
        const i = icon({
            iconUrl: '/assets/img/icon.svg', // I'd like to use this svg for my markers
            iconSize: [20, 20], // size of the icon
        });
        const marker = circleMarker([foo.lat, foo.long]); // ADD ICON HERE

        return marker.on('click', () => this.onSelectMarker(foo, marker));
    });
    // Filter markers only in bounds of the map
    this.markers = this.markers.filter(m => this.map.getBounds().contains(m.getLatLng()));
    // Draw the markers onto the map
    this.markers.forEach(marker=> marker.addTo(this.map));
}

I’d like to replace or customize the leaflet circleMarker with my svg or find a performant way to render a lot of svg elements in my map (thousands).

I know, I could use markers to display svg icons, however the performance of the application will suffer immensely, once you hit a few hundred markers.

I’d like to have the option to initialize the map like so:

new Map('mapId', {preferCanvas: true})

or be able to use a custom renderer, like so:

const marker = circleMarker([foo.lat, foo.long], {renderer: canvas()});

That way, the markers will be drawn onto the canvas and not be treated as single DOM-Elements.

I tried to implement this solution, but I was unable to integrate it in my angular-typescript application properly.

I also looked at this question and installed and tested all the suggested libraries. However the question was too open and the libraries weren’t satisfying to me and seemed to only serve a minimalistic purpose. Maybe I’m just to dumb to integrate them properly (I don’t want to loose the benefits of angular and typescript, though)…

I feel like there has to be a simple solution here, but I cannot seem to find it. Am I missing something here?

Any help is greatly appreciated. Thanks!

Answer

Ok, so after many hours of trial and error, I eventually figured it out. I used and changed the code from several answers and examples to fit my specific use case. So if anyone is curious to what I did, here it goes…

I put all my code into one file for your convenience.

map.page.ts:

@Component({
selector: 'app-map',
templateUrl: './map.page.html',
styleUrls: ['./map.page.scss'],
})
export class MapPage implements OnInit {
    map: Map; // Leaflet map
    userLocation: Marker; // Leaflet marker
    foos$: Observable<Foo[]>; // Your data
    // Some other variables ...

    constructor(
        private geocoder: NativeGeocoder,
        private fooStore: Store<fromFoo.FooState>,
        //... 
    ) {}

    ionViewDidEnter() {
        this.map = this.getInitialMap(); // Init map
        this.fooStore.dispatch(...); // Load foos to display
        this.foos$ = this.fooStore.pipe(select(fromFoo.getFoos));
        this.foos$.subscribe(foos => {
            if (foos && foos.length > 0) {
                this.displayFoos(foos);
            }
        });
        // Some more stuff here...
    }

    getInitialMap() {
        const layer = tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
            maxZoom: 19
        });
        return new Map('mapId', {
            zoomControl: !Browser.mobile,
            layers: [layer],
            center: [???, ???], // Define arbitrary location
            zoom: 19,
        });
    }

   
    displayFoos(foos: Foo[]) {
        const renderer = new Canvas(); // Important! use a canvas to render your data
        // Map the foos (your data) to leaflet markers
        const fooMarkers = foos.map((foo) => 
              new CustomMarker([foo.lat, foo.long], {renderer})
        ); // Note the CustomMarker here (See below for implementation)
        // Draw the markers onto the map
        fooMarkers.forEach(fooMarker => fooMarker.addTo(this.map));
    }

    // More functions here...
}

// This is important!
// Create a class for your custom markers that extend the CircleMarker from Leaflet
class CustomMarker extends CircleMarker {
    _updatePath() { // Make sure to name it "_updatePath()"
        // @ts-ignore
        this._renderer._displayCustomSVG(this); // Call the _renderer, which
        // to my understanding is a property on a circle marker that
        // refers to a canvas. You can extend the Canvas by your 
        // own custom render function (see below)
    }
}

const imageBitmap = new Image(); // Create a bitmap. Found on another answer
// I defined the new image outside the _displayCustomSVG to increase performance.
// That way the image instance is only created once, rather than foo.length times.

// Include ("extend") the leaflet canvas by your custom render function
Canvas.include({
    _displayCustomSVG(layer) {
        if (!this._drawing || layer._empty()) {
            return;
        }
        const p = layer._point;
        // Insert your own svg as string. (I stripped the svg here)
        const svgStr = `<svg width="10px" height="10px" viewBox="0.0 0.0 100.0 113.75853018372703" fill="none" stroke="none" stroke-linecap="square" stroke-miterlimit="10" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"></svg>`;
        // (I used an online editor to upload my svg and turn it
        // into an svg string.)
        // You might want to change the width or height of your svg
  
        imageBitmap.src = 'data:image/svg+xml;base64,' + window.btoa(svgStr);

        const ctx = this._ctx;
        imageBitmap.onload = ctx.drawImage(imageBitmap, p.x, p.y);
    },
});

This is the result: enter image description here

This works for me, however I don’t know if there’s a more performant or better way of doing this. Anyway, I hope it helps.

EDIT

I realized that if you put the const imageBitmap = new Image(); outside the _displayCustomSVG() you could run into some inconsistencies with the drawImage function from leaflet.