Search code examples
ms-wordoffice-jsopenlayersoffice-addinsword-addins

Use OpenLayers in a Word Addin (Office.js)


I have been working on this for a few days now, but I cannot get it to work.

I want to use OpenLayers to insert a map in my word-document through a Word Addin. On the site of OpenLayers, a simple example can be found of how to generate a map. To insert the map a few things have to be done:

  • Generate the map
  • Convert the map to a base64 image
  • Insert the image in the document

In the end, I would like the function to work in my command.ts, but because debugging is hard when using the UI-less part of the addIn, I decided I would first use the task.ts so I could see what happens under the hood.

So I wrote the next function:

function initMap() {
  return new Map({
    target: "map",
    layers: [
      new TileLayer({
        source: new OSM(),
      }),
    ],
    view: new View({
      center: fromLonLat([7.0785, 51.4614]),
      zoom: 4,
    }),
  });
}

On the task.html page I have got a div with ID "map". I run the initMap() when I click a button on the task.html page. The map then shows up in the DIV. So far so good.

Then I have code that converts the inserted map to a base64 image. But that part isn't working. The code I use comes from the example of the generated map on the OL site.

 let map = initMap();

    var dataURL = "";

    const mapCanvas = document.createElement("canvas");
    const size = map.getSize();
    mapCanvas.width = size[0];
    mapCanvas.height = size[1];
    const mapContext = mapCanvas.getContext("2d");

    console.log("Get Canvas");
    //console.log(map.getViewport().innerHTML);

    Array.prototype.forEach.call(
      map.getViewport().querySelectorAll(".ol-layer canvas, canvas.ol-layer"),
      (canvas) => {
        console.log(canvas.height);
        if (canvas.width > 0) {
          const opacity = canvas.parentNode.style.opacity || canvas.style.opacity;
          // mapContext.globalAlpha = opacity === "" ? 1 : Number(opacity);

          const backgroundColor = canvas.parentNode.style.backgroundColor;
          if (backgroundColor) {
            mapContext.fillStyle = backgroundColor;
            mapContext.fillRect(0, 0, canvas.width, canvas.height);
          }

          let matrix;
          const transform = canvas.style.transform;
          const transformMatch = transform.match(/^matrix\(([^(]*)\)$/);

          if (transformMatch && transformMatch.length > 1) {
            // Get the transform parameters from the style's transform matrix and apply it to the export map context
            const matrix = transformMatch[1].split(",").map(Number);

            mapContext.setTransform(...matrix);
          }
          mapContext.drawImage(canvas, 0, 0);
        }
      }
    );

    mapContext.globalCompositeOperation = "destination-over";
    mapContext.fillStyle = "white";
    mapContext.fillRect(0, 0, mapCanvas.width, mapCanvas.height);
    mapContext.globalAlpha = 1;
    dataURL = mapCanvas.toDataURL("image/png", 1);

var base64Image = dataURL.replace("data:image/png;base64,", "");

this code is tested in a regular HTML page. There it works. But in the AddIn it doesn't. I use MS Edge Developer Tools to monitor the process. I can see the map is generated. I can even see the layers that are generated by OL:

enter image description here

A div with class ol-layer is present and within this div the canvas-element is present. The task.html looks like this (after clicking the run button):

enter image description here

The problem is that:

 Array.prototype.forEach.call(
          map.getViewport().querySelectorAll(".ol-layer canvas, canvas.ol-layer"),

cannot find the div called ol-layer and the canvas element. I tested this using console.log(canvas.width) (see code above). I also tried console.log(map.getViewport().length) which results in the value 0. Another thing I tried is console.log(map.getViewport().innerHTML). But then I see the Divs generated except for the ol-layer div and the canvas-element. These are not shown when using the innerHTML function. But that is maybe because these elements are children of another Div (the Div with class "ol-unselectable ol-layers" in the image).

So I end up without an image.

A base64 is generated but it does not contain the map. It is just an image with a white background. Inserting the image is done using the word document function:

var image = body.insertInlinePictureFromBase64(base64Image, Word.InsertLocation.start);

Does anyone have suggestions on how to solve this?


Solution

  • With the help of a colleague, this issue was solved!!!

    We got it working in the takspane.ts and used the same code in the command.ts, where it should be in the final solution.

    The code used in the command.ts:

    async function insertMapImage(dataURL: string) {
      return Word.run(async (context) => {
        var base64Image = dataURL.replace("data:image/png;base64,", "");
        let selectionRange = context.document.getSelection();
        let paragraph = selectionRange.insertParagraph("", Word.InsertLocation.before);
        paragraph.alignment = Word.Alignment.centered;
        let image = paragraph.insertInlinePictureFromBase64(base64Image, Word.InsertLocation.end);
        image.lockAspectRatio = false;
    
        await context.sync();
      });
    }
    
    function initMap() {
      return new Map({
        target: "map",
        layers: [
          new TileLayer({
            source: new OSM(),
          }),
        ],
        view: new View({
          center: fromLonLat([7.0785, 51.4614]),
          zoom: 4,
        }),
      });
    }
    
    // This is the function that is referenced in the manifest.
    async function createMap(event: Office.AddinCommands.Event) {
      return Word.run(async (context) => {
        let map = initMap();
        var dataURL = "";
        map.once("rendercomplete", function () {
          const mapCanvas = document.createElement("canvas");
          const size = map.getSize();
          mapCanvas.width = size[0];
          mapCanvas.height = size[1];
          const mapContext = mapCanvas.getContext("2d");
    
          console.log("Get Canvas");
          //console.log(map.getViewport().innerHTML);
          Array.prototype.forEach.call(
            map.getViewport().querySelectorAll(".ol-layer canvas, canvas.ol-layer"),
            (canvas) => {
              console.log(canvas.height);
              if (canvas.width > 0) {
                const opacity = canvas.parentNode.style.opacity || canvas.style.opacity;
                // mapContext.globalAlpha = opacity === "" ? 1 : Number(opacity);
    
                const backgroundColor = canvas.parentNode.style.backgroundColor;
                if (backgroundColor) {
                  mapContext.fillStyle = backgroundColor;
                  mapContext.fillRect(0, 0, canvas.width, canvas.height);
                }
    
                let matrix;
                const transform = canvas.style.transform;
                const transformMatch = transform.match(/^matrix\(([^(]*)\)$/);
    
                if (transformMatch && transformMatch.length > 1) {
                  // Get the transform parameters from the style's transform matrix and apply it to the export map context
                  const matrix = transformMatch[1].split(",").map(Number);
    
                  mapContext.setTransform(...matrix);
                }
                mapContext.drawImage(canvas, 0, 0);
              }
            }
          );
    
          mapContext.globalCompositeOperation = "destination-over";
          mapContext.fillStyle = "white";
          mapContext.fillRect(0, 0, mapCanvas.width, mapCanvas.height);
          mapContext.globalAlpha = 1;
    
          dataURL = mapCanvas.toDataURL("image/png", 1);
          insertMapImage(dataURL);
        });
    
        map.renderSync();
        await context.sync();
      });
    
      event.completed();
    }
    

    Also, on the command.html a DIV with an ID has to be present. The ID in my case is called map (<DIV Id="map" class="map"></DIV>).

    So what I missed:

    In the function CreateMap, I didn't use map.once("rendercomplete") that is basically why an image was not shown.

    Hope this information will be useful for others. This question can be closed.