0

I'm trying create a webpage using open layers 7 that allows users to load a map then zoom, rotate or change the tilelayer being used as the bg.

Once day they can hit a button which should copy their map to an html canvas. I am trying to use html2canvas for this process.

So, the script works as expected for 2 of the basemaps but for the others i don't get any output when trying to copy the map to the canvas.

I believe this is an issue with a tainted canvas due to where the images are being hosted maybe? Any help on understandig what's going on and how I can fix it so I can copy any of the map backgrounds would be greatly appreciated.

HTML-

<button onclick="duplicateMap()">Copy Map</button>

<div

 id="mapBox">
        <div id="map" class="map"></div>
        <div id="rotationContainer">
            0 <input id="rotationSlider" type="range" min="0" max="360" value="0"> 360
            <br>
        </div>
        <input id="zoomSlider" type="range" orient="vertical" min="10" max="18" value="13" />
    </div>
    <div id="mapCopy"></div>
    
    <div id="baseMapSelect">
      <h4 style="margin:0px; text-decoration: underline;">Select your map style</h4>
    </div>

CSS-

    button{margin: 10px 0}
#mapBox{
    position: absolute;
    width: 40%;
}

#map {
  width: 300px;
  height: 300px;
    border: 2px solid black;
}
#mapCopy{
    position:absolute;
    top: 50px;
    left: 450px;
    width: 40%;
    float: right;
}

#baseMapSelect {
    position: relative;
  top: 330px;
}

input[type=range][orient="vertical"] {
  writing-mode: bt-lr;
  /* IE */
  -webkit-appearance: slider-vertical;
  /* Chromium */
  width: 8px;
  min-height: 175px;
  padding: 0 5px;
}

#zoomSlider {
  position: absolute;
    top: 0px;
  left: 315px;
  height: 280px;
  margin: 10px;
}

#rotationContainer{
    position: relative;
    left: 5px;
}

JS-

 window.onload = init;

// set some variables
var selectedBaseMap = "OSMStandard";
var currentLonLat = [-63.5859487, 44.648618]; //Halifax Lon/Lat
var defaultZoom = 12;
var defaultRotation = 0;
var rotationSlider = document.getElementById("rotationSlider");
var zoomSlider = document.getElementById("zoomSlider");

mapCenter = ol.proj.fromLonLat(currentLonLat); //Converts Lon/Lat to center

function init() {
  // setup the map
  const map = new ol.Map({
    view: new ol.View({
      center: mapCenter,
      zoom: defaultZoom,
      rotation: defaultRotation,
    }),
    target: "map",
  });

  // ROTATION //
  rotationSlider.oninput = function () {
    map.getView().setRotation(degreesToRads(this.value));
  };
  // ZOOM //
  zoomSlider.oninput = function () {
    map.getView().setZoom(this.value);
  };

  map.getView().on("change:rotation", function (event) {
    deg = radsToDegrees(event.target.getRotation());
    console.log(deg);
    if (deg > 360) {
      deg = deg - 360;
    }
    rotationSlider.value = deg;
  });

  /* Base Map Layer */
  const openStreetMapStandard = new ol.layer.Tile({
    source: new ol.source.OSM(),
    visible: true,
    title: "OSMStandard",
  });
  const openStreetMapHumanitarian = new ol.layer.Tile({
    source: new ol.source.OSM({
      url: "https://{a-c}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png",
    }),
    visible: false,
    title: "OSMHumanitarian",
  });
  const stamenToner = new ol.layer.Tile({
    source: new ol.source.XYZ({
      url: "https://stamen-tiles.a.ssl.fastly.net/toner/{z}/{x}/{y}.png",
      attributions:
        'Map tiles by <a href="http://stamen.com">Stamen Design</a>, \
                        under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. \
                        Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.',
    }),
    visible: false,
    title: "StamenToner",
  });
  const stamenTerrain = new ol.layer.Tile({
    source: new ol.source.XYZ({
      url: "https://stamen-tiles.a.ssl.fastly.net/terrain/{z}/{x}/{y}.jpg",
      attributions:
        'Map tiles by <a href="http://stamen.com">Stamen Design</a>, \
                        under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. \
                        Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.',
    }),
    visible: false,
    title: "StamenTerrain",
  });
  const stamenWaterColor = new ol.layer.Tile({
    source: new ol.source.XYZ({
      url: "https://stamen-tiles.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg",
      attributions:
        'Map tiles by <a href="http://stamen.com">Stamen Design</a>, \
                        under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. \
                        Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>.',
    }),
    visible: false,
    title: "StamenWatercolor",
  });

  /* End Base Map Layer */

  // Layer Group
  const baseLayerGroup = new ol.layer.Group({
    layers: [
      openStreetMapStandard,
      openStreetMapHumanitarian,
      stamenToner,
      stamenTerrain,
      stamenWaterColor,
    ],
  });

  map.addLayer(baseLayerGroup);

  // load the base maps into radio buttons for user selection
  var baseMapSelect = document.getElementById("baseMapSelect");
  baseLayerGroup.getLayers().forEach(function (element, index, array) {
    var baseLayerTitle = element.get("title");
    var html = "";
    if (baseLayerTitle == selectedBaseMap) {
      html +=
        '<input type="radio" id="baseSelect' +
        (index + 1) +
        '" class="baseLayerRadio" name="baseLayerSelect" value="' +
        baseLayerTitle +
        '" onchange="changeBaseMap(this)" checked>';
    } else {
      html +=
        '<input type="radio" id="baseSelect' +
        (index + 1) +
        '" class="baseLayerRadio" name="baseLayerSelect" value="' +
        baseLayerTitle +
        '" onchange="changeBaseMap(this)">';
    }
    html +=
      '<label for="baseSelect' +
      (index + 1) +
      '">' +
      baseLayerTitle +
      "</label>";
    var div = document.createElement("div");
    div.innerHTML = html;
    baseMapSelect.appendChild(div.firstChild);
  });

  const baseLayerElements = document.querySelectorAll(
    "#baseMapSelect > input[type=radio]"
  );
  for (let baseLayerElement of baseLayerElements) {
    //add a listener for each radio
    baseLayerElement.addEventListener("change", function () {
      let baseLayerElementValue = this.value;
      baseLayerGroup.getLayers().forEach(function (element, index, array) {
        let baseLayerTitle = element.get("title");

        // if baseLayerTitle = baseLayerElementValue then setVisible = true, else setVisible = false
        element.setVisible(baseLayerTitle === baseLayerElementValue);
      });
    });
  }
}

//update the currently selected basemap
function changeBaseMap(x) {
  selectedBaseMap = x.value;
}

function duplicateMap() {
  //create an image of map.
  html2canvas(document.getElementById("map")).then(function (canvas) {
    console.log(canvas);
    document.getElementById('mapCopy').appendChild(canvas);
  });
}

/* rotation function */
function degreesToRads(deg) {
  return (deg * Math.PI) / 180;
}
function radsToDegrees(rads) {
  return (rads * 180) / Math.PI;
}

and here is a JSFiddle with what I've got so far.

Thanks in advance!

user3182413
  • 59
  • 1
  • 7
  • If the source does not default to `crossOrigin: 'anonymous',` you need to specify it (that is the default for OSM but not XYZ). – Mike May 31 '23 at 09:25
  • thanks @Mike. I added the line `crossOrigin: 'anonymous'` in the code as follows and it still only works for the OSM maps, not XYZ): **html2canvas(document.getElementById("map"),{ crossOrigin: 'anonymous', useCORS: true, }).then(function (canvas) { console.log(canvas);`` mapCopy.appendChild(canvas); });** [Update JSFiddle](https://jsfiddle.net/chudnovskym/0nyvr532/57/) – user3182413 May 31 '23 at 10:31
  • It should be specified in `ol.source.XYZ` https://openlayers.org/en/latest/apidoc/module-ol_source_XYZ-XYZ.html – Mike May 31 '23 at 11:35
  • https://jsfiddle.net/no8g7jre/ – Mike May 31 '23 at 13:46
  • @Mike, I tried this and it's still not working. I got the same results as the Fiddle you posted above. When i add the `crossOrigin: 'anonymous'` to any of the XYZ layers they are not only not able to be copied with html2canvas, but they also don't even load those maps when the basemap is changed anymore. Thanks. – user3182413 May 31 '23 at 16:51
  • @Mike, with a little more digging I see that when I have the the `crossOrigin: 'anonymous'` set i get the following error: **Access to image at 'https://stamen-tiles.a.ssl.fastly.net/toner/12/1323/1478.png' from origin 'https://fiddle.jshell.net' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.** I've tried changing `useCors` to both true and false as well as `allowTaint` to both true and false. Any other thoughts? As always, really appreciate your help. – user3182413 May 31 '23 at 17:06
  • The fiddle is working for me. – Mike May 31 '23 at 20:41
  • weird. for me the last 3 radio buttons produce no map in the map container. I can see the zoom button and the other controls but no map data. Any thoughts on what else I could try @Mike? Thanks – user3182413 May 31 '23 at 20:47
  • I tried this again in a different browser and also on a different computer and the JSFiddle worked. So maybe my broswer has cached some bad information or something, but you're right, adding the `crossOrigin: 'anonymouos'` did solve the problem. Thanks again @Mike! – user3182413 Jun 01 '23 at 11:18

1 Answers1

0

With some help I did find the solution.

The solution was adding crossOrigin: 'anonymous' to the XYZ baselayers as it is not their default setting.

The final code now works.

Here is a working Fiddle, for those interested.

user3182413
  • 59
  • 1
  • 7