viernes, 31 de mayo de 2013

[Dynamic CRM] Integrar Google Maps – PopUp Mapa Web (1/2)

 

Introducción


Al trabajar con contactos un requerimiento bastante buscado se relaciona con la localización o distribución de estos en un mapa, poder desplegar los cliente en una distribución geográfica da una idea de área de cobertura de ventas.

El objetico del articulo será justamente poder despegar en un grafico dinámico los cliente listados en Dynamic CRM, se contara con un botón en la toobox, el cual abrirá una ventana popup del browser con el mapa y los puntos representando cada cliente.

SNAGHTML33391976

 

image

Durante la confección del articulo no solo se explicara como generar el código que genere el grafico, sino que también incluirá su publicación e integración dentro de Dynamic CRM.

Comenzaremos analizando el código que permite hace ruso de las API de Google Maps, para continuar luego con la integración de este a CRM.

 

Estructura del Código


Lo primero que haremos será integrar código de la API de Google Maps con javascript, pero será necesario consultar los servicio de Dynamic CRM para recuperar los datos del fetchxml que se define en la lista de clientes.

En un proyecto web crearemos varios js y un html que contendrá el mapa

SNAGHTML333bd352

 

Invocar Servicios CRM desde javascript

En el archivo CRMHelper.js se encontrara toda al funcionalidad requerida para invocar el servicio de CRM.

Se necesitara de una query xml conocida en CRM como fetchxml para conocer que cuentas estaban listadas cuando el usuario presiono en el icono del mapa, estas cuentas serán posicionadas como puntos.

El primer paso es conocer como consultar un servicio CRM mediante javascript.

 

//toma el root de la url del sitio CRM
var context = window.parent.opener.Xrm.Page.context;
var serverUrl = context.getServerUrl();
if (serverUrl.match(/\/$/)) {
    serverUrl = serverUrl.substring(0, serverUrl.length - 1);
}

//ejecuta la consulta fetchxml al servicio de CRM
function FetchResultsXml(fetchXml) {


    var request = '<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">' +
    '<s:Body>'+
    '<Execute xmlns="http://schemas.microsoft.com/xrm/2011/Contracts/Services">' +
    '<request i:type="b:RetrieveMultipleRequest" ' +
    ' xmlns:b="http://schemas.microsoft.com/xrm/2011/Contracts" ' +
    ' xmlns:i="http://www.w3.org/2001/XMLSchema-instance">' +
    '<b:Parameters xmlns:c="http://schemas.datacontract.org/2004/07/System.Collections.Generic">' +
    '<b:KeyValuePairOfstringanyType>' +
    '<c:key>Query</c:key>' +
    '<c:value i:type="b:FetchExpression">' +
    '<b:Query>'+ CrmEncodeDecode.CrmXmlEncode(fetchXml) +
    '</b:Query>' +
    '</c:value>' +
    '</b:KeyValuePairOfstringanyType>' +
    '</b:Parameters>' +
    '<b:RequestId i:nil="true"/>' +
    '<b:RequestName>RetrieveMultiple</b:RequestName>' +
    '</request>' +
    '</Execute>' +
    '</s:Body></s:Envelope>';
    
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.open("POST", serverUrl + "/XRMServices/2011/Organization.svc/web", false);
    xmlhttp.setRequestHeader("Accept", "application/xml, text/xml, */*");
    xmlhttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
    xmlhttp.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute");

    xmlhttp.send(request);
 
    return xmlhttp.responseXML;
}

 

Aquí se define el xml que define el mensaje SOAP para invocación al servicio de CRM, dentro se ubica el fetchxml.

La url que utilizada para invocar el servicio se descubre por medio de window.parent.opener.Xrm.Page.context, usando la función getServerUrl()

El resultado que obtendremos es un xml con los datos de las cuentas, los cuales serán parseados para formar objetos javascript que represente los datos de cada cuenta. 

El las siguientes funciones se parsean el resultado y vuelcan la info. a un array definido en javascript. Resultara mas  simple si se tiene a mano el xml ha procesar, para esto lo que hice fue poner el resultad en un div usando la líneas:

var resultCRM = FetchResultsXml(fetchxml);

$('#xmlresult').html(resultCRM.xml);

Se invoca a la función que ejecuta la invocación al servicio, mostrando el resultado en el div, con el Developer Tools del IE (accedemos mediante F12) analizaremos la estructura de tag que deberemos parsear.

 

function GetFieldValue(entity, fieldName) {
    var vals = entity.getElementsByTagName("a:KeyValuePairOfstringanyType");
    
    for (var j = 0; j < vals.length; j++) {

        if (vals[j].getElementsByTagName("b:key")[0].firstChild.nodeValue == fieldName) {

            var valueNode = vals[j].getElementsByTagName("b:value");
            
            if (valueNode.length > 0)

                if (valueNode[0].childNodes.length == 1)
                    return valueNode[0].firstChild.nodeValue;
                else
                    return valueNode[0].childNodes[2].firstChild.nodeValue; //se toma el valor del nodo "name"
                    
            else
                return "";
        }
    }
}

function GetEntityName(fetchResults) {

    var entityElems = fetchResults.getElementsByTagName("a:EntityName");
    
    if (entityElems.length > 0) {
        return entityElems[0].firstChild.nodeValue;
    }
   
}


function GetArrayFromFetchResults(fetchResults) {

    var entityResults = new Array();

    var entityname = GetEntityName(fetchResults);

    var entityElems = fetchResults.getElementsByTagName("a:Entity");


    for (var i = 0; i < entityElems.length; i++) {

        entityResults[i] = new Object();

        if (entityname == 'account') {
            entityResults[i].id = GetFieldValue(entityElems[i], "accountid");
            entityResults[i].name = GetFieldValue(entityElems[i], "name");
            entityResults[i].addressname = GetFieldValue(entityElems[i], "address1_line1") == null ? "" : GetFieldValue(entityElems[i], "address1_line1");

            entityResults[i].latitude = GetFieldValue(entityElems[i], "address1_latitude");
            entityResults[i].longitude = GetFieldValue(entityElems[i], "address1_longitude");

            entityResults[i].stateorprovince = GetFieldValue(entityElems[i], "address1_stateorprovince") == null ? "" : GetFieldValue(entityElems[i], "address1_stateorprovince");
            entityResults[i].territory = GetFieldValue(entityElems[i], "territoryid") == null ? "" : GetFieldValue(entityElems[i], "territoryid");
        }

    }
   
    
    return entityResults;
}

La funcionalidad inicia en el método GetArrayFromFetchResults() el cual invoca a GetEntityName() para conocer que entidad se esta trabajando, en al imagen se puede observar el tag que se recupera:

SNAGHTML343cfd92

si bien en este ejemplo solo se usa la entidad account podría adaptarse el código para visualidad otro tipo de entidades por eso es bueno conocer de cual se trata.

El siguiente paso será recuperar cada uno de los atributos por cada account. Tomamos el tag “entity” usando la línea:

var entityElems = fetchResults.getElementsByTagName("a:Entity");

como se observan son 4, una por cada account de la lista que muestra CRM.

SNAGHTML3440082c

Dentro de la función GetFieldValue() accedemos directo al tag KeyValuePairOfstringanyType, la colección de estos tag contiene el valor de cada atributo, en este punto hay que diferenciar entre datos simple del CRM como ser, por ejemplo, el name del account.

SNAGHTML34459eb1

en donde el value es un valor simple, esto se recupera mediante la líneas:

if (valueNode[0].childNodes.length == 1) 
      return valueNode[0].firstChild.nodeValue;

ya que el tag “value” no tendrá ningún nodo hijo.Pero hay otros atributos complejos como ser el territorio, al ser una entidad relacionada el valor a recuperar tiene otra estructura de nodos.

SNAGHTML3449d338

es por eso que pasara por al línea:

return valueNode[0].childNodes[2].firstChild.nodeValue; //se toma el valor del nodo "name"

una vez entendido como parsear el xml solo queda armar el array con las propiedades de la entidad, esto es justamente lo que retorna GetArrayFromFetchResults()

 

Uso de las Google Map API


Luego de obtener los datos se procede a configurar el mapa usando las API de Google MAP, esto código se define en el archivo Mapa.html

Lo primero será declarar las librerías necesarias

 

<script type="text/javascript" src="http://maps.googleapis.com/maps/api/js?sensor=false"></script>

<script src="ClientGlobalContext.js.aspx" type="text/javascript"></script>

<script src="new_jquery1.9" type="text/javascript"></script>
<script src="new_CRMHelper" type="text/javascript"></script>
<script src="new_linqjs" type="text/javascript"></script>

 

Jquery es una librería necesaria. También vemos el link a las API de Google Map.

Pero seguro notaran una de nombre linqjs, esta será usada para poder agrupar los datos desde javascript, se trata de:

linq.js - LINQ for JavaScript

Nota: los script que llevan el prefijo new_ es porque fueron subidos como resource al propio CRM, este paso lo veremos mas adelante al publicar los js y html

El código que define el mapa es el siguiente:

 

        <script type="text/javascript">

            $(function() {
                try {

                    var data = decodeURIComponent($.getUrlVar('Data'));
                    var grupo = data.split('=')[1];

                    var fetchxml = window.opener.parent.document.getElementById('contentIFrame').contentDocument.getElementById('effectiveFetchXml').attributes['value'].nodeValue;

                    var resultCRM = FetchResultsXml(fetchxml);

                    $('#xmlresult').html(resultCRM.xml);
                    var resultArray = GetArrayFromFetchResults(resultCRM);

                    //se define la lista de colores que puede tomar los marker
                    var listcolor = new Array('FE7569', '0404B4', 'FFFF00', '088A08', '01DFD7', '8A0886', '1C1C1C');


                    var mapOptions = {
                        zoom: 4,
                        mapTypeId: google.maps.MapTypeId.ROADMAP
                    }

                    var map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions);

                    var bounds = new google.maps.LatLngBounds();

                    var infowindow = new google.maps.InfoWindow();

                    var index = 0;
                    Enumerable.From(resultArray)
                                .GroupBy('$.' + grupo, '', function(key, group) { return { sucu: key, group: group} })
                                .ForEach(function(x) {

                                    var pinImage = new google.maps.MarkerImage("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|" + listcolor[index],
                                                                new google.maps.Size(21, 34),
                                                                new google.maps.Point(0, 0),
                                                                new google.maps.Point(10, 34));

                                    x.group.ForEach(function(y) {

                                        var marker = new google.maps.Marker({
                                            position: new google.maps.LatLng(y.latitude, y.longitude),
                                            icon: pinImage,
                                            map: map,
                                            html: "<b>" + y.name + "</b>
Direccion: " + y.addressname + "
Estado: " + y.stateorprovince + "
Territorio: " + y.territory
                                        });

                                        google.maps.event.addListener(marker, "click", function() {

                                            infowindow.setContent(this.html);
                                            infowindow.open(map, this);

                                        });

                                        bounds.extend(marker.position);
                                    });

                                    index++;

                                    if (index == listcolor.length)
                                        index = 0;

                                });


                    map.fitBounds(bounds);

                }
                catch (err) {
                    var txt = "There was an error on this page.\n\n";
                    txt += "Error description: " + err.message + "\n\n";
                    txt += "Click OK to continue.\n\n";
                    alert(txt);
                }
            });

        </script>

 

Ahora vamos a ir analizando las diferentes línea que permite crear el mapa. Empezamos tomando de la url el valor de la querystring, el cual define como agruparemos para generar en el mapa los puntos de diferentes colores

var data = decodeURIComponent($.getUrlVar('Data'));
var grupo = data.split('=')[1];

Luego tomaremos el fetchxml

var fetchxml = window.opener.parent.document.getElementById('contentIFrame').contentDocument.getElementById('effectiveFetchXml').attributes['value'].nodeValue;

la línea es bastante larga pues el dato se encuentra bastante escondido, si analizamos el html del CRM con el Developer Tools y buscamos “effectiveFetchXml” veremos que el tag se encuentra dentro de un iframe.

De esta forma obtenemos valor que conforma el xml que utiliza CRM para definir las cuentas que lista en pantalla.

SNAGHTML3473800b

 

A continuación lanzamos la consulta al servicio con el código analizado en la sección previa.

var resultCRM = FetchResultsXml(fetchxml);

//$('#xmlresult').html(resultCRM.xml);
var resultArray = GetArrayFromFetchResults(resultCRM);

 

Se declaran las variables que definen el mapa:

  • mapOptions, define información global del mapa, en este caso zoom y tipo
  • map, contiene la instancia del div que contendrá el mapa
  • bounds, se utiliza para general la región que permita centrar la vista, de por si el centrado no es automático, es necesario indicar las posiciones que conforman la región en que se quiere centrar el foco visual
  • infowindow, define el popup con al información contextual para cada marca

 

var mapOptions = {
    zoom: 4,
    mapTypeId: google.maps.MapTypeId.ROADMAP
}

var map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions);

var bounds = new google.maps.LatLngBounds();

var infowindow = new google.maps.InfoWindow();

La siguiente líneas serán las encargadas de agrupar e iterar los datos de las cuentas, es aquí donde se hace uso de la librería linq.js para poder agrupar la lista de cuenta por la propiedad definida en la url.

 

Enumerable.From(resultArray)
          .GroupBy('$.' + grupo, '', function(key, group) { return { sucu: key, group: group} })
          .ForEach(function(x) {

Las líneas de código que se encuentran dentro del ForEach crearan las marcas en el mapa.

Se define la forma del icono, en el link Google Maps Pins se podrá encontrar información sobre la generacion de estos iconos. 

var pinImage = new google.maps.MarkerImage("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|" + listcolor[index],
                            new google.maps.Size(21, 34),
                            new google.maps.Point(0, 0),
                            new google.maps.Point(10, 34));

Se itera por lo ítem que forman cada grupo, recordemos que cada grupo determina el color de sus ítems

x.group.ForEach(function(y) { ....

Se crea el market, en este se define la posición concreta del punto en el mapa, también se define un contenido asociado cuando se pulse sobre la marca asignando el html que representa el contenido

var marker = new google.maps.Marker({
	position: new google.maps.LatLng(y.latitude, y.longitude),
	icon: pinImage,
	map: map,
	html: "<b>" + y.name + "</b><br/>Direccion: " + y.addressname + "<br/>Estado: " + y.stateorprovince + "<br/>Territorio: " + y.territory
});

Se asocia el market con un evento, el cual desplegara el popup con información de la localización. Además se agrega la posición a la lista de bounds para definir el área en que se centrara la grafica.

 

google.maps.event.addListener(marker, "click", function() {

    infowindow.setContent(this.html);
    infowindow.open(map, this);

});

bounds.extend(marker.position);

Luego de terminado el ciclo por cada grupo y cuenta, se asigna los bounds al map

map.fitBounds(bounds);

 

Hasta aquí seria todo lo necesario con código para generar el mapa.

En la siguiente parte del artículo veremos como publicar en CRM y asociar a un botón que se encuentra en la ribbon

 

Código


No hay comentarios:

Publicar un comentario