Neste codelab, você irá criar uma visualização de milhares de pontos de dados sobre um mapa interativo do Google Maps, iremos utilizar nestas implementações recursos do Polymer e dos Google Web Components para que possamos facilmente carregar os dados, ou seja, os pontos que iremos desenhar, processá-los dentro do WebGL, e então manipulá-los em tempo real. Tenha em mente que todo o código relacionado ao WebGL que iremos utilizar já está pronto, deixando assim você livre para alterá-lo caso seja necessário.

O que você aprenderá

Conhecimentos e recursos desejados

Como você avalia sua experiência com o Polymer?

Iniciante Intermediário Avançado

Crie um novo projeto

A primeira vez que você executar o Chrome Dev Editor, ele irá pedir para que você configure seu ambiente de trabalho.

Abra o Chrome Dev Editor, inicialize um novo projeto e siga os passos a seguir:

  1. Clique no ícone para iniciar um novo projeto.
  2. Escreva "PolymerVizCodelab" como Project name(Nome do projeto).
  3. No dropdown Project type(Tipo de projeto), escolha "JavaScript web app (using Polymer paper elements)".
  4. Clique no botão Create(Criar).

Após estas etapas, o Chrome Dev Editor irá criar a estrutura básica de sua aplicação Polymer. O Bower é utilizado em background para download e instalação da lista de dependências do projeto (que inclui a biblioteca principal do Polymer) dentro do diretório bower_components/, mas não se preocupe, você irá aprender mais sobre isso e também sobre o que é o Bower a seguir.

Sua estrutura de projeto deve ser semelhante a isto:

PolymerVizCodelab/
  bower_components/ <!-- Dependências instaladas via Bower -->
  bower.json  <!-- Metadados para o Bower gerenciar dependências -->
  index.html  <!-- sua aplicação -->
  main.js
  styles.css

Pré-visualizando a aplicação

A qualquer momento neste codelab você poderá pré-visualizar mudanças na aplicação, e para isso, basta selecionar o arquivo index.html e apertar o botão , com esta simples ação um servidor web será inicializado possibilitando a você navegar pelo conteúdo da página index.html.

Próximo passo

Neste momento, como observado acima, após a execução da nossa aplicação vemos que ela ainda não se parece muito com um mapa, sendo assim, vamos adicionar um mapa e desenhar algo nele.

O conjunto Google Web Components do Polymer contém o elemento <google-map> que possibilita a renderização de um mapa através da API do Google Maps. Para utilizá-lo, você precisará instalar este elemento utilizando o Bower.

Instalando o elemento <google-map>

Normalmente, você iria executar o comando bower install GoogleWebComponents/google-map --save na linha de comando para adicionar o elemento <google-map> como dependência do projeto, certo? Entretanto, com o uso do Chrome Dev Editor, basicamente eliminamos qualquer uso do Bower via linha de comando, no lugar, você precisará manualmente editar o arquivo bower.json incluindo a dependência google-map, e então executar a funcionalidade Bower Update, que por sua vez, irá verificar as dependências do arquivo bower.json instalando todas que forem necessárias.

  1. Altere o arquivo bower.json adicionando o elemento google-map como dependência:
"dependencies": {
  "iron-elements": "PolymerElements/iron-elements#^1.0.0",
  "paper-elements": "PolymerElements/paper-elements#^1.0.1",
  "google-map": "GoogleWebComponents/google-map#76a5c53"
}
  1. Clique com o botão direito no arquivo bower.json no editor.
  2. E no menu de contexto escolha a opção Bower Update(Atualizar Bower).

O download será feito em alguns segundos, e após finalizado, você poderá verificar o elemento <google-map> (e suas dependências) no diretório bower_components/google-map/.

Aplicando o elemento <google-map>

Para utilizar o elemento <google-map> você precisa:

  1. Usar um HTML Import para carregá-lo no arquivo index.html
  2. Declarar uma instância do elemento na página

No arquivo index.html, remova todos os outros HTML imports do <head> (deixe apenas o stylesheet, pois iremos utilizá-lo posteriormente), após a remoção aplique um único import apontando para google-map.html:

index.html

<head>
  ....
  <script src="bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
  <link rel="import" href="bower_components/google-map/google-map.html">

  <link rel="stylesheet" href="styles.css">
</head>

Em seguida, substitua o conteúdo do <body> com uma instância do <google-map>:

index.html

<body unresolved>
  <google-map latitude="37.779" longitude="-122.3892" zoom="13"></google-map>
</body>

Como você pode ver na declaração, o <google-map> é completamente declarativo. O mapa aqui será centralizado utilizando os atributos latitude e longitude e também o nível de zoom através do atributo zoom.

Adicionando estilo ao mapa

Se você executar o aplicativo agora, nada será exibido, pois para que o mapa seja exibido adequadamente é necessário prover ao container de exibição (neste caso, <body>) uma altura.

Para isso, abra o arquivo styles.css e substitua o conteúdo original pelo seguinte conteúdo:

styles.css

body, html {
  font-family: 'Roboto', Arial, sans-serif;
  height: 100%;
  margin: 0;
}

Se você executar a aplicação agora, irá ver um mapa centralizado em San Francisco e utilizando as cores padrões fornecidas pelo Google Maps, porém no nosso caso, tais cores são muito claras e poderão atrapalhar a análise dos dados que você irá colocar no mapa em próximos passos deste codelab, sendo assim, o que vamos utilizar para melhorar isso? O Google Maps provê uma extensa API para estilização de mapas disponibilizada no elemento <google-map> através do atributo styles. Por hora vamos manter o estilo simples diminuindo a saturação e tonar a água um pouco mais escura.

Substitua a nossa antiga simples declaração do elemento <google-map>, por:

index.html

<body unresolved>
  <google-map latitude="37.779" longitude="-122.3892" zoom="13"
    styles='[{"stylers":[{"saturation":-85}]},{"featureType":"water","stylers":[{"lightness":-20}]}]'>
  </google-map>
</body>

Executando a aplicação

Neste momento, se você ainda não o fez, pressione o botão . Após tudo que já realizamos até aqui, você deverá ver um mapa menos saturado exibindo o mapa de San Francisco.

Próximo passo

Vamos adicionar uma camada de dados ao mapa e finalmente visualizar alguma coisa nele!

Agora iremos utilizar o elemento <point-overlay>, que por sua vez, utiliza a biblioteca CanvasLayer para desenhar sobre o mapa. Embora a Google Maps API já forneça internamente diversas maneiras de desenhar dados em um mapa, a biblioteca CanvasLayer irá utilizar WebGL para renderização de conteúdo no mapa, possibilitando assim mais performance ao se desenhar milhões de itens em uma mesma tela em tempo real.

Instalando o elemento <point-overlay>

Nesta etapa, mais uma vez iremos utilizar o esquema de instalação de dependência provido pelo Bower para instalarmos o elemento. Edite o arquivo bower.json e adicione a declaração do elemento <point-overlay> nele:

bower.json

"dependencies": {
    "iron-elements": "PolymerElements/iron-elements#^1.0.0",
    "paper-elements": "PolymerElements/paper-elements#^1.0.1",
    "google-map": "GoogleWebComponents/google-map#76a5c53",
    "point-overlay": "googlecodelabs/polymer-webgl-visualization"
  }

Agora, clique com o botão direito sobre o arquivo bower.json e selecione a opção "Bower Update"(Atualizar Bower).

Para utilizar o elemento <point-overlay> você precisa:

  1. Usar um HTML Import para carregá-lo no arquivo index.html.
  2. Declarar uma instância do elemento na página.
  3. E por fim, precisamos "conectá-lo" ao nosso mapa para que ele entenda onde será preciso desenhar.

No arquivo index.html, adicione um HTML Import referenciando o arquivo point-overlay.html:

index.html

<head>
  ....
  <script src="bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
  <link rel="import" href="bower_components/google-map/google-map.html">
  <link rel="import" href="bower_components/point-overlay/point-overlay.html">

  <link rel="stylesheet" href="styles.css">
</head>

Declare o elemento <point-overlay> como um irmão do elemento <google-map>, e adicione o atributo data com o valor '[{"lat": 37.779, "lng": -122.3892}]'. E por fim, ao fazer isto, você estará desenhando o primeiro ponto no seu mapa.

index.html

<body unresolved>
  <google-map latitude="37.779" longitude="-122.3892" zoom="13"
    styles='[{"stylers":[{"saturation":-85}]},{"featureType":"water","stylers":[{"lightness":-20}]}]'>
  </google-map>
  <point-overlay data='[{"lat": 37.779, "lng": -122.3892}]'>
  </point-overlay>
</body>

Adicionando o overlay ao mapa

O elemento <point-overlay> pode fazer muito pouco sem um mapa para desenhar. O elemento <google-map> tem uma propriedade chamada map que é referente a instância de Google Map criada por ele. <point-overlay> também possui uma propriedade map, mas que por padrão vem vazia, e ela espera até que um objeto Map seja associado a ela para utilizá-la para desenhas. Você pode utilizar as técnicas de data-binding providas pelo Polymer e assim, fazer os dois elementos conversarem.

As funcionalidades de data-binding estarão disponíveis somente quando criamos um componente utilizando o <dom-module>, porém o Polymer fornece uma versão extendida do elemento <template> chamada "dom-bind", que basicamente irá de forma implícita fornecer funcionalidades do Polymer fora do contexto dele, ou seja, isto por exemplo permitirá o uso direto de {{}} para data-binding na sua página principal.

Sendo assim, com essa propriedade envolva todo o conteúdo dentro de <body> com a seguinte declaração <template is="dom-bind">, que por sua vez, possibilitará criar um contexto único do valor map, facilitando assim a atribuição do valor map onde você quiser.

index.html

<body unresolved>
  <template is="dom-bind">
    <google-map map="{{map}}" latitude="37.779" longitude="-122.3892" zoom="13"
      styles='[{"stylers":[{"saturation":-85}]},{"featureType":"water","stylers":[{"lightness":-20}]}]'>
    </google-map>
    <point-overlay map="[[map]]" data='[{"lat": 37.779, "lng": -122.3892}]'>
    </point-overlay>
  </template>
</body>

Agora, quando o elemento <google-map> é inicializado e a propriedade map é modificada de null para um objeto de mapa, a propriedade map do elemento <point-overlay> também irá ser modificada com este mesmo objeto de mapa. Observe que aqui estamos utilizando {{map}} como uma propriedade declarativa, você poderia utilizar qualquer nome para ela (por exemplo, map="{{foo}}").

Executando a aplicação

Pressione o botão . Neste ponto, você deverá ver um ponto desenhado no mapa utilizando as coordenadas fornecidas.

Próximo passo

Vamos utilizar mais funcionalidades de data-binding do Polymer para controlar o que é desenhado com WebGL através de uma interface gerada via HTML.

Vamos agora adicionar um simples slider para que possamos interagir com o código WebGL que desenha o ponto no seu mapa. Para isso, o Polymer oferece uma série de elementos para controle da entrada de dados; por hora, vamos começar com <paper-card> e <paper-slider>.

Adicionando o elemento <paper-slider>

Os elementos já foram importados pela entrada paper-elements no arquivo bower.json, então no index.html, adicione os novos HTML Imports para estes componentes dentro do elemento <head>:

index.html

<head>
  ....
  <link rel="import" href="bower_components/point-overlay/point-overlay.html">
  <link rel="import" href="bower_components/paper-card/paper-card.html">
  <link rel="import" href="bower_components/paper-slider/paper-slider.html">

  <link rel="stylesheet" href="styles.css">
</head>

No final do corpo do seu template, adicione o elemento <paper-slider> dentro do elemento <paper-card>:

index.html

<template is="dom-bind">
  ...
  <point-overlay map="[[map]]" data='[{"lat": 37.779, "lng": -122.3892}]'>
  </point-overlay>
    
  <paper-card elevation="2">
    <paper-slider min="5" max="100" value="30">
    </paper-slider>
  </paper-card>
</template>

No arquivo styles.css adicione regras para colocar o card no lado superior direito do mapa.

styles.css

body, html {
  ...
}

paper-card {
  position: absolute;
  top: 25px;
  right: 25px;
}

Conectando o slider ao WebGL uniform

Agora, para fazer com que o slider faça o controle, precisamos conectar o valor dele ao ponto desenhado no mapa, para isso, o elemento <point-overlay> usa WebGL para desenhar sobre o mapa, e este por sua vez, utiliza-se de shaders para desenhar.

Shaders possuem um conjunto de parâmetros chamados de uniforms que podem ser configurados via código e executados pela CPU que por sua vez refletirá no shader que é executado na GPU. O elemento <point-overlay> pega automaticamente alguns uniforms declarados no código e os expõe em suas propriedades uniforms, possibilitando atribuir valores através do Polymer. O shader padrão do elemento <point-overlay> tem um uniform chamado pointSize, que é exatamente o qual você irá utilizar.

Uma vez que pointSize é uma propriedade do objeto uniforms, você precisará declará-lo através do seu caminho, e para isso:

  1. Atribua o valor da propriedade uniforms do elemento <point-overlay> a variável (chamada de "uniforms") dentro do template.
  2. Atribua o valor da propriedade immediateValue do elemento <paper-slider> a propriedade pointSize do uniforms.

index.html

<template is="dom-bind">
  ...
  <point-overlay map="[[map]]" uniforms="{{uniforms}}" data='[{"lat": 37.779, "lng": -122.3892}]'>
  </point-overlay>

  <paper-card elevation="2">
    <paper-slider min="5" max="100" value="30" immediate-value="{{uniforms.pointSize}}">
    </paper-slider>
  </paper-card>
</template>

Executando a aplicação

Clique no botão e você deverá conseguir controlar o tamanho do ponto desenhado através do slider!

Próximo passo

Vamos desenhar vários pontos no mapa.

Incluído com o <point-overlay> há um conjunto de dados de exemplo do NOAA's Storm Prediction Center Severe Weather Database com coordenadas de todos os tornados registrados nos (contíguo) Estados Unidos de 1950 até 2014. A propriedade data do <point-overlay> irá pegar qualquer array de objetos JSON que possua propriedades de lat e lng para saber onde desenhar estes pontos e nosso conjunto de dados de exemplo já está formatado desta maneira.

Carregando os dados

Para carregar os dados, você poderá utilizar do elemento <iron-ajax> do Polymer. E como feito anteriormente, você precisará importá-lo no head do documento:

index.html

<head>
  ...
  <link rel="import" href="bower_components/iron-ajax/iron-ajax.html">
</head>

Após o elemento <point-overlay> adicione o elemento <iron-ajax> configurando-o para carregar automaticamente o arquivo JSON:

index.html

<template is="dom-bind">
  <google-map map="{{map}}" latitude="39.788" longitude="-105.887" zoom="4"
styles='[{"stylers":[{"saturation":-85}]},{"featureType":"water","stylers":[{"lightness":-20}]}]'>
  </google-map>
  <point-overlay map="[[map]]" uniforms="{{uniforms}}" data="{{data}}">
  </point-overlay>
  <iron-ajax
    auto
    url="bower_components/point-overlay/tornadoes-1950-2014.json"
    handle-as="json"
    last-response="{{data}}">
  </iron-ajax>
  ...
</template>

Observe que adicionalmente a inclusão do elemento <iron-ajax>, a propriedade data do elemento <point-overlay> é atribuída à propriedade lastResponse do elemento <iron-ajax>. Sendo assim, quando o arquivo JSON for carregado, o elemento <point-overlay> será populado com dados e iniciará o processo de desenho no mapa.

E por fim, você pode querer melhorar a perspectiva do mapa para uma melhor experiência.

Para isso siga a declaração abaixo como uma das possibilidades:

index.html

<google-map map="{{map}}" latitude="39.788" longitude="-105.887" zoom="5"
styles='[{"stylers":[{"saturation":-85}]},{"featureType":"water","stylers":[{"lightness":-20}]}]'>
</google-map>

Executando a aplicação

Finalmente, pressione o botão e você deverá ver cada local dos Estados Unidos em que ocorreu um tornado nos últimos 64 anos. Você pode precisar ajustar o pointSize slider para ver todos os pontos claramente no seu nível de zoom favorito.

Próximo passo

Ao reajustar o zoom, podemos perceber que os pontos do mapa não são reajustados de acordo com o nível de zoom preferido, e por isso, no próximo passo vamos conectar o controle de zoom do mapa para reajustar o tamanho dos pontos automaticamente caso exista interação com controle de nível de zoom do mapa, e assim, otimizar ainda mais a experiência de visualização.

Adicionando uma função pointSize-changing

Não há uma forma fácil de mapear do nível de zoom para o pointSize que possa ser expressada em um Polymer binding, então utilizaremos uma função JavaScript comum para ajustar o tamanho baseado no zoom. A função a ser chamada precisa ser adicionada em algum lugar e como os bindings variáveis das propriedades em nossa página, a função pointSize-changing pode ficar envolvida pelo elemento <template> dom-bind.

Adicione um elemento <script> ao final de body e nele encontre o elemento <template> e adicione a função:

index.html

<body unresolved>
  <template is="dom-bind" id="t">
    ...
  </template>
  <script>
    var t = document.querySelector('#t');
    t.setPointSize = function(e) {
      this.uniforms.pointSize = Math.exp(0.3 * this.map.getZoom());
      this.notifyPath('uniforms.pointSize', this.uniforms.pointSize);
    };
  </script>
</body>

Note algumas coisas sobre a mudança:

Disparando Eventos

Agora setPointSize precisa ser chamado somente nos momentos certos. Um computed binding é uma forma de fazê-lo, utilizando um binding especial para chamar uma função quando suas entradas mudarem, mas como pointSize é acessado através de um path binding (ele não é uma propriedade direta de <point-overlay>), isto se torna difícil de expressar na sintaxe de binding do Polymer. Ao invés disso, vamos confiar nos eventos do elemento <google-map> para controlar as mudanças no pointSize.

Como queremos que a função rode em cada mudança do nível de zoom, nós podemos usar o evento zoom-changed do elemento <google-map> para disparar a função. O Polymer provê um atalho conveniente para configurar event listeners associando o atributo on-event-name (neste caso on-zoom-changed) em nosso elemento para o nome de nossa função.

Apesar do zoom-changed somente disparar quando o zoom mudar; isto ainda deixa o valor inicial de pointSize não configurado ou com um valor default. Como a configuração de diversos valores iniciais na web, "timing" é um problema: nós não queremos que setPointSize seja chamado muito cedo — Polymer teria sido inicializado ainda e todas as suas variáveis ainda estariam não inicializadas — e nós não queremos que essa função seja chamada muito tarde — pontos de dados default teriam sido desenhados na tela e os usuários veriam um Flash Of Unstyled pointSize (FOUpS).

O elemento <google-map> fornece outro evento que resolve este problema: google-map-ready, disparado quando a Maps API foi totalmente carregada e está pronta para ser utilizada. Isto é perfeito pois significa que this.map.getZoom() será resolvido corretamente, mas nós ainda estaremos no mesmo loop de eventos que a inicialização do mapa, então nada terá sido desenhado no mapa ainda.

Para terminar este passo, configure os eventos on-zoom-changed e on-google-map-ready para setPointSize, e, finalmente, remova o binding do uniforms.pointSize para o immediateValue do <paper-slider>. Você pode deixar o elemento <paper-slider> pois nós precisaremos dele em um passo futuro deste codelab.

Tudo Junto Agora

Toda essa implementação precisou de diversas mudanças, portanto aqui estão todas alterações no nosso arquivo index.html para te dar uma ideia de como ele deve estar agora (considere quaisquer alterações que você tenha feito adicionalmente durante o codelab):

index.html

<!doctype html>

<html>
<head>
  <title>PolymerVizCodelab</title>

  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-capable" content="yes">

  <script src="bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>

  <link rel="import" href="bower_components/google-map/google-map.html">
  <link rel="import" href="bower_components/point-overlay/point-overlay.html">
  <link rel="import" href="bower_components/paper-card/paper-card.html">
  <link rel="import" href="bower_components/paper-slider/paper-slider.html">
  <link rel="import" href="bower_components/iron-ajax/iron-ajax.html">

  <link rel="stylesheet" href="styles.css">
</head>

<body unresolved>
  <template is="dom-bind" id="t">
    <google-map
      map="{{map}}"
      latitude="39.788" longitude="-105.887"
      zoom="5"
      styles='[{"stylers":[{"saturation":-85}]},{"featureType":"water","stylers":[{"lightness":-20}]}]'
      on-google-map-ready="setPointSize"
      on-zoom-changed="setPointSize">
    </google-map>
    <point-overlay map="[[map]]" uniforms="{{uniforms}}" data="{{data}}"></point-overlay>
    <iron-ajax
      auto
      url="bower_components/point-overlay/tornadoes-1950-2014.json"
      handle-as="json"
      last-response="{{data}}">
    </iron-ajax>
    
    <paper-card elevation="2">
      <paper-slider min="5" max="50" pin value="30"></paper-slider>
    </paper-card>
  </template>
  
  <script>
    var t = document.querySelector('#t');
    t.setPointSize = function(e) {
      this.uniforms.pointSize = Math.exp(0.3 * this.map.getZoom());
      this.notifyPath('uniforms.pointSize', this.uniforms.pointSize);
    };
  </script>
</body>
</html>

Executando a Aplicação

Pressione o botão ! Agora com o pointSize sendo gerenciado automaticamente o mapa deve estar com uma aparência interessante em qualquer nível de zoom.

Próximo Passo

Ainda temos um mar de diversos pontos laranjas. Agora é hora de estilizar cada ponto baseando-se em seus aspectos únicos.

Neste codelab você construiu o início de uma aplicação para exibição de quantidades massivas de pontos geográficos. Aprendeu que o elemento <point-overlay> abstrai alguns dos detalhes pra você, mas você ainda pode obter o objeto CanvasLayer do elemento <point-overlay> e até mesmo acessar o próprio <canvas>. Tudo pode ser modificado, dos dados até os shaders utilizados.

E agora que você começou, mude qualquer coisa para ver o que mais você pode fazer.

Outras referências

Polymer

Outros