(PT)
Já passou muito tempo desde que fui convidado para fazer alguns blocos para o popfly, utilizando serviços do sapo. Os blocos viriam a ser depois utilizados para uma demonstração do popfly no Sapo Code Bits 2007 (eu avisei que já tinha passado muito tempo). Finalmente, "encontrei" algum tempo, e decidi agarrar num dos (vários) workitems que tenho pendentes nesta "categoria" :)
Para quem não conhece, o Popfly é uma ferramenta online para a criação de mashups. Para quem não sabe o que é um Mashup... bem... resta-me perguntar em que buraco tem andado enfiado nos últimos anos... :)
Software necessário
- Web browser :) - Testado por mim no Firefox e no Internet Explorer 7. A aplicação é baseada em Silverlight, por isso, deverá funcionar em qualquer browser que suporte Silverlight.
- Opcionalmente, pode ser utilizado um editor de texto, para a edição e manutenção de uma cópia local do código antes de submissão na aplicação Silverlight.
Nota: O código-fonte dos blocos fica visível para outros utilizadores, por isso cuidado com o código que escreverem.
Software recomendado
Referência
Para instalar o Popfly Explorer sobre o Visual Web Developer, é necessário efectuar o registo deste último. O registo é gratuito, e fica associado ao Live ID.
Quando criei os blocos Sapo Pharmacy e Sapo Maps para apresentar no Sapo Code Bits, não existia este plug-in, pelo que já agora, aproveito para o apresentar, uma vez que me parece simplificar muito o processo.
Para começar, vou apresentar os conceitos associados aos blocos, bem como o código / markup criado para a criação dos blocos Sapo Pharmacy e Sapo Maps.
Mas afinal, o que é um bloco?
Um bloco representa um serviço, e é composto por várias operações. Em Popfly, um bloco é composto por uma parte em XML, que descreve o bloco, e por uma parte em JavaScript, onde se define o comportamento do bloco.
Como começar?
Para começar, vamos ao serviço Web de pesquisa de Farmácias do Sapo, e escolhemos uma operação. Por exemplo, vamos começar com a operação "GetPharmaciesAtServiceByCoordinates". Podemos usar esta página para efectuar alguns testes, e observar o output da chamada da operação do serviço.
O que colocar nestes campos? Bem, é fácil, podemos utilizar o Sistema de Informação Geográfica do Sapo para, por exemplo obter uma lista de Distritos, ordenada pelo nome.
Surgirá uma lista de concelhos, onde cada um deles, terá o seguinte aspecto:

Fica a faltar o campo "radius". Este campo é utilizado para definir o raio da pesquisa (em metros), em torno das coordenadas seleccionadas.
Assim sendo, escolhemos um concelho, e usamos os valores associados na página anterior. (Escolhi um raio de 10Km).
Seja quais forem os parâmetros escolhidos, desde que não exista um erro nos argumentos, surgirá uma resposta semelhante a esta:
O que há aqui a reter é a estrutura da resposta, o url que surge no browser após a satisfação do pedido, que neste caso é algo do género:
http://services.sapo.pt/Pharmacy/GetPharmaciesAtServiceByCoordinates?latitude=38.8031578&longitude=-9.381098&radius=10000
Este dado também nos vai ser útil.
Criar o bloco
Esta é a parte fácil: no Popfly, menu "Create Stuff"-> "Block"
Estrutura (Markup)
Assim sendo, já é mais fácil justificar o markup que tem de ser criado para dar suporte a esta operação, no bloco Popfly (recomenda-se uma passagem de olhos rápida pelo documento que consta nas referências):
<?xml version="1.0" encoding="utf-8"?>
<!-- O código Javascript do bloco reside na classe SapoPharmacyClass, que tem um método initialize -->
<block class="SapoPharmacyClass" hasInitialize="true">
<providerName>Sapo</providerName>
<operations>
<!-- A lista de operações disponibilizadas pelo bloco. Para já apenas uma. -->
<operation name="getPharmaciesAtServiceByCoordinates"> <!-- O nome do método em JavaScript que será chamado (sobre uma instância do tipo previamente identificado) -->
<description>
Obtém uma lista de farmácias de serviço a partir das coordenadas, e num raio especificado
</description>
<inputs>
<!-- A caracterização de cada um dos argumentos da operação -->
<input name="latitude" required="true" type="latitude">
<description>A latitude do local</description>
<defaultValue>38.9982</defaultValue>
<constraints/>
</input>
<input name="longitude" required="true" type="longitude">
<description>A longitude do local</description>
<defaultValue>-9.16351</defaultValue>
<constraints/>
</input>
<input name="radius" required="true" type="nonNegativeInteger">
<description>O raio da pesquisa</description>
<defaultValue>10000</defaultValue>
<constraints/>
</input>
</inputs>
<outputs>
<!-- A caracterização do resultado da operação -->
<output isArray="true" type="custom" object="SapoPharmacy"/>
</outputs>
</operation>
</operations>
<objects>
<!-- Definição do tipo custom "SapoPharmacy", referenciado no argumento de saída -->
<object name="SapoPharmacy">
<field name="LastUpdate" isArray="false" type="date"/>
<field type="nonNegativeInteger" isArray="false" name="Code"/>
<field type="string" isArray="false" name="Name"/>
<field type="phoneNumber" isArray="false" name="Phone" />
<field type="phoneNumber" isArray="false" name="Fax" />
<field type="string" isArray="false" name="Director" />
<field type="string" isArray="false" name="Distance" />
<field type="custom" isArray="true" name="Services" object="ServiceType"/>
<field type="boolean" isArray="false" name="IsAtService"/>
<field type="boolean" isArray="false" name="IsLateNight"/>
<field type="location" isArray="false" name="Street"/>
<field type="latitude" isArray="false" name="Latitude"/>
<field type="longitude" isArray="false" name="Longitude"/>
<field type="zipCode" isArray="false" name="ZipCode"/>
<field type="integer" isArray="false" name="DistrictId"/>
<field type="location" isArray="false" name="District"/>
<field type="integer" isArray="false" name="MunicipalityId"/>
<field type="location" isArray="false" name="Municipality"/>
<field type="integer" isArray="false" name="ParishId"/>
<field type="location" isArray="false" name="Parish"/>
</object>
<!-- Definição do tipo custom "ServiceType", referenciado no tipo custom SapoPharmacy, no campo "Services" -->
<object name="ServiceType">
<field type="date" isArray="false" name="Date"/>
<field type="string" isArray="false" name="Type"/>
<field type="date" isArray="false" name="LastUpdate"/>
</object>
</objects>
</block>
Penso que a descrição é bastante auto-explanatória (hmm isto existe?..), e que não são necessárias mais explicações desta parte.
Comportamento (JavaScript)
De seguida, basta definir o código JavaScript para realizar a operação definida no Markup:
function SapoPharmacyClass()
{
}
//Esta função é chamada antes de ser iniciado o render do bloco.
SapoPharmacyClass.prototype.initialize = function() {
if (environment.designTime)
throw "Isto não é suportado pelo bloco de farmácias do sapo em modo de desenho.";
environment.output("<LINK REL=StyleSheet HREF=\"http://farmacias.sapo.pt/css/farmacias.css\" TYPE=\"text/css\>");
}
//A operação propriamente dita:
SapoPharmacyClass.prototype.getPharmaciesAtServiceByCoordinates = function (latitude, longitude, radius) {
//Lembram-se da query string que surgiu quando foi feito o pedido com os argumentos de teste?
url = "http://services.sapo.pt/Pharmacy/GetPharmaciesAtServiceByCoordinates?latitude="+latitude+"&longitude="+longitude+"&radius="+radius;
return this.__getResults(url, "GetPharmaciesAtServiceByCoordinatesResult");
};
SapoPharmacyClass.prototype.__getElementValue = function (rootElement, tagName){
return rootElement.getElementsByTagName(tagName)[0]&&rootElement.getElementsByTagName(tagName)[0].firstChild!==null?rootElement.getElementsByTagName(tagName)[0].firstChild.nodeValue : "";
};
SapoPharmacyClass.prototype.__getResults = function (url, firstElemTagName) {
var resultsArray = new Array();
var resultXML = environment.getXml(url); //Este método é fornecido pelo popfly. Sem este método, seria impossível realizar este código, porque o browser não permite chamadas em script para outros domínios.
//boring... interpretação da resposta......
if(resultXML.getElementsByTagName(firstElemTagName).length >= 1)
{
var total = resultXML.getElementsByTagName("Total")[0]?resultXML.getElementsByTagName("Total")[0].firstChild.nodeValue : "";
var pharmaciesNodes = resultXML.getElementsByTagName("Pharmacy");
var pharmaciesCount = pharmaciesNodes.length;
for(var i = 0; i < pharmaciesCount; i++)
{ //Elementos simples
var LastUpdate = this.__getElementValue(pharmaciesNodes[i], "LastUpdate");
var Code = this.__getElementValue(pharmaciesNodes[i], "Code");
var Name = this.__getElementValue(pharmaciesNodes[i], "Name");
var Phone = this.__getElementValue(pharmaciesNodes[i], "Phone");
var Fax = this.__getElementValue(pharmaciesNodes[i], "Fax");
var Director = this.__getElementValue(pharmaciesNodes[i], "Director");
var Distance = this.__getElementValue(pharmaciesNodes[i], "Distance");
var IsAtService = this.__getElementValue(pharmaciesNodes[i], "IsAtService");
var IsLateNight = this.__getElementValue(pharmaciesNodes[i], "IsLateNight");
//tipos complexos
//Address
var Address = pharmaciesNodes[i].getElementsByTagName("Address")[0];
var Street = this.__getElementValue(Address, "Street");
var ZipCode = this.__getElementValue(Address, "ZipCode");
var DistrictId = this.__getElementValue(Address, "DistrictId");
var District = this.__getElementValue(Address, "District");
var MunicipalityId = this.__getElementValue(Address, "MunicipalityId");
var Municipality = this.__getElementValue(Address, "Municipality");
var ParishId = this.__getElementValue(Address, "ParishId");
var Parish = this.__getElementValue(Address, "Parish");
//Coordinates
var Coordinates = Address.getElementsByTagName("Coordinates")[0];
var Latitude = this.__getElementValue(Coordinates, "Latitude");
var Longitude = this.__getElementValue(Coordinates, "Longitude");
//Services
var ServicesNodes = pharmaciesNodes[i].getElementsByTagName("Service");
var ServicesCount = ServicesNodes===null?0:ServicesNodes.length;
var servicesArray = new Array();
for(var k = 0; k < ServicesCount; k++){
var Date = this.__getElementValue(ServicesNodes[k], "Date");
var Type = this.__getElementValue(ServicesNodes[k], "Type");
var LastUpdate2 = this.__getElementValue(ServicesNodes[k], "LastUpdate");
servicesArray[k] = new SapoPharmacyService(Date, Type, LastUpdate2);
}
resultsArray[i] = new SapoPharmacy(LastUpdate, Code, Name, Street, Latitude, Longitude, ZipCode, DistrictId, District, MunicipalityId, Municipality, ParishId, Parish, Phone, Fax, Director, Distance, servicesArray, IsAtService, IsLateNight);
}
}
return resultsArray; //array de instâncias do tipo SapoPharmacy, tal como descrito no Markup.
};
//usado para encapsular tipo de dados SapoPharmacyService
function SapoPharmacyService(date, type, lastUpdate){
this.Date=date;
this.Type=type;
this.LastUpdate=lastUpdate;
}
//usado para encapsular tipo de dados SapoPharmacy
function SapoPharmacy(lastUpdate, code, name, street, latitude, longitude, zipCode, districtId, district, municipalityId, municipality, parishId, parish, phone, fax, director, distance, services, isAtService, isLateNight)
{
this.LastUpdate = lastUpdate;
this.Code=code;
this.Name=name;
this.Phone=phone;
this.Fax=fax;
this.Director=director;
this.Distance=distance;
this.Services=services;
this.IsAtService=isAtService;
this.IsLateNight=isLateNight;
this.Street = street;
this.ZipCode = zipCode;
this.DistrictId = districtId;
this.District = district;
this.MunicipalityId = municipalityId;
this.Municipality = municipality;
this.ParishId = parishId;
this.Parish = parish;
this.Latitude = latitude;
this.Longitude = longitude;
}
//Utilizado pelo popfly quando o bloco é o último na cadeia, não entregando os resultados a outro bloco.
SapoPharmacy.prototype.toString = function(){
var sb = new Sys.StringBuilder();
sb.append("<div style='height:110px' class='farm-destaque'><h2>");
sb.append(this.Name);
sb.append("</h2>");
if (this.Distance>0){
sb.append("Diste2ncia ");
var dist = parseFloat(this.Distance);
dist=dist/1000;
sb.append(dist.toFixed(2));
sb.append(" Km <br />");
}
sb.append(this.Street);
sb.append("<br/>");
sb.append(this.ZipCode);
sb.append(" ");
sb.append(this.Parish);
sb.append("<br/>Tel. ");
sb.append(this.Phone);
sb.append("<br/><a href='http://mapas.sapo.pt/#c");
sb.append(this.Latitude);
sb.append("_");
sb.append(this.Longitude);
sb.append("_17'>Mapa</a></br></div>");
return sb.toString(); //tirar partido dos mapas do sapo para apresentar a localização da farmácia em questão
};
Este último método (toString), é chamado pelo Popfly quando o output do nosso mashup não é entregue a mais nenhum bloco, isto é, quando o nosso bloco está no fim da cadeia de invocação:
Entretanto reparei que a página dos mapas foi alterada, e os pedidos na forma _-_">http://mapas.sapo.pt/#c<LATITUDE>_-<LONGITUDE>_<NIVEL_ZOOM> que utilizava no toString, deixaram de funcionar desde que fiz o bloco (era apresentado um mapa centrado nas coordenadas especificadas, ao nível de zoom indicado)... Estive a ver assim meio à pressa e não vi como é o novo formato para obter o mesmo efeito. Se alguém souber, apite!
Pronto agora basta guardar, dar nome, etc etc.
Bem, agora que temos o nosso bloco criado, toca a fazer Mashups!
Criar um Mashup
Aqui vem a parte divertida da coisa!
Menu "Create Stuff"->"Mashup"
Do lado esquerdo da aplicação, existe uma lista de blocos já existentes. Mesmo no fim dessa lista, surgem os blocos criados por nós.
Lá deve residir o nosso Sapo Pharmacy. Cliquem nele (ou arrastem-no para a superfície de edição), e alterem as definições dos dados de entrada:
Aqui coloquem os valores que pretenderem (ou obtenham-nos através de outro bloco...)
Para um exemplo simples, vamos apenas exibir os resultados da pesquisa num mapa do Virtual Earth:
Agora basta fazer "Run", e voilá:
Agora que me preparava para juntar aqui o Bloco do Sapo Mapas que fiz também para o Sapo Code Bits, reparei que a malta do sapo mudou também o controlo dos mapas! Aqueles malandros... Lixaram-me a demo... Pronto, mas já deu para ficar com a ideia de como criar um bloco simples que consome dados de um Serviço. O dos mapas também tinha alguns conceitos engraçados, mas paciência... O código continua disponível no Popfly (tornei o bloco público), mas actualmente não funciona.
Para a próxima vez que não tiver nada para fazer num serão (ou que não me apeteça fazer o que devia :) ), apresento o Popfly Explorer. Uma das coisas interessantes que vi é a geração de blocos de forma automática a partir do contrato WSDL. Isto promete...