Reputation: 1519
I'm making an Angular4 app with leaflet maps and I need to export the current view of a map in one JPG image. Something like taking a screen shot but just the map with markers and polylines.
So, first I put markers and polylines in my leaflet map, and then I have to press a button that export the current view (including markers an polylines) in a JPG or PNG image and then ask me where to save the image.
Is there any way to do that? Some plugin that I could use?
Please help
Upvotes: 2
Views: 1507
Reputation: 21
Here is my solution without using the archived saveSvgAsPng library and without the need of adding the svg element to the DOM!
Tested it with OpenStreetMap maps, rendered using ngx-leaflet.
HTML:
<div leaflet id="map" class="map" [leafletOptions]="leafletOptions"
(leafletMapReady)="onMapReady($event)"
(leafletMapMoveEnd)="updateSvgMap()"
(leafletMouseUp)="updateSvgMap()">
</div>
Component:
map: L.Map | undefined;
leafletOptions: MapOptions = {
layers: this.getLayers(),
center: [38.8951, -77.0364],
zoom: 18
}
onMapReady(map: L.Map) {
this.map = map;
}
updateSvgMap() {
const map: L.Map | undefined = this.map;
if (!map) {
return;
}
const mapSvgElement:SVGElement = this.createSvgElement(map);
this.convertCanvasToBlob(mapSvgElement).then((blob: Blob | null) => {
console.log(blob);
}, (error: Error) => {
console.log(error);
});
}
//create svg element from Map
private createSvgElement(map: L.Map): SVGElement {
const defaultNameSpace = 'http://www.w3.org/2000/svg';
const xlinkNameSpace = 'http://www.w3.org/1999/xlink';
const mapContainerRect: DOMRect = map.getContainer().getBoundingClientRect();
const svgElement: SVGElement = document.createElementNS(defaultNameSpace,'svg');
svgElement.setAttribute('height', mapContainerRect.height.toString());
svgElement.setAttribute('width', mapContainerRect.width.toString());
//svgElement.setAttribute('id','svgMap'); //only if needed to reference it later
svgElement.setAttributeNS(xlinkNameSpace, "xlink:href", "link")
let mapTiles: NodeListOf<Element> = document.querySelectorAll('.leaflet-tile-loaded');
let markers: NodeListOf<Element> = document.querySelectorAll('.leaflet-marker-icon');
mapTiles.forEach((tile: Element) => {
const image: SVGImageElement = document.createElementNS(defaultNameSpace, 'image');
const tileRect: DOMRect = tile.getBoundingClientRect();
image.setAttribute('width', tileRect.width.toString());
image.setAttribute('height', tileRect.width.toString());
image.setAttribute('x', (tileRect.left - mapContainerRect.left).toString());
image.setAttribute('y', (tileRect.top - mapContainerRect.top).toString());
image.setAttributeNS(xlinkNameSpace, 'href', (tile as any)?.src);
svgElement.appendChild(image);
});
markers.forEach((marker: Element) => {
const image: SVGImageElement = document.createElementNS(defaultNameSpace, 'image');
const markerRect: DOMRect = marker.getBoundingClientRect();
image.setAttribute('width', markerRect.width.toString());
image.setAttribute('height', markerRect.height.toString());
image.setAttribute('x', (markerRect.left - mapContainerRect.left).toString());
image.setAttribute('y', (markerRect.top - mapContainerRect.top).toString());
image.setAttributeNS(xlinkNameSpace, 'href',(marker as any)?.src);
svgElement.appendChild(image);
});
return svgElement;
}
//convert svg with all inline images to Image-Blob
private convertCanvasToBlob(svgElement: SVGElement): Promise<Blob | null> {
return new Promise((resolve, reject) => {
const parser: DOMParser = new DOMParser();
const svgString: string = new XMLSerializer().serializeToString(svgElement);
const svgDoc: Document = parser.parseFromString(svgString, 'image/svg+xml');
const svgImage: HTMLImageElement = new Image();
svgImage.onload = () => {
const canvas: HTMLCanvasElement = document.createElement('canvas');
const context: CanvasRenderingContext2D | null = canvas.getContext('2d');
if (!context) {
return;
}
canvas.width = svgImage.width;
canvas.height = svgImage.height;
context.drawImage(svgImage, 0, 0);
//const pngBase64String: string = canvas.toDataURL('image/png');
//console.log(pngBase64String);
canvas.toBlob((blob: Blob | null) => {
resolve(blob);
}, 'image/png'); //or jpeg or webp
};
svgImage.onerror = (error: string | Event) => {
if (typeof error === 'string') {
reject(Error(error));
} else {
reject(Error('Could not load image' + error.type));
}
}
const imageElements: HTMLCollectionOf<SVGImageElement> = svgDoc.getElementsByTagName('image');
let imagesLoaded = 0;
function checkAllImagesLoaded() {
imagesLoaded++;
if (imagesLoaded === imageElements.length) {
svgImage.src = 'data:image/svg+xml;base64,' + btoa(new XMLSerializer().serializeToString(svgDoc));
}
}
for (let i: number = 0; i < imageElements.length; i++) {
const image: HTMLImageElement = new Image();
image.crossOrigin = "Anonymous";
image.onload = () => {
const canvas: HTMLCanvasElement = document.createElement('canvas');
const context: CanvasRenderingContext2D | null = canvas.getContext('2d');
if (!context) {
checkAllImagesLoaded();
return;
}
canvas.width = image.width;
canvas.height = image.height;
context.drawImage(image, 0, 0);
const imageDataURL = canvas.toDataURL('image/png');
imageElements[i].setAttributeNS('http://www.w3.org/1999/xlink', 'href', imageDataURL);
checkAllImagesLoaded();
};
image.onerror = (error: string | Event) => {
if (typeof error === 'string') {
console.log(error);
} else {
console.log('Could not load image' + error.type);
}
checkAllImagesLoaded();
}
let imageHref: string | null = imageElements[i].getAttribute('xlink:href');
if (!imageHref) {
imageHref = imageElements[i].getAttribute('href');
}
if (imageHref) {
image.src = imageHref;
}
}
});
}
Upvotes: 0
Reputation: 2258
Had the same issue, but heavy performance losses using some of the libraries to covert a dom node to an image because of html5 font downloads and processing.
I needed to tweak the excellent work from @Trash Can above. My use case was also to upload the final png as a blob.
// Save the map as a png image
async mapToBlob() {
let defaultNameSpace = 'http://www.w3.org/2000/svg';
let xlinkNameSpace = 'http://www.w3.org/1999/xlink';
let mapContainerRect = this.map.getContainer().getBoundingClientRect();
var svg = document.createElementNS(defaultNameSpace,'svg');
svg.setAttribute('height',mapContainerRect.height);
svg.setAttribute('width',mapContainerRect.width);
svg.setAttribute('id','svgMap');
svg.setAttributeNS(xlinkNameSpace, "xlink:href", "link")
let mapTiles = document.querySelectorAll('.leaflet-tile-loaded');
let markers = document.querySelectorAll('.leaflet-marker-icon');
mapTiles.forEach((tile, index) => {
const image = document.createElementNS(defaultNameSpace, 'image');
const tileRect = tile.getBoundingClientRect();
image.setAttribute('width', tileRect.width.toString());
image.setAttribute('height', tileRect.width.toString());
image.setAttribute('x', (tileRect.left - mapContainerRect.left).toString());
image.setAttribute('y', (tileRect.top - mapContainerRect.top).toString());
image.setAttributeNS(xlinkNameSpace, 'href', (tile as any)?.src);
svg.appendChild(image);
});
markers.forEach(marker => {
const image = document.createElementNS(defaultNameSpace, 'image');
const markerRect = marker.getBoundingClientRect();
image.setAttribute('width', markerRect.width.toString());
image.setAttribute('height', markerRect.height.toString());
image.setAttribute('x', (markerRect.left - mapContainerRect.left).toString());
image.setAttribute('y', (markerRect.top - mapContainerRect.top).toString());
image.setAttributeNS(xlinkNameSpace, 'href',(marker as any)?.src);
svg.appendChild(image);
});
// Hide the live map, and replace it with the SVG we want to render
this.hideLiveMap = true;
// Actually add the svg to the form (useful for debugging to see we have output)
let form = document.querySelector('#dataForm');
form.appendChild(svg);
// Create an element for the conversion library to generate png from
let svgElement = document.querySelector("#svgMap");
//await saveSvgAsPng(svgElement, "test.png");
let data = await svgAsPngUri(svgElement);
//console.log("data", data);
return data;
}
...
async save(form: NgForm) {
let pngData = await this.mapToBlob();
fetch(pngData)
.then(res => res.blob())
.then(async blob => {
...
<form (ngSubmit)="save(dataForm)" #dataForm="ngForm" class="sign_in_form" id="dataForm">
<div class="input_element" [class.hide]="viewOnly">
<div class="search_box">
<i class="fa-solid fa-magnifying-glass"></i>
<input type="text" (focus)="searchTextControlFocussed()"
#searchTextControl placeholder="Search..."
data-test-id="location-selector-search-text-input">
</div>
<div class="results-chooser" >
<div class="results-chooser-dropdown" [class.hide]="!osmSearchResults?.length || hideSearchResults"
data-test-id="location-selector-search-results">
<a *ngFor="let result of osmSearchResults; let i = index" (click)="searchResultClicked(result)" [attr.data-test-id]="'location-selector-search-result-' + i">
{{result.display_name}}
</a>
</div>
</div>
</div>
<div class="map"
leaflet
#mapContainer
[style.display]="hideLiveMap ? 'none' : 'block'"
[style.opacity]="busy? 0.5 : 1"
[(leafletCenter)]="leafletCenter"
[leafletOptions]="options"
(leafletClick)="mapClicked($event)"
(leafletMouseDown)="mapMouseDown()"
(leafletMapReady)="onMapReady($event)">
</div>
</form>
Upvotes: 0
Reputation: 6814
Here is a rough implementation, substitute in your own relevant code.
The last function saveSvgAsPng()
is from this library https://github.com/exupero/saveSvgAsPng, it allows you to save a <svg>
element into a PNG or data url
function convertToPng() {
const mapContainerRect = yourLeafletMapInstance.getContainer().getBoundingClientRect();
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const mapTiles = document.querySelectorAll('classe-of-map-tile-image');
const markers = document.querySelectorAll('classe-of-marker');
const polylines = document.querySelectorAll('polyline-element-class');
svg.setAttribute('width', mapContainerRect.width;
svg.setAttribute('height', mapContainerRect.height);
mapTiles.forEach(tile => {
const image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
const tileRect = tile.getBoundingClientRect();
image.setAttribute('width', tileRect.width);
image.setAttribute('height', tileRect.height);
image.setAttribute('x', tileRect.left - mapContainerRect.left);
image.setAttribute('y', tileRect.top - mapContainerRect.top);
image.setAttribute('xlink:href', tile.src);
svg.appendChild(image);
});
markers.forEach(marker => {
const image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
const markerRect = marker.getBoundingClientRect();
image.setAttribute('width', markerRect.width);
image.setAttribute('height', markerRect.height);
image.setAttribute('x', markerRect.left - mapContainerRect.left);
image.setAttribute('y', markerRect.top - mapContainerRect.top);
image.setAttribute('xlink:href', marker.src);
svg.appendChild(image);
});
polylines.forEach(polyline => {
const copy = polyline.cloneNode();
svg.appendChild(copy);
});
saveSvgAsPng(svg, "map.png");
}
Upvotes: 3