map/print.js

  1. 'use strict';
  2. // ugly way to add rgbcolor to global scope so it can be used by canvg inside the viewer; this is done because canvg uses UMD loader and has rgbcolor as internal dependency; there is no elegant way around it; another approach would be to clone canvg and change its loader;
  3. window.RGBColor = require('rgbcolor');
  4. const canvg = require('canvg-origin');
  5. const shared = require('../shared.js')();
  6. const XML_ATTRIBUTES = {
  7. xmlns: 'http://www.w3.org/2000/svg',
  8. 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
  9. version: '1.1'
  10. };
  11. /**
  12. * The `mapPrint` module provides map print and export image related functions.
  13. *
  14. * This module exports an object with the following functions
  15. * - `printMap`
  16. *
  17. * NOTE: unit tests might be difficult to implement as DOM is required...
  18. */
  19. /**
  20. * Generate the image from the esri print task
  21. *
  22. * @param {Object} esriBundle bundle of API classes
  23. * @param {Object} map esri map object
  24. * @param {Object} options options for the print task
  25. * url - for the esri geometry server
  26. * format - output format
  27. * width - target image height if different from default
  28. * height - target image width if different from default
  29. * @return {Promise} resolving when the print task created the image
  30. * resolve with a "response: { url: value }" where url is the path
  31. * for the print task export image
  32. */
  33. function generateServerImage(esriBundle, map, options) {
  34. // create esri print object with url to print server
  35. const printTask = esriBundle.PrintTask(options.url, { async: true });
  36. const printParams = new esriBundle.PrintParameters();
  37. const printTemplate = new esriBundle.PrintTemplate();
  38. // each layout has an mxd with that name on the server. We can modify and add new layout (mxd)
  39. // we only support MAP_ONLY for now. See https://github.com/fgpv-vpgf/fgpv-vpgf/issues/1160
  40. printTemplate.layout = 'MAP_ONLY';
  41. // only use when layout is MAP_ONLY
  42. printTemplate.exportOptions = {
  43. height: options.height || map.height,
  44. width: options.width || map.width,
  45. dpi: 96
  46. };
  47. // pdf | png32 | png8 | jpg | gif | eps | svg | svgz
  48. printTemplate.format = options.format;
  49. printTemplate.showAttribution = false;
  50. // define whether the printed map should preserve map scale or map extent.
  51. // if true, the printed map will use the outScale property or default to the scale of the input map.
  52. // if false, the printed map will use the same extent as the input map and thus scale might change.
  53. // we always use false because the output image needs to be of the same extent as the size might be different
  54. // we fit the image later because trying to fit the image with canvg when we add user added
  55. // layer is tricky!
  56. printTemplate.preserveScale = false;
  57. // set map and template
  58. printParams.map = map;
  59. printParams.template = printTemplate;
  60. // need to hide svg layers since we can generate an image for them locally
  61. const svgLayers = hideLayers(map);
  62. const printPromise = new Promise((resolve, reject) => {
  63. // can be use to debug print task. Gives parameters to call directly the print task from it's interface
  64. // http://resources.arcgis.com/en/help/rest/apiref/exportwebmap_spec.html
  65. // http://snipplr.com/view/72400/sample-json-representation-of-an-esri-web-map-for-export-web-map-task
  66. // const mapJSON = printTask._getPrintDefinition(map, printParams);
  67. // console.log(JSON.stringify(mapJSON));
  68. // TODO: catch esriJobFailed. it does not trigger the complete or the error event. Need a way to catch it!
  69. // execute the print task
  70. printTask.execute(printParams,
  71. response =>
  72. resolve(shared.convertImageToCanvas(response.url)),
  73. error =>
  74. reject(error)
  75. );
  76. });
  77. // show user added previously visible for canvg to create canvas
  78. showLayers(svgLayers);
  79. return printPromise;
  80. }
  81. /**
  82. * Set svg-based layer visibility to false to avoid CORS error
  83. *
  84. * @param {Object} map esri map object
  85. * @return {Array} layer array of layers where visibility is true
  86. */
  87. function hideLayers(map) {
  88. return map.graphicsLayerIds
  89. .map(layerId => map.getLayer(layerId))
  90. .filter(layer => layer.visible)
  91. .map(layer => {
  92. layer.setVisibility(false);
  93. return layer;
  94. });
  95. }
  96. /**
  97. * Set user added layer visibility to true for those whoe where visible
  98. *
  99. * @param {Array} layers array of graphic layers to set visibility to true
  100. */
  101. function showLayers(layers) {
  102. layers.forEach((layer) => layer.setVisibility(true));
  103. }
  104. /**
  105. * Create a canvas from the user added layers (svg tag)
  106. *
  107. * @param {Object} map esri map object
  108. * @param {Object} options [optional = null] { width, height } values; needed to get canvas of a size different from default
  109. * width {Number}
  110. * height {Number}
  111. * @param {Object} canvas [optional = null] canvas to draw the image upon; if not supplied, a new canvas will be made
  112. * @return {Promise} resolving when the canvas have been created
  113. * resolve with a canvas element with user added layer on it
  114. */
  115. function generateLocalCanvas(map, options = null, canvas = null) {
  116. canvas = canvas || document.createElement('canvas'); // create canvas element
  117. // find esri map's svg node
  118. // check if xmlns prefixes are set - they aren't; add them
  119. // without correct prefixes, Firefox and IE refuse to render svg onto the canvas; Chrome works;
  120. // related issues: fgpv-vpgf/fgpv-vpgf#1324, fgpv-vpgf/fgpv-vpgf#1307, fgpv-vpgf/fgpv-vpgf#1306
  121. const svgNode = document.getElementById(`esri\.Map_${map.id.split('_')[1]}_gc`);
  122. if (!svgNode.getAttribute('xmlns')) {
  123. Object.entries(XML_ATTRIBUTES).forEach(([key, value]) =>
  124. svgNode.setAttribute(key, value));
  125. }
  126. let originalOptions;
  127. if (options) {
  128. originalOptions = resizeSVGElement(svgNode, options);
  129. }
  130. const generationPromise = new Promise((resolve, reject) => {
  131. // parse the svg
  132. // convert svg text to canvas and stuff it into canvas canvas dom node
  133. // wrapping in try/catch since canvg has NO error handling; not sure what errors this can catch though
  134. try {
  135. // convert svg to text (use map id to select the svg container), then render svgxml back to canvas
  136. canvg(canvas, svgNode.outerHTML, {
  137. useCORS: true,
  138. ignoreAnimation: true,
  139. ignoreMouse: true,
  140. renderCallback: () => {
  141. if (options) {
  142. resizeSVGElement(svgNode, originalOptions.originalSize, originalOptions.originalViewbox);
  143. }
  144. resolve(canvas);
  145. }
  146. });
  147. } catch (error) {
  148. reject(error);
  149. }
  150. });
  151. return generationPromise;
  152. /**
  153. * Scales up or down the specified svg element. To scale it, we need to set the viewbox to the current size and change the size of the element itself.
  154. * @function resizeSVGElement
  155. * @private
  156. * @param {Object} element target svg element to be resized
  157. * @param {Object} targetSize object with target sizes in the form of { width, height }
  158. * width {Number}
  159. * height {Number}
  160. * @param {Object} targetViewbox [optional = null] target viewbox sizes in the form of { minX, minY, width, height }; if not specified, the original size will be used as the viewbox
  161. * minX {Number}
  162. * minxY {Number}
  163. * width {Number}
  164. * height {Number}
  165. * @return {Object} returns original size and viewbox of the svg element in the form of { originalSize: { width, height }, originalViewbox: { minX, minY, width, height } }; can be used to restore the element to its original state
  166. * originalSize:
  167. * width {Number}
  168. * height {Number}
  169. * originalViewbox:
  170. * minX {Number}
  171. * minxY {Number}
  172. * width {Number}
  173. * height {Number}
  174. */
  175. function resizeSVGElement(element, targetSize, targetViewbox = null) {
  176. const originalSize = {
  177. width: element.width.baseVal.value,
  178. height: element.height.baseVal.value
  179. };
  180. // get the current viewbox sizes
  181. // if the viewbox is not defined, the viewbox is assumed to have the same dimensions as the svg element
  182. // getAttribute('viewBox') returns a string in the form '{minx} {miny} {width} {height}'
  183. // setAttribute('viewBox') accepts a string in the same form
  184. const [ovMinX, ovMinY, ovWidth, ovHeight] =
  185. (element.getAttribute('viewBox') || `0 0 ${originalSize.width} ${originalSize.height}`).split(' ');
  186. const originalViewbox = {
  187. minX: ovMinX,
  188. minY: ovMinY,
  189. width: ovWidth,
  190. height: ovHeight
  191. };
  192. // set the width/height of the svg element to the target values
  193. element.setAttribute('width', targetSize.width);
  194. element.setAttribute('height', targetSize.height);
  195. // set the viewbox width/height of the svg element to the target values; or the values of the original viewbox (if the viewbox wasn't defined before, it is now)
  196. element.setAttribute('viewBox', [
  197. (targetViewbox || originalViewbox).minX,
  198. (targetViewbox || originalViewbox).minY,
  199. (targetViewbox || originalViewbox).width,
  200. (targetViewbox || originalViewbox).height
  201. ].join(' '));
  202. return {
  203. originalSize,
  204. originalViewbox
  205. };
  206. }
  207. }
  208. // Print map related modules
  209. module.exports = (esriBundle) => {
  210. return {
  211. printLocal: (map, options) => generateLocalCanvas(map, options),
  212. printServer: (map, options) => generateServerImage(esriBundle, map, options)
  213. };
  214. };