﻿// Utility class for printing from JS API client via custom printing service
// REQUIRES: DOJO LIBRARY
function PrintUtility(map, printServiceUrl) {
    if (!map)
        alert("PrintUtility.js: map argument cannot be null.");
    else {
        this._map = map;
        this._serviceUrl = printServiceUrl;
        this._maintainScale = true; // if true, prints at same scale as main map; if false, print includes full map extent.
        this._updateTexts = new Array();
        this._timeout = 300000;
        this._pdf = null;
        this._error = null;
    }
}

PrintUtility.prototype = {
    get_map: function () {
        /// The JS API Map object to print from.
        return this._map;
    },
    set_map: function (value) {
        this._map = value;
    },
    get_serviceUrl: function () {
        /// The URL of the print service.
        return this._serviceUrl;
    },
    set_serviceUrl: function (value) {
        this._serviceUrl = value
    },
    get_maintainScale: function (value) {
        /// If true (default), map prints at same scale as the source map; if false, print includes the full map extent, 
        /// and scale will be different from source map. Must be set to true if print includes a Google map layer, since Google layers always print to scale.
        this._maintainScale = value;
    },
    set_maintainScale: function (value) {
        return this._maintainScale;
    },
    get_updateText: function (name) {
        var text = null;
        for (var i = 0; i < this._updateTexts.length; i++) {
            if (this._updateText[i].Name == name) {
                text = this._updateTexts[i].Text;
                break;
            }
        }
        return text;
    },
    add_updateText: function (name, text) {
        /// A text item to update on the print layout. A text element must exist in the template with this name.
        var nameText = { 'Name': name, 'Text': text };
        this._updateTexts.push(nameText);
    },
    remove_updateText: function (name) {
        var idx = this._updateTexts.indexOf(name);
        if (idx > -1) {
            this._updateTexts.splice(idx, 1);
            return true;
        }
        return false;
    },
    get_requestTimeout: function () {
        /// Time before print request should timeout, in milliseconds (300000 = 5 minutes)
        return this._timeout;
    },
    set_requestTimeout: function (value) {
        this._timeout = value;
    },
    get_pdfUrl: function () {
        /// The URL of the PDF produced by the print request.
        return this._pdf;
    },
    get_error: function () {
        return this._error;
    }
}

PrintUtility.prototype._getConstantValue = function(name, value) {
    // constant may use name or not (e.g., STYLE_SOLID or solid)
    if (value.toUpperCase().indexOf(name.toUpperCase()) == 0)
        return value.substring(name.length + 1).toLowerCase();
    else
        return value;
}

PrintUtility.prototype.exportMap = function(height, width, callbackFunction, errorHandler) {
    // Just export a map image and get the URL
    var printLayers = this._getPrintLayers();
    var graphics = this._getPrintGraphics();

    var mapArea = this._getMapExtent();
    var mapOptions = { "Extent": mapArea, "DPI": 96, "CoordinateSystemWkid": this._map.spatialReference.wkid, "Graphics": graphics, "TransparentColor": "#ffffff" };

    var data = { 'mapServices': printLayers,
        'options': mapOptions,
        'mapWidth': width,
        'mapHeight': height
    };

    // Make sure we're calling the ExportMap method, not the Print method
    var svcUrl = this._serviceUrl;
    if (svcUrl.indexOf('Print') == svcUrl.length - 5)
        svcUrl = svcUrl.substring(0, svcUrl.lengt - 5) + 'ExportMap';

    this._sendRequest(svcUrl, data, callbackFunction, errorHandler);
}

PrintUtility.prototype.createPrint = function (template, callbackFunction, errorHandler) {
    // Outputs PDF via print service
    var reportOptions = { 'UpdateTexts': this._updateTexts };

    var printLayers = this._getPrintLayers();
    var graphics = this._getPrintGraphics();

    var mapArea = this._getMapExtent();    
    var mapOptions = { "Extent": mapArea, "DPI": 96, "Graphics": graphics, "TransparentColor": "#ffffff" };

    // Create the parameters for the print service request
    var data = { 'template': template,
        'services': printLayers,
        'options': mapOptions,
        'printOptions': reportOptions
    };

    // Send the request
    this._sendRequest(this._serviceUrl, data, callbackFunction, errorHandler);
}

PrintUtility.prototype._sendRequest = function(serviceUrl, data, callbackFunction, errorHandler) {
    
    // Convert the JSON object into a string
    var dataString = dojo.toJson(data);

    var args = {
        url: serviceUrl,
        handleAs: "json",
        headers: { "Content-Type": "application/json; charset=utf-8" },
        postData: dataString,
        load: function(response) {
            if (response != null && response.d != null && response.d.indexOf("http") == 0) {
                this._pdf = response.d;
                callbackFunction(response.d);
            }
            else {
                this._error = response.d;
                errorHandler(response.d);
            }
        },
        error: function(error) {
            this._error = error;
            errorHandler(error);
        },
        timeout: this._timeout
    }

    try {
        dojo.xhrPost(args);
    } catch (err) {
        this._error = err;
        errorHandler(err);
    }
}

PrintUtility.prototype._getMapExtent = function () {
    // Gets the print area, which may preserve scale or extent
    var mapExt = this._map.extent;
    var mapArea;
    if (this._maintainScale) {
        // print at scale of main map
        var scale = esri.geometry.getScale(this._map);
        var center = mapExt.getCenter();
        mapArea = { "CenterX": center.x, "CenterY": center.y, "Scale": scale };
    }
    else
        mapArea = { "MinX": mapExt.xmin, "MinY": mapExt.ymin, "MaxX": mapExt.xmax, "MaxY": mapExt.ymax };
    
    return mapArea;
}

PrintUtility.prototype._getPrintLayers = function () {
    // Compiles an array of print layers to send to print service
    var layerIds = this._map.layerIds;
    var layer, service, transp;
    var printLayers = new Array();

    for (var i = 0; i < layerIds.length; i++) {
        layer = this._map.getLayer(layerIds[i]);
        if (!layer.visible)
            continue;

        switch (layer.declaredClass) {

            case "esri.layers.ArcGISDynamicMapServiceLayer":
                transp = 1.0 - layer.opacity;
                var useTransp = layer.imageTransparency;
                service = { 'Url': layer.url, 'VisibleLayers': layer.visibleLayers,
                    'Transparency': transp, 'UseTransparentColor': useTransp,
                    'TransparentColor': '#ffffff'
                };
                printLayers.push(service);
                break;

            case "esri.layers.ArcGISTiledMapServiceLayer":
                transp = 1.0 - layer.opacity;
                service = { "Url": layer.url, "Transparency": transp };
                printLayers.push(service);
                break;

            case "gmaps.GoogleMapsLayer":
                if (this._maintainScale) {
                    var mapType = layer.getGMap().getMapTypeId();
                    // Get center & zoom level for Google static map
                    var centerLatLong = esri.geometry.webMercatorToGeographic(this._map.extent.getCenter());
                    var zoomLevel = layer.getGMap().getZoom();
                    // Set "url" to custom string that the printing service will recognize as Google map
                    var urlStr = "google:" + mapType + ":" + zoomLevel + ":" + centerLatLong.x + ":" + centerLatLong.y;
                    transp = 1.0 - layer.opacity;
                    service = { "Url": urlStr, "Transparency": transp };
                }
                else {
                    // If not maintaining scale, Google map won't print correctly under AGS layers. In this case, 
                    //  swap in ESRI street/imagery layer for custom GMaps layers.
                    var swapLayer = this._getEsriLayerSubstitute(layer);
                    service = { "Url": swapLayer.url, "Transparency": transp };
                }
                printLayers.push(service);
                break;
        }
    }
    return printLayers;
}

PrintUtility.prototype._getPrintGraphics = function() {
    // Compiles all graphics in the map into the format required by print service
    var points = new Array();
    var lines = new Array();
    var polygons = new Array();
    var texts = new Array();
    var graphics = { "Points": points, "Lines": lines, "Polygons": polygons, "Texts": texts };
    var layerIds = this._map.graphicsLayerIds;

    for (var i = 0; i < layerIds.length; i++) {
        layer = this._map.getLayer(layerIds[i]);
        if (!layer.visible)
            continue;

        if (layer.declaredClass == "esri.layers.GraphicsLayer") {
            this._addPrintGraphics(layer, graphics);
        }
    }
    // Add default map Graphics layer
    this._addPrintGraphics(map.graphics, graphics);

    return graphics;
}

PrintUtility.prototype._getEsriLayerSubstitute = function(layer) {
    /// Get equivalent ESRI layer to swap for Google layer
    var esriLyr = null;
    var mapType = layer.getGMap().getMapTypeId();
    switch (mapType) {
        case "roadmap":
            esriLyr = this._getEsriLayer("World_Street_Map");
            break;

        case "satellite":
            esriLyr = this._getEsriLayer("World_Imagery");
            break;

        case "hybrid":
            esriLyr = this._getEsriLayer("World_Imagery");
            break;
            
        case "terrain":
            esriLyr = this._getEsriLayer("World_Street_Map");
            break;
    }
    return esriLyr;
}

PrintUtility.prototype._getEsriLayer = function(type) {
    var esriLyr = null;
    var lyr;
    var lyrs = this._map.layerIds;

    for (var i = 0; i < lyrs.length; i++) {
        lyr = this._map.getLayer(lyrs[i]);
        if (lyr.declaredClass == "esri.layers.ArcGISTiledMapServiceLayer") {
            if (lyr.url.indexOf(type) > 0) {
                esriLyr = lyr;
                break;
            }
        }
    }

    return esriLyr;
}

// Private function to convert graphics from JS API to format for print service
PrintUtility.prototype._addPrintGraphics = function(layer, graphics) {
	
	if (layer.graphics == null)
		return;

    var graphic, printGraphic, geom, geomString, symbol;
    var geomArr;
    var renderer = layer.renderer;

    for (var i = 0, il = layer.graphics.length; i < il; i++) {
        graphic = layer.graphics[i];
        if (graphic.visible != undefined && !graphic.visible)
            continue;
        printGraphic = {};  // empty object for print graphic

        // Get the geometry and symbol
        geom = graphic.geometry;
        // Get symbol from layer renderer if using one, otherwise from graphic
        symbol = (renderer) ? renderer.getSymbol(graphic) : graphic.symbol;
        geomString = "";
        switch (geom.type) {
            case "point":
                printGraphic.X = geom.x;
                printGraphic.Y = geom.y;

                if (symbol.type.toLowerCase() == "textsymbol") {
                    this._setTextSymbol(symbol, printGraphic);
                    graphics.Texts.push(printGraphic);
                }
                else {
                    printGraphic.Color = symbol.color.toHex();
                    printGraphic.Angle = symbol.angle
                    printGraphic.Width = symbol.size / 96 * 72; // convert pixels to points
                    printGraphic.XOffset = symbol.xoffset / 96 * 72;
                    printGraphic.YOffset = symbol.yoffset / 96 * 72;
                    if (symbol.type == "simplemarkersymbol") {
                        // Supported styles: Circle, Square, Cross, X, or Diamond
                        // JS adds STYLE_ prefix to above values
                        printGraphic.Style = this._getConstantValue("STYLE", symbol.style);
                        if (symbol.outline && symbol.outline.width > 0) {
                            // outline is simplelinesymbol
                            printGraphic.DrawOutline = true;
                            printGraphic.OutlineColor = symbol.outline.color.toHex();
                        }
                    } else if (symbol.type == "picturemarkersymbol") {
                        // not yet supported
                    }
                    graphics.Points.push(printGraphic);
                }
                break;

            case "polyline":
                // Single path supported currently
                var path = geom.paths[0];
                geomArr = new Array();
                for (var j = 0, jl = path.length; j < jl; j++) {
                    geomArr.push(path[j].join(","));
                }
                printGraphic.Coords = geomArr.join(";");

                printGraphic.Color = symbol.color.toHex();
                printGraphic.Width = symbol.width;
                // Solid, Dash, Dot, DashDotDot or Null: JS adds STYLE_ prefix
                printGraphic.Style = this._getConstantValue("STYLE", symbol.style);
                if (symbol.type == "cartographiclinesymbol") {
                    // extra properties not yet supported
                }
                graphics.Lines.push(printGraphic);
                break;

            case "polygon":
                var ring = geom.rings[0];
                geomArr = new Array();
                for (var r = 0, rl = ring.length; r < rl; r++) {
                    geomArr.push(ring[r].join(","));
                }
                printGraphic.Coords = geomArr.join(";"); //geomString.substring(0, geomString.length - 1);

                this._getFillSymbol(symbol, printGraphic);
                graphics.Polygons.push(printGraphic);
                break;

            // Multipoint not yet implemented                                            
            //                    case "multipoint":                                            
            //                        var pts = geom.points;                                            
            //                        for (var i = 0, il = pts.length; i < il; i++) {                                            
            //                            geomString += pts[i][0] + "," + pts[i][1] + ";";                                            
            //                        }                                            
            //                        printGraphic.Coords = geomString.substring(0, geomString.length - 1);                                            
            //                        break;                                            

            case "extent":
                // treat as polygon
                printGraphic.Coords = geom.xmin + "," + geom.ymin + ";"
                    + geom.xmin + "," + geom.ymax + ";"
                    + geom.xmax + "," + geom.ymax + ";"
                    + geom.xmax + "," + geom.ymin + ";"
                    + geom.xmin + "," + geom.ymin;

                this._getFillSymbol(symbol, printGraphic);
                graphics.Polygons.push(printGraphic);
                break;
        }

        if (geomString = "")
            continue;
    }
}

PrintUtility.prototype._getFillSymbol = function(symbol, printGraphic) {
    // Sets symbol properties for polygon and extent graphics
    if (symbol.outline) {
        // simplelinesymbol
        printGraphic.OutlineWidth = symbol.outline.width;
        printGraphic.OutlineColor = symbol.outline.color.toHex();
        printGraphic.OutlineStyle = this._getConstantValue("STYLE", symbol.outline.style);
    }
    if (symbol.type == "simplefillsymbol") {

        // Default fill is empty
        var fillStyle = symbol.style;

        if (fillStyle == "null") // swap none for null
            fillStyle = "none";
        // Semitransparent colors not supported - if < .9 opacity with solid fill, just use empty; also
        //  use empty fill for any color transparency under 0.3
        else if (fillStyle == "solid")
            if (!symbol.color || symbol.color.a < 0.9 || symbol.color.a < 0.3)
                fillStyle = "none";

        printGraphic.FillStyle = fillStyle;
        // For solid fill, use the symbol's fill color
        printGraphic.FillColor = symbol.color.toHex();
    }
    // picturefillsymbol not yet supported
}

PrintUtility.prototype._setTextSymbol = function(symbol, printGraphic) {
    printGraphic.Color = symbol.color.toHex();
    printGraphic.Text = symbol.text;
    printGraphic.Angle = symbol.angle;
    printGraphic.FontName = symbol.font.family;
    printGraphic.Size = parseFloat(symbol.font.size);
    printGraphic.Kerning = symbol.kerning;
    printGraphic.XOffset = symbol.xoffset / 96 * 72;  // convert pixels to points
    printGraphic.YOffset = symbol.yoffset / 96 * 72;

    if (symbol.font.style.toLowerCase().indexOf("italic") >= 0)
        printGraphic.UseItalics = true;
    if (symbol.font.variant.toLowerCase().indexOf("smallcaps") >= 0)
        printGraphic.TextCase = "SmallCaps";
    if (symbol.font.weight.toLowerCase().indexOf("bold") > 0)
        printGraphic.UseBold = true;

    switch (symbol.decoration.toLowerCase()) {
        case "decoration_linethrough", "linethrough":
            printGraphic.UseStrikethrough = true;
            break;
        case "decoration_underline", "underline":
            printGraphic.UseUnderline = true;
            // DECORATION_OVERLINE not supported in AGS SOAP
    }

    switch (symbol.align.toLowerCase()) {
        case "align_start", "start":
            printGraphic.HorizontalAlignment = "Left";
            break;
        case "align_middle", "middle":
            printGraphic.HorizontalAlignment = "Center";
            break;
        case "align_end", "end":
            printGraphic.HorizontalAlignment = "Right";
            break;
    }
}

