Sergio Mendez
Sergio Mendez

Reputation: 1519

Export Leaflet Map as JPG in typescript angular 4

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

Answers (3)

kotsy
kotsy

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

Malcolm Swaine
Malcolm Swaine

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

Trash Can
Trash Can

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

Related Questions