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!
Advertisement
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: '© <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 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.