/* * Copyright (c) 2008-2015 The Open Source Geospatial Foundation * * Published under the BSD license. * See https://github.com/geoext/geoext2/blob/master/license.txt for the full * text of the license. */ /* * @include OpenLayers/Format/JSON.js * @include OpenLayers/Format/GeoJSON.js * @include OpenLayers/Util.js * @include OpenLayers/Geometry/Point.js * @include OpenLayers/Feature/Vector.js * @include OpenLayers/Layer/Vector.js * @requires GeoExt/Version.js */ /** * Provides an interface to a Mapfish or GeoServer print module. For printing, * one or more instances of {@link GeoExt.data.PrintPage} are also required * to tell the PrintProvider about the scale and extent (and optionally * rotation) of the page(s) we want to print. * * Minimal code to print as much of the current map extent as possible as * soon as the print service capabilities are loaded, using the first layout * reported by the print service: * * Example: * * var mapPanel = Ext.create('GeoExt.panel.Map', { * renderTo: "mappanel", * layers: [new OpenLayers.Layer.WMS("wms", "/geoserver/wms", * {layers: "topp:tasmania_state_boundaries"})], * center: [146.56, -41.56], * zoom: 7 * }); * var printProvider = Ext.create('GeoExt.data.MapfishPrintProvider', { * url: "/geoserver/pdf", * listeners: { * "loadcapabilities": function() { * var printPage = Ext.create('GeoExt.data.PrintPage', { * printProvider: printProvider * }); * printPage.fit(mapPanel, true); * printProvider.print(mapPanel, printPage); * } * } * }); * * @class GeoExt.data.MapfishPrintProvider */ Ext.define('GeoExt.data.MapfishPrintProvider', { extend: 'Ext.util.Observable', requires: [ 'Ext.data.JsonStore', 'GeoExt.Version' ], /** * Base url of the print service. Will always have a trailing "/". * * @private * @property {String} url */ url: null, /** * If set to true, the capabilities will be loaded upon instance creation, * and `loadCapabilities` does not need to be called manually. Setting this * when `capabilities` and no `url` is provided has no effect. Default is * false. * * @cfg {Boolean} autoLoad */ /** * Capabilities of the print service. Only required if `url` * is not provided. This is the object returned by the `info.json` * endpoint of the print service, and is usually obtained by including a * script tag pointing to http://path/to/printservice/info.json?var=myvar * in the head of the html document, making the capabilities accessible as * `window.myvar`. * * This property should be used when no local print service or proxy is * available, or when you do not listen for the `loadcapabilities` * events before creating components that require the PrintProvider's * capabilities to be available. * * @cfg {Object} capabilities */ /** * Capabilities as returned from the print service. * * @private * @property {Object} capabilities */ capabilities: null, /** * Either `POST` or `GET` (case-sensitive). Method to use when sending print * requests to the servlet. If the print service is at the same origin as * the application (or accessible via proxy), then `POST` is recommended. * Use `GET` when accessing a remote print service with no proxy available, * but expect issues with character encoding and URLs exceeding the maximum * length. Default is `POST`. * * @cfg {String} method */ /** * Either `POST` or `GET` (case-sensitive). Method to use when sending print * requests to the servlet. * * @property {String} method * @private */ method: "POST", /** * The encoding to set in the headers when requesting the print service. * Prevent character encoding issues, especially when using IE. Default is * retrieved from document `charset` or `characterSet` if existing * or `UTF-8` if not. * * @cfg {String} encoding */ encoding: document.charset || document.characterSet || "UTF-8", /** * Timeout of the POST Ajax request used for the print request (in * milliseconds). Default of 30 seconds. Has no effect if `method` is set to * `GET`. * * @cfg {Number} timeout */ timeout: 30000, /** * Key-value pairs of custom data to be sent to the print service. This is * e.g. useful for complex layout definitions on the server side that * require additional parameters. Optional. * * @property {Object} customParams */ customParams: null, /** * Key-value pairs of base params to be add to every request to the service. * Optional. * * @cfg {Object} baseParams */ /** * Read-only. A store representing the scales available. * * Fields of records in this store: * * * name - `String` the name of the scale * * value - `Float` the scale denominator * * @property {Ext.data.JsonStore} scales */ scales: null, /** * Read-only. A store representing the dpis available. * * Fields of records in this store: * * * name - `String` the name of the dpi * * value - `Float` the dots per inch * * @property {Ext.data.JsonStore} dpis */ dpis: null, /** * Read-only. A store representing the output formats available. * * Fields of records in this store: * * * name - `String` the name of the output format * * @property {Ext.data.JsonStore} outputFormats */ outputFormats: null, /** * Read-only. A store representing the layouts available. * * Fields of records in this store: * * * name - `String` the name of the layout * * size - `Object` width and height of the map in points * * rotation - `Boolean` indicates if rotation is supported * * @property {Ext.data.JsonStore} layouts */ layouts: null, /** * The record for the currently used resolution. Read-only, use `#setDpi` to * set the value. * * @property {Ext.data.Record} dpi */ dpi: null, /** * The record of the currently used output format. Read-only, use * `#setOutputFormat` to set the value. * * @property {Ext.data.Record} outputFormat */ outputFormat: null, /** * The record of the currently used layout. Read-only, use `#setLayout` to * set the value. * * @property {Ext.data.Record} layout */ layout: null, /** * Triggered when the capabilities have finished loading. This * event will only fire when `#capabilities` is not configured. * * Listener arguments: * * * printProvider - {@link GeoExt.data.MapfishPrintProvider} this * PrintProvider. * * capabilities - `Object` the capabilities. * * @event loadcapabilities */ /** * Triggered when the output format is changed. * * Listener arguments: * * * printProvider - {@link GeoExt.data.MapfishPrintProvider} this * PrintProvider. * * outputFormat - {@link Ext.data.Record} the new outputFormat. * * @event outputformatchange */ /** * Triggered when the layout is changed. * * Listener arguments: * * * printProvider - {@link GeoExt.data.MapfishPrintProvider} this * PrintProvider. * * layout - {@link Ext.data.Record} the new layout. * * @event layoutchange */ /** * Triggered when the dpi value is changed. * * Listener arguments: * * * printProvider - {@link GeoExt.data.MapfishPrintProvider} this * PrintProvider. * * dpi - {@link Ext.data.Record} the new dpi record. * * @event dpichange */ // TODO: rename this event to beforeencode /** * Triggered when the print method is called. * * Listener arguments: * * * printProvider - {@link GeoExt.data.MapfishPrintProvider} this * PrintProvider. * * map - `OpenLayers.Map` the map being printed. * * pages - Array of {@link GeoExt.data.PrintPage} the print * pages being printed. * * options - `Object` the options to the print command. * * @event beforeprint */ /** * Triggered when the print document is opened. * * Listener arguments: * * * printProvider - {@link GeoExt.data.MapfishPrintProvider} this * PrintProvider. * * url - `String` the url of the print document. * * @event print */ /** * Triggered when using the `POST` method, when the print backend * returns an exception. * * Listener arguments: * * * printProvider - {@link GeoExt.data.MapfishPrintProvider} this * PrintProvider. * * response - `Object` the response object of the XHR. * * @event printexception */ /** * Triggered before a layer is encoded. This can be used to exclude * layers from the printing, by having the listener return false. * * Listener arguments: * * * printProvider - {@link GeoExt.data.MapfishPrintProvider} this * PrintProvider. * * layer - `OpenLayers.Layer` the layer which is about to be * encoded. * * @event beforeencodelayer */ /** * Triggered when a layer is encoded. This can be used to modify * the encoded low-level layer object that will be sent to the * print service. * * Listener arguments: * * * printProvider - {@link GeoExt.data.MapfishPrintProvider} this * PrintProvider. * * layer - `OpenLayers.Layer` the layer which is about to be * encoded. * * encodedLayer - `Object` the encoded layer that will be * sent to the print service. * * @event encodelayer */ /** * Triggered before the PDF is downloaded. By returning false from * a listener, the default handling of the PDF can be cancelled * and applications can take control over downloading the PDF. * TODO: rename to beforeprint after the current beforeprint event * has been renamed to beforeencode. * * Listener arguments: * * * printProvider - {@link GeoExt.data.MapfishPrintProvider} this * PrintProvider. * * url - `String` the url of the print document. * * @event beforedownload */ /** * Triggered before the legend is encoded. If the listener * returns false, the default encoding based on GeoExt.LegendPanel * will not be executed. This provides an option for application * to get legend info from a custom component other than * GeoExt.LegendPanel. * * Listener arguments: * * * printProvider - {@link GeoExt.data.MapfishPrintProvider} this * PrintProvider. * * jsonData - `Object` The data that will be sent to the print * server. Can be used to populate jsonData.legends. * * legend - `Object` The legend supplied in the options which were * sent to the print function. * * @event beforeencodelegend */ /** * Private constructor override. * * @private */ constructor: function(config) { var me = this; me.initialConfig = config; Ext.apply(me, config); if(!me.customParams) { me.customParams = {}; } me.callParent(arguments); me.scales = Ext.create('Ext.data.JsonStore', { proxy: me.getProxyConfiguration('scales'), fields: [ "name", {name: "value", type: "float"} ], sortOnLoad: true, sorters: { property: 'value', direction : 'DESC' } }); me.dpis = Ext.create('Ext.data.JsonStore', { proxy: me.getProxyConfiguration('dpis'), fields: [ "name", {name: "value", type: "float"} ] }); me.outputFormats = Ext.create('Ext.data.JsonStore', { proxy: me.getProxyConfiguration('outputFormats'), fields: [{name: "name", defaultValue: "pdf"}] }); me.layouts = Ext.create('Ext.data.JsonStore', { proxy: me.getProxyConfiguration('layouts'), fields: [ "name", {name: "size", mapping: "map"}, {name: "rotation", type: "boolean"} ] }); if(config.capabilities) { me.loadStores(); } else { if(me.url.split("/").pop()) { me.url += "/"; } if (me.initialConfig.autoLoad) { me.loadCapabilities(); } } }, /** * An internal method that creates a valid proxy configuration object for * the passed rootPropertyname. This method is mostly existing because the * name of the rootProperty key we need in the JSON reader is different * between ExtJS 4 and 5 (`root` and `rootProperty` respectively). * * Will always return a memory proxy configuration with a JSON reader where * the correct value for the root of the data is set to the value given. * * this.getProxyConfiguration('dpis'); * // results in ExtJS 4 in the following configuration... * { * type: "memory", * reader: { * type: "json", * root: "dpis" * } * } * // ...while the same call in ExtJS 5 evaluates to * { * type: "memory", * reader: { * type: "json", * rootProperty: "dpis" * } * } * * This method is only used in the #constructor to set up the stores for * the available scales, DPIs and layouts. * * @private */ getProxyConfiguration: function(rootPropertyName) { var readerRootProperty = GeoExt.isExt4 ? 'root' : 'rootProperty', readerCfg = {type: "json"}, proxyCfg; readerCfg[readerRootProperty] = rootPropertyName; proxyCfg = { type: "memory", reader: readerCfg }; return proxyCfg; }, /** * Sets the output format for this printProvider. * * @param {Ext.data.Record} outputFormat The record of the output format. */ setOutputFormat: function(outputFormat) { this.outputFormat = outputFormat; this.fireEvent("outputformatchange", this, outputFormat); }, /** * Sets the layout for this printProvider. * * @param {Ext.data.Record} layout The record of the layout. */ setLayout: function(layout) { this.layout = layout; this.fireEvent("layoutchange", this, layout); }, /** * Sets the dpi for this printProvider. * * @param {Ext.data.Record} dpi The dpi record. * */ setDpi: function(dpi) { this.dpi = dpi; this.fireEvent("dpichange", this, dpi); }, /** * Sends the print command to the print service and opens a new window * with the resulting PDF. * * Valid properties for the `options` argument: * * * `legend` - {@link GeoExt.LegendPanel} If provided, the legend * will be added to the print document. For the printed result to * look like the LegendPanel, the following `!legends` block * should be included in the `items` of your page layout in the * print module's configuration file: * * - !legends * maxIconWidth: 0 * maxIconHeight: 0 * classIndentation: 0 * layerSpace: 5 * layerFontSize: 10 * * * `overview` - `OpenLayers.Control.OverviewMap` If provided, * the layers for the overview map in the printout will be taken from * the OverviewMap control. If not provided, the print service will * use the main map's layers for the overview map. Applies only for * layouts configured to print an overview map. * * @param {GeoExt.MapPanel/OpenLayers.Map} map The map to print. * @param {GeoExt.data.PrintPage[]/GeoExt.data.PrintPage} pages Page or * pages to print. * @param {Object} options Object with additional options, see above. */ print: function(map, pages, options) { if(map instanceof GeoExt.MapPanel) { map = map.map; } pages = pages instanceof Array ? pages : [pages]; options = options || {}; if(this.fireEvent("beforeprint", this, map, pages, options) === false) { return; } var jsonData = Ext.apply({ units: map.getUnits(), srs: map.baseLayer.projection.getCode(), layout: this.layout.get("name"), outputFormat: this.outputFormat.get("name"), dpi: this.dpi.get("value") }, this.customParams); var pagesLayer = pages[0].feature.layer; var encodedLayers = []; // ensure that the baseLayer is the first one in the encoded list var layers = map.layers.concat(); Ext.Array.remove(layers, map.baseLayer); Ext.Array.insert(layers, 0, [map.baseLayer]); Ext.each(layers, function(layer){ if(layer !== pagesLayer && layer.getVisibility() === true) { var enc = this.encodeLayer(layer); enc && encodedLayers.push(enc); } }, this); jsonData.layers = encodedLayers; var encodedPages = []; Ext.each(pages, function(page) { encodedPages.push(Ext.apply({ center: [page.center.lon, page.center.lat], scale: page.scale.get("value"), rotation: page.rotation }, page.customParams)); }, this); jsonData.pages = encodedPages; if (options.overview) { var encodedOverviewLayers = []; Ext.each(options.overview.layers, function(layer) { var enc = this.encodeLayer(layer); enc && encodedOverviewLayers.push(enc); }, this); jsonData.overviewLayers = encodedOverviewLayers; } if(options.legend && !(this.fireEvent("beforeencodelegend", this, jsonData, options.legend) === false)) { var legend = options.legend; var rendered = legend.rendered; if (!rendered) { legend = legend.cloneConfig({ renderTo: document.body, hidden: true }); } var encodedLegends = []; legend.items && legend.items.each(function(cmp) { if(!cmp.hidden) { var encFn = this.encoders.legends[cmp.getXType()]; // MapFish Print doesn't currently support per-page // legends, so we use the scale of the first page. encodedLegends = encodedLegends.concat( encFn.call(this, cmp, jsonData.pages[0].scale)); } }, this); if (!rendered) { legend.destroy(); } jsonData.legends = encodedLegends; } if(this.method === "GET") { var url = Ext.urlAppend(this.capabilities.printURL, "spec=" + encodeURIComponent(Ext.encode(jsonData))); this.download(url); } else { Ext.Ajax.request({ url: this.capabilities.createURL, timeout: this.timeout, jsonData: jsonData, headers: {"Content-Type": "application/json; charset=" + this.encoding}, success: function(response) { var url = Ext.decode(response.responseText).getURL; this.download(url); }, failure: function(response) { this.fireEvent("printexception", this, response); }, params: this.initialConfig.baseParams, scope: this }); } }, /** * Actually triggers a 'download' of the passed URL. * * @param {String} url * @private */ download: function(url) { if (this.fireEvent("beforedownload", this, url) !== false) { if (Ext.isOpera) { // Make sure that Opera don't replace the content tab with // the pdf window.open(url); } else { // This avoids popup blockers for all other browsers window.location.href = url; } } this.fireEvent("print", this, url); }, /** * Loads the capabilities from the print service. If this instance is * configured with either `#capabilities` or a `#url` and `#autoLoad` * set to true, then this method does not need to be called from the * application. */ loadCapabilities: function() { if (!this.url) { return; } var url = this.url + "info.json"; Ext.Ajax.request({ url: url, method: "GET", disableCaching: false, success: function(response) { this.capabilities = Ext.decode(response.responseText); this.loadStores(); }, params: this.initialConfig.baseParams, scope: this }); }, /** * Loads the internal stores and fires the #loadcapabilities event when done. * * @private */ loadStores: function() { this.scales.loadRawData(this.capabilities); this.dpis.loadRawData(this.capabilities); this.outputFormats.loadRawData(this.capabilities); this.layouts.loadRawData(this.capabilities); this.setLayout(this.layouts.getAt(0)); this.setOutputFormat(this.outputFormats.findRecord("name", "pdf")); this.setDpi(this.dpis.getAt(0)); this.fireEvent("loadcapabilities", this, this.capabilities); }, /** * Encodes a given layer according to the definitions in #encoders. * * @param {OpenLayers.Layer} layer * @return {Object} * @private */ encodeLayer: function(layer) { var encLayer; for(var c in this.encoders.layers) { if(OpenLayers.Layer[c] && layer instanceof OpenLayers.Layer[c]) { if(this.fireEvent("beforeencodelayer", this, layer) === false) { return; } encLayer = this.encoders.layers[c].call(this, layer); this.fireEvent("encodelayer", this, layer, encLayer); break; } } // only return the encLayer object when we have a type. Prevents a // fallback on base encoders like HTTPRequest. return (encLayer && encLayer.type) ? encLayer : null; }, /** * Converts the provided url to an absolute url. * * @param {String} url * @return {String} * @private */ getAbsoluteUrl: function(url) { var a; if(Ext.isIE6 || Ext.isIE7 || Ext.isIE8) { a = document.createElement("<a href='" + url + "'/>"); a.style.display = "none"; document.body.appendChild(a); a.href = a.href; document.body.removeChild(a); } else { a = document.createElement("a"); a.href = url; } return a.href; }, /** * Encoders for all print content. * * @property {Object} encoders * @private */ encoders: { "layers": { "Layer": function(layer) { var enc = {}; if (layer.options && layer.options.maxScale) { enc.minScaleDenominator = layer.options.maxScale; } if (layer.options && layer.options.minScale) { enc.maxScaleDenominator = layer.options.minScale; } return enc; }, "WMS": function(layer) { var enc = this.encoders.layers.HTTPRequest.call(this, layer); Ext.apply(enc, { type: 'WMS', layers: [layer.params.LAYERS].join(",").split(","), format: layer.params.FORMAT, styles: [layer.params.STYLES].join(",").split(",") }); var param; for(var p in layer.params) { param = p.toLowerCase(); if(!layer.DEFAULT_PARAMS[param] && "layers,styles,width,height,srs".indexOf(param) == -1) { if(!enc.customParams) { enc.customParams = {}; } enc.customParams[p] = layer.params[p]; } } return enc; }, "OSM": function(layer) { var enc = this.encoders.layers.TileCache.call(this, layer); return Ext.apply(enc, { type: 'OSM', baseURL: enc.baseURL.substr(0, enc.baseURL.indexOf("$")), extension: "png" }); }, "TMS": function(layer) { var enc = this.encoders.layers.TileCache.call(this, layer); return Ext.apply(enc, { type: 'TMS', format: layer.type }); }, "TileCache": function(layer) { var enc = this.encoders.layers.HTTPRequest.call(this, layer); return Ext.apply(enc, { type: 'TileCache', layer: layer.layername, maxExtent: layer.maxExtent.toArray(), tileSize: [layer.tileSize.w, layer.tileSize.h], extension: layer.extension, resolutions: layer.serverResolutions || layer.resolutions }); }, "WMTS": function(layer) { var enc = this.encoders.layers.HTTPRequest.call(this, layer); return Ext.apply(enc, { type: 'WMTS', layer: layer.layer, version: layer.version, requestEncoding: layer.requestEncoding, tileOrigin: [layer.tileOrigin.lon, layer.tileOrigin.lat], tileSize: [layer.tileSize.w, layer.tileSize.h], style: layer.style, formatSuffix: layer.formatSuffix, dimensions: layer.dimensions, params: layer.params, maxExtent: (layer.tileFullExtent != null) ? layer.tileFullExtent.toArray() : layer.maxExtent.toArray(), matrixSet: layer.matrixSet, zoomOffset: layer.zoomOffset, resolutions: layer.serverResolutions || layer.resolutions }); }, "KaMapCache": function(layer) { var enc = this.encoders.layers.KaMap.call(this, layer); return Ext.apply(enc, { type: 'KaMapCache', // group param is mandatory when using KaMapCache group: layer.params['g'], metaTileWidth: layer.params['metaTileSize']['w'], metaTileHeight: layer.params['metaTileSize']['h'] }); }, "KaMap": function(layer) { var enc = this.encoders.layers.HTTPRequest.call(this, layer); return Ext.apply(enc, { type: 'KaMap', map: layer.params['map'], extension: layer.params['i'], // group param is optional when using KaMap group: layer.params['g'] || "", maxExtent: layer.maxExtent.toArray(), tileSize: [layer.tileSize.w, layer.tileSize.h], resolutions: layer.serverResolutions || layer.resolutions }); }, "HTTPRequest": function(layer) { var enc = this.encoders.layers.Layer.call(this, layer); return Ext.apply(enc, { baseURL: this.getAbsoluteUrl(layer.url instanceof Array ? layer.url[0] : layer.url), opacity: (layer.opacity != null) ? layer.opacity : 1.0, singleTile: layer.singleTile }); }, "Image": function(layer) { var enc = this.encoders.layers.Layer.call(this, layer); return Ext.apply(enc, { type: 'Image', baseURL: this.getAbsoluteUrl(layer.getURL(layer.extent)), opacity: (layer.opacity != null) ? layer.opacity : 1.0, extent: layer.extent.toArray(), pixelSize: [layer.size.w, layer.size.h], name: layer.name }); }, "Vector": function(layer) { if(!layer.features.length) { return; } var encFeatures = []; var encStyles = {}; var features = layer.features; var featureFormat = new OpenLayers.Format.GeoJSON(); var styleFormat = new OpenLayers.Format.JSON(); var nextId = 1; var styleDict = {}; var feature, style, dictKey, dictItem, styleName; for(var i=0, len=features.length; i<len; ++i) { feature = features[i]; style = feature.style || layer.style || layer.styleMap.createSymbolizer(feature, feature.renderIntent); dictKey = styleFormat.write(style); dictItem = styleDict[dictKey]; if(dictItem) { //this style is already known styleName = dictItem; } else { //new style styleDict[dictKey] = styleName = nextId++; if(style.externalGraphic) { encStyles[styleName] = Ext.applyIf({ externalGraphic: this.getAbsoluteUrl( style.externalGraphic)}, style); } else { encStyles[styleName] = style; } } var featureGeoJson = featureFormat.extract.feature.call( featureFormat, feature); featureGeoJson.properties = OpenLayers.Util.extend({ _gx_style: styleName }, featureGeoJson.properties); encFeatures.push(featureGeoJson); } var enc = this.encoders.layers.Layer.call(this, layer); return Ext.apply(enc, { type: 'Vector', styles: encStyles, styleProperty: '_gx_style', geoJson: { type: "FeatureCollection", features: encFeatures }, name: layer.name, opacity: (layer.opacity != null) ? layer.opacity : 1.0 }); }, "Markers": function(layer) { var features = []; for (var i=0, len=layer.markers.length; i<len; i++) { var marker = layer.markers[i]; var geometry = new OpenLayers.Geometry.Point(marker.lonlat.lon, marker.lonlat.lat); var style = {externalGraphic: marker.icon.url, graphicWidth: marker.icon.size.w, graphicHeight: marker.icon.size.h, graphicXOffset: marker.icon.offset.x, graphicYOffset: marker.icon.offset.y}; var feature = new OpenLayers.Feature.Vector(geometry, {}, style); features.push(feature); } var vector = new OpenLayers.Layer.Vector(layer.name); vector.addFeatures(features); var output = this.encoders.layers.Vector.call(this, vector); vector.destroy(); return output; } }, "legends": { "gx_wmslegend": function(legend, scale) { var enc = this.encoders.legends.base.call(this, legend); var icons = []; for(var i=1, len=legend.items.getCount(); i<len; ++i) { var url = legend.items.get(i).url; if(legend.useScaleParameter === true && url.toLowerCase().indexOf( 'request=getlegendgraphic') != -1) { var split = url.split("?"); var params = Ext.urlDecode(split[1]); params['SCALE'] = scale; url = split[0] + "?" + Ext.urlEncode(params); } icons.push(this.getAbsoluteUrl(url)); } enc[0].classes[0] = { name: "", icons: icons }; return enc; }, "gx_urllegend": function(legend) { var enc = this.encoders.legends.base.call(this, legend); enc[0].classes.push({ name: "", icon: this.getAbsoluteUrl(legend.items.get(1).url) }); return enc; }, "base": function(legend){ return [{ name: legend.getLabel(), classes: [] }]; } } } });