Mostrando postagens com marcador Mapas. Mostrar todas as postagens
Mostrando postagens com marcador Mapas. Mostrar todas as postagens

terça-feira, 13 de janeiro de 2015

Dominando o Android - Atualização 1 - Capítulo 14

Olá pessoal,

Acho que com o livro as atualizações aqui do blog ficarão mais frequentes. Pelo menos é o que eu espero. Vou fazer o possível para colocar as correções/atualizações aqui através de mini-posts falando rapidamente sobre a mudança e o por quê.

Para quem não sabe, o código-fonte dos exemplos do livro estão disponíveis no meu gitHub (https://github.com/nglauber/dominando_android). E lá vocês vão ver dois branchs master e master_updates. O código que estiver com algum bug, eu vou corrigir e colocar no branch master e o que for atualização ficará no branch master_updates.
Também criei um grupo de discussão (http://groups.google.com.br/d/forum/livro-dominando-o-android) que todos podem se inscrever.

Recebi hoje uma avaliação do livro que informava que o mesmo "tinha uma falha grave" e "estava desatualizado" porque eu não estou utilizando API do Google Play Services 6.5. E que uma classe (a LocationClient) que eu usava no exemplo não existia.

Não sei se todos sabem, mas o processo de finalização de um livro requer algumas etapas que demandam tempo: revisão ortográfica, diagramação, checar figuras, referências, tempo de impressão na gráfica, etc.  A versão 6.5 do Google Play Services foi anunciada no dia 17/11/2014, mas só foi disponibilizada para os desenvolvedores dia 08/12/2014. Época que o livro já estava com a editora em processo de finalização e impressão. Sendo assim, não pude atualizar o capítulo 14 (Mapas e Localização) a tempo. Sendo assim, peço desculpas pelo meu livro ter ficado desatualizado, por conta de alguns dias... :(
Mas quero reiterar que o código apresentado no livro funciona perfeitamente do jeito que ele se encontra.

--

Pedido de desculpas feito, vamos ao que mudou! As alterações feitas em relação ao exemplo do livro estão nesse commit aqui no branch master_updates, uma vez que é uma atualização da nova API e não uma correção de um erro do livro.

Conforme o comentário do leitor, a classe com.google.android.gms.location.LocationClient não existe mais. Mas seus métodos foram distribuídos em outras classes.
  • Na classe GeofenceReceiver, os eventos de Geofence podem ser tratados com a classe GeofencingEvent.
  • Na classe MainActivity, muito do que era feito com a LocationClient passou a ser responsabilidade da GoogleApiClient, inclusive os eventos de conexão (ConnectionCallbacks e OnConnectionFailedListener).
  • Ainda na MainActivity para obter a localização atual passamos a utilizar a classe LocationServices.FusedLocationApi. Bem como para requisitar atualizações sobre mudança na localização.
  • E finalmente, para adicionar um Geofence, usamos a classe LocationServices.GeofencingApi.
Isso é um resumo pessoal. Quem quiser mais informações sobre essa nova API, recomendo esses links:



Qualquer dúvida, sugestão ou crítica serão muito bem-vindas.

4br4ç05,
nglauber

domingo, 30 de dezembro de 2012

Android Google Maps v2

Olá povo,

No último dia 03/12/2012  a Google disponibilizou a versão 2.0 da API do Google Maps para Android que trouxe diversas melhorias em relação a versão anterior. O problema é que tudo mudou completamente :) Desde a geração da chave de acesso até as classes envolvidas. A parte boa disso tudo, é que essa API funciona desde a versão 2.2 do Android e trouxe melhorias significativas \o/
Nesse post vou mostrar como dar os primeiros passos nessa nova API.

Gerando a chave de acesso

Quem já fez algum exemplo sabe que precisamos de uma chave de acesso para utilizar o serviço de mapas. Para ter acesso aos mais diversos serviços do Google, precisamos criar um projeto no site do Google APIs Console. Uma vez que o projeto foi criado, acesse a opção Services e habilite o serviço Google Maps Android API v2. Uma vez habilitado o serviço, acesse a opção API access depois clique no botão Create new Android key, no popup que for exibido clique em Create.
Será gerada uma chave no padrão SHA-1 que usaremos no nosso código similar a essa: AIzbSyCyoMobvh72ZrSv4xQddOLzDOlaLqCcILU

Biblioteca dos serviços do Google Play
A segunda etapa é baixar o pacote de serviços do Google Play. Abra o Android SDK Manager e baixe o Google Play services.
Quando terminar o download, será criado o seguinte diretório [android-sdk/extras/google/google_play_services/libproject] que contém as classes necessárias para usar a nova API. Precisamos importar esse projeto para dentro do Eclipse, pois nosso projeto irá referencia-lo.
Acesse a opção File | New project... Em seguida, selecione Android | Android Project From Existing Code e então selecione o diretório citado anteriormente. O projeto será importado para o Eclipse e agora podemos começar nosso projeto de exemplo.

Projeto de exemplo
Crie um novo projeto Android e vamos começar a brincadeira :) A primeira coisa é referenciar o projeto  do Google Play Service dentro do nosso projeto. Para isso, clique com o botão direito sobre o projeto e selecione  Properties. Na janela que for exibida, selecione Android do lado esquerdo. Clique no botão Add... e selecione google-play-services_lib e então clique em Ok.
Estamos prontos pra começar, deixe o AndroidManifest.xml como abaixo.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android=
  "http://schemas.android.com/apk/res/android"
  package="ngvl.testegmaps_v2"
  android:versionCode="1"
  android:versionName="1.0" >

  <uses-sdk
    android:minSdkVersion="9"
    android:targetSdkVersion="15" />

  <uses-permission android:name=
    "android.permission.INTERNET" />
  <uses-permission android:name=
    "android.permission.ACCESS_NETWORK_STATE"/>
  <uses-permission android:name=
    "android.permission.WRITE_EXTERNAL_STORAGE" />
  <uses-permission android:name=
    "com.google.android.providers.gsf.permission.READ_GSERVICES" />

  <uses-feature
    android:glEsVersion="0x00020000"
    android:required="true" />

  <application
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
    <activity
      android:name="ngvl.testegmaps_v2.MainActivity"
      android:label="@string/app_name" >
      <intent-filter>
        <action android:name=
          "android.intent.action.MAIN" />
        <category android:name=
          "android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>

    <meta-data android:name=
      "com.google.android.maps.v2.API_KEY"
      android:value="suaApiKeyAqui!!!!!!" />
  </application>
</manifest>

Vamos comentar alguns detalhes do arquivo. Começamos utilizando as permissões de INTERNET e WRITE_EXTERNAL_STORAGE para podermos baixar os mapas e fazer cache dos mesmos no cartão de memória. Um requisito dessa nova API é que o aparelho deve suportar OpenGL que declaramos logo em seguida. A última configuração importante que fizemos aqui é a tag meta-data que declaramos a chave do Google Maps que geramos na primeira etapa desse post.
Agora deixe o arquivo de layout conforme abaixo.
<fragment
  xmlns:android=
    "http://schemas.android.com/apk/res/android"
  android:id="@+id/map"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:name=
    "com.google.android.gms.maps.SupportMapFragment" />
Aqui temos uma grande mudança: a classe MapView e MapActivity não são mais utilizadas. Em seu lugar entrou a classe MapFragment, mas como quis deixar a compatibilidade com versões anteriores do Android, usei a SupportMapFragment. Isso nos permite utilizar mapas em fragments, o que só era possível anteriormente através de APIs de terceiros, uma vez que tínhamos que herdar de MapActivity. Outro detalhe é que o nome do pacote também mudou, agora é com.google.android.gms.maps.
Por fim, vamos ao código da Activity.

package ngvl.testegmaps_v2;

import android.os.Bundle;
import android.support.v4.app.*;

import com.google.android.gms.common.*;
import com.google.android.gms.maps.*;
import com.google.android.gms.maps.model.*;

public class MainActivity extends FragmentActivity {

  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
 
    SupportMapFragment fragment =
     (SupportMapFragment)getSupportFragmentManager()
       .findFragmentById(R.id.map);
    GoogleMap map = fragment.getMap();

    LatLng latLng = new LatLng(-23.561706,-46.655981);
    map.addMarker(new MarkerOptions()
      .position(latLng)
      .icon(BitmapDescriptorFactory.fromResource(
        R.drawable.ic_launcher))
      .title("Av. Paulista")
      .snippet("São Paulo"));
  
    configuraPosicao(map, latLng);
  }

  private void configuraPosicao(
    GoogleMap map, LatLng latLng) {

    map.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
    map.animateCamera(
     CameraUpdateFactory.newLatLngZoom(latLng, 17.0f));
  }
}
Nossa classe herda de FragmentActivity da API de compatibilidade do Google que falei aqui. Em seguida, obtemos a referência para o MapFragment, e a partir dele obtemos a instância do GoogleMaps (que substitui o MapView). A classe GeoPoint foi substituída pela classe LatLng que representa uma coordenada geográfica. Com essa informação, adicionamos um ponto no mapa, passando uma imagem que servirá de marcador no mapa, um título e um resumo, que serão exibidos ao clicar na imagem no mapa. O método configuraPosicao, define o tipo de mapa para satélite e o zoom para 17 (os valores variam de 2 a 21, sendo 21 mais próximo possível).
Se rodarmos nossa aplicação, em um aparelho 2.3 ficará similar a figura abaixo:
Uma das novidades dessa versão da API do GoogleMaps é poder visualizar o mapa em relevo, mostrando, por exemplo, os prédios do local selecionado no mapa. Modifique o método configuraPosicao para ficar como abaixo:
private void configuraPosicao(
  GoogleMap map, LatLng latLng) {
  map.moveCamera(
   CameraUpdateFactory.newLatLngZoom(latLng, 15));
  map.animateCamera(
   CameraUpdateFactory.zoomTo(10), 2000, null);

  CameraPosition cameraPosition = 
    new CameraPosition.Builder()
      .target(latLng)   
      .zoom(17)     
      .bearing(90)
      .tilt(45)
      .build();
  map.animateCamera(
    CameraUpdateFactory.newCameraPosition(
      cameraPosition));
}
Notem que não usamos o modo satélite aqui (não ficou muito bom quando usei :) mas posicionamos a câmera de uma maneira diferente. Além da Latitude/Longitude e do Zoom, setamos o bearing (rotação do mapa em 90 graus) e o tilt (inclinação em 45 graus). O resultado pode ser visto abaixo.
Bem pessoal, espero que tenham gostado e qualquer dúvida, deixem seus comentários.
Mais detalhes em: https://developers.google.com/maps/documentation/android/

 4br4ç05,
nglauber

segunda-feira, 24 de setembro de 2012

Mapas no iOS

Olá povo,

Esse post é grande, mas é bem legal. Nele, vou falar sobre a utilização de Mapas no iOS. Conheceremos as principais classes do MapKit, e além desse objetivo principal, utlizaremos um conceito interessante do Objective-C, os blocks.
Para começar crie um novo projeto no Xcode (pode ser Single View Application sem utilizar Storyboards). Adicione os frameworks MapKit e CoreLocation à sua aplicação. Em seguida, crie uma nova classe chamada Endereço e deixe-a conforme abaixo:
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface Endereco : NSObject<MKAnnotation>

@property (strong, nonatomic) NSString *titulo;
@property (strong, nonatomic) NSString *subtitulo;

- (id)initWithCoordinate:(CLLocationCoordinate2D) c;
@end
#import "Endereco.h"

@implementation Endereco

@synthesize titulo, subtitulo;

@synthesize coordinate;

- (id)initWithCoordinate:(CLLocationCoordinate2D) c {
    coordinate = c;
    NSLog(@"%f,%f", c.latitude, c.longitude);
    return self;
}

- (NSString *)subtitle {
    return subtitulo;
}

- (NSString *) title {
    return titulo;
}
@end
A classe acima está implementando o protocolo MKAnnotation que exige que exista uma propriedade coordinate e os métodos subtitle e title. Isso porque, usaremos essa classe para representar um ponto no Mapa.
Agora deixe o ViewController da sua aplicação conforme abaixo (o nome da classe obviamente pode ser alterado...).
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#import "Endereco.h"

@interface NGViewController : UIViewController 
  <UISearchBarDelegate, MKMapViewDelegate>

@property (weak, nonatomic) IBOutlet 
  UISearchBar *searchBar;
@property (weak, nonatomic) IBOutlet 
  MKMapView *mapa;
@property (weak, nonatomic) IBOutlet 
  UISegmentedControl *segMapType;
@property (strong, nonatomic) 
  Endereco *endereco;

- (IBAction)mapViewChanged:(id)sender;

@end
#import "NGViewController.h"
#import "Endereco.h"

@implementation NGViewController

@synthesize searchBar;
@synthesize mapa;
@synthesize segMapType;
@synthesize endereco;

#define API_KEY @"SUA_CHAVE"
#define TEMPLATE_URL 
  @"http://maps.google.com/maps/geo?q=%@&output=csv&key=%@"

#pragma mark - View lifecycle
- (void)viewDidUnload {
    [self setSearchBar:nil];
    [self setMapa:nil];
    [self setSegMapType:nil];
    [super viewDidUnload];
}

#pragma mark - SegmentedControl value changed
- (IBAction)mapViewChanged:(id)sender {
  // De acordo com a posição selecionada no 
  // SegmentedControl, mudamos o tipo de 
  // visualização do mapa
  switch ([segMapType selectedSegmentIndex]) {
    case 0: mapa.mapType = MKMapTypeStandard;  break;
    case 1: mapa.mapType = MKMapTypeSatellite; break;
    case 2: mapa.mapType = MKMapTypeHybrid;    break;
  }
}

#pragma mark - SearchBar methods
-(void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
  // Ao clicar em cancelar na barra de busca, 
  // ocultamos o teclado
  [self.searchBar resignFirstResponder];
}

- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
  [self.searchBar resignFirstResponder];    
    
  [self retrieveLocation:self.searchBar.text];
}

#pragma mark - Utility Methods

- (void) retrieveLocation:(NSString *)search {
  // Criando URL de requisição
  NSString *query = 
    [search stringByAddingPercentEscapesUsingEncoding:
      NSUTF8StringEncoding]; 
   
  NSString *urlString = [NSString stringWithFormat:
    TEMPLATE_URL, query, API_KEY];
    
  // Preparando request
  NSURLRequest *request = [[NSURLRequest alloc]
    initWithURL:[NSURL URLWithString:urlString]];
    
  // Declarando o Block "trataResposta" para tratar 
  // retorno. Esse block é uma função que retorna 
  // void e recebe 3 parâmetros: uma response, os 
  // dados e um erro. 
  id trataResposta = ^(NSURLResponse *response, 
    NSData *data, NSError *error) {
      if (error){
        [self showErrorDialog];
            
      } else {
        [self trataResposta:data];
      }
    };
    
  // Fazendo a requisição assícrona para evitar que 
  // a tela pareça estar travada. Aqui usamos o 
  // recurso de blocks, declarado acima e que será 
  // chamado após a requisição ser concluída
  [NSURLConnection sendAsynchronousRequest:request 
    queue:[NSOperationQueue mainQueue] 
      completionHandler:trataResposta];
}

- (void) trataResposta:(NSData *)data {
  NSString *locationString = [[NSString alloc]
    initWithData:data encoding:NSUTF8StringEncoding];

  // Nesse array, o primeiro elemento é o código de 
  // resposta HTTP (200 é sucesso) os próximos (se 
  // deu tudo certo) é a latitude e longitude.
  NSArray *listItems = [locationString 
    componentsSeparatedByString:@","];
    
  double longitude = 0;
  double latitude = 0;
    
  if ([listItems count] > 0 && 
     [[listItems objectAtIndex:0] 
       isEqualToString:@"200"]){

    latitude = [[listItems objectAtIndex:2] 
      doubleValue];
    longitude = [[listItems objectAtIndex:3] 
      doubleValue];
  }
    
  // Cria o objeto location fazer obter informações 
  // do local via GeoCoder
  CLLocation *location = [[CLLocation alloc] 
    initWithLatitude:latitude longitude:longitude];
  [self retrieveGeocoder:location];
}

- (void) retrieveGeocoder:(CLLocation *)location {
  CLGeocoder *geocoder = [[CLGeocoder alloc] init];
    
  // Mais uma chamada assícrona que recebe 
  // um block como parâmetro
  [geocoder reverseGeocodeLocation:location 
    completionHandler:
      ^(NSArray *placemarks, NSError *error) {
         
    if ([placemarks count] > 0){
             
      CLPlacemark *placemark = 
        [placemarks objectAtIndex:0];
             
      [self addAnnotation:location.coordinate 
         withMark:placemark];
             
    } else {
      [self showErrorDialog];
    }
  }];
}

- (void)showErrorDialog {
  UIAlertView *alert = [[UIAlertView alloc] 
    initWithTitle:@"Erro" 
    message:@"Erro ao obter dados da localização" 
    delegate:nil cancelButtonTitle:@"OK" 
    otherButtonTitles:nil, nil];
  [alert show];
}

- (void)addAnnotation:
  (CLLocationCoordinate2D)coordinate 
   withMark:(CLPlacemark *)placemark {

  // Se já houver uma marcação no mapa, remova
  if (self.endereco != nil) {
    [mapa removeAnnotation:self.endereco];
    self.endereco = nil;
  }
    
  // Criando MKNotation para adicionar no Mapa
  self.endereco = [[Endereco alloc]
  initWithCoordinate:coordinate];
    
  // Configura o título e subtítulo da MKNotation 
  // baseado no CLPlacemark
  self.endereco.titulo = [NSString stringWithFormat:
    @"%@, %@\n%@", 
      placemark.locality, 
      placemark.administrativeArea, 
      placemark.postalCode];
    
  self.endereco.subtitulo = 
    [NSString stringWithFormat:@"%@, %f,%f", 
      placemark.thoroughfare,
      coordinate.latitude, 
      coordinate.longitude];
    
  // Adiciona a notação ao mapa
  [self.mapa addAnnotation:self.endereco];

  /* A MKCoordinateSpan representa a quantidade 
     de área usada pela view e é usada para 
     determinar o zoom. Quanto maior o valor, 
     maior a área visível. */
  MKCoordinateSpan span;
  span.latitudeDelta = 0.02;
  span.longitudeDelta = 0.02;
    
  // A MKCoordinateRegion serve para movimentar o 
  // mapa até a região determinada no MKNotation
  MKCoordinateRegion region;
  region.center = coordinate;
  region.span = span;
    
  [self.mapa setRegion:region animated:YES];
  [self.mapa regionThatFits:region]; 
}

#pragma mark - MapView
-(MKAnnotationView *)mapView:(MKMapView *)mapView 
  viewForAnnotation:(id<MKAnnotation>)annotation {
  
  /* Ao adicionar uma notação no mapa, esse método 
     é chamado. É aqui que criamos o pin que aparece
     no mapa */
  MKPinAnnotationView *pin = 
    [[MKPinAnnotationView alloc] initWithAnnotation:
      annotation reuseIdentifier:@"currentLoc"];
    
  pin.pinColor = MKPinAnnotationColorGreen;
  pin.animatesDrop = TRUE;
  pin.canShowCallout = YES;
  pin.calloutOffset = CGPointMake(-5, 5);
    
  return pin;
}

@end
O código está comentado, qualquer dúvida, deixe seus comentários. Agora abra o XIB referente a essa tela e adicione uma SearchBar, um MapView e um SegmentedControl de modo que ele fique conforme a figura abaixo:
No SearchBar, ligue-o com a propriedade searchBar e sete o delegate. Para o MapView, faça o mesmo com a propriedade mapa. E por fim, com o SegmentedControl ligue-o com a propriedade segMapType,  e no evento Value Changed ligue com o método mapViewChanged.

Agora é só rodar e pesquisar pelos lugares desejados.
Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 12 de dezembro de 2011

Google Maps: Traçando Rotas

[EDITADO EM 03/04/2013: atualizado para Google Api v2]

Olá povo,

A API do Google Maps disponível para Android é muito bacana, e incorporar esse recurso na sua aplicação pode ser um bom atrativo. Em um dos projetos que trabalhei, precisava mostrar a rota entre dois pontos no mapa, e a Google disponibilizou em 27/07/12 uma API Web que retorna a rota entre duas coordenadas geográficas no formato JSON (ou XML se preferir).

Nesse arquivo, temos uma lista com as coordenadas geográficas que devem ser percorridas desde o ponto de partida até o destino. Com essas coordenadas lidas do JSON, criamos uma lista de LatLng que devem ser desenhadas no objeto GoogleMap através de uma PolyLine.

Não vou detalhar muito a configuração do Google Maps v2 no Android pois já falei nesse post aqui. Também não vou entrar em detalhes sobre leitura de JSON pois falei nesse post aqui. E se você tiver dúvidas sobre AsyncTask, dê uma olhada nesse post.

Vou começar pelo código da Activity mostrado abaixo. No onCreate pegamos a instância de GoogleMap e definimos a posição inicial no mapa e um zoom padrão. Em seguida iniciamos a AsyncTask passando a GoogleMap no construtor e informando a latitude e longitude de origem e destino.
public class TesteRotaMapaActivity 
  extends FragmentActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
  
    SupportMapFragment fragment = 
      (SupportMapFragment)
        getSupportFragmentManager()
          .findFragmentById(R.id.map);

    GoogleMap map = fragment.getMap();

    LatLng latLng = new LatLng(-8.102739,-34.89917);
  
    map.animateCamera(
      CameraUpdateFactory.newLatLngZoom(latLng, 12.0f));
  
    new RotaAsyncTask(this, map).execute(
      // Latitude, Logintude de Origem
      latLng.latitude, latLng.longitude,    
      // Latitude, Longitude de Destino
      -23.604262,-46.676513);  
    }
}

A classe RotaAsyncTask é quem faz o trabalho de acessar o site do Google Maps, baixar o JSON com a informação da rota.
public class RotaAsyncTask extends 
  AsyncTask<Double, Void, Void>{

  private ProgressDialog dialog;
  private GoogleMap mapView;
  private Context context;
  private Route rota;

  public RotaAsyncTask(Context ctx, GoogleMap mapa) {
    mapView = mapa;
    context = ctx;
  }
 
  @Override
  protected void onPreExecute() {
    super.onPreExecute();
    dialog = ProgressDialog.show(
      mapView.getContext(), "Aguarde", 
      "Calculando rota");
  }
 
  @Override
  protected Void doInBackground(Double... params) {

    rota = directions(
      new LatLng(params[0], params[1]), 
      new LatLng(params[2], params[3]));
    return null;
  }
 
  @Override
  protected void onPostExecute(Void result) {
    super.onPostExecute(result);
    PolylineOptions options = new PolylineOptions()
      .width(5)
      .color(Color.RED)
      .visible(true);

    for (LatLng latlng : rota.getPoints()) {
      options.add(latlng);
    }

    mapView.addPolyline(options);
    dialog.dismiss();
  }
 
  private Route directions(
    final LatLng start, final LatLng dest) {
  
    // Formatando a URL com a latitude e longitude  
    // de origem e destino.  
    String urlRota = String.format(Locale.US,
      "http://maps.googleapis.com/maps/api/"+
      "directions/json?origin=%f,%f&"+
      "destination=%f,%f&" +
      "sensor=true&mode=driving",   
      start.latitude, 
      start.longitude, 
      dest.latitude, 
      dest.longitude); 
  
    GoogleParser parser;
    parser = new GoogleParser(urlRota);
    return parser.parse();
  }
}

Quem viu o post anterior pode notar que houveram algumas modificações. Delegamos o parse do JSON para a classe GoogleParser e exibimos as rotas utilizando a classe PoluLineOptions (da API v2) com ajuda da classe Route. Essas novas classes foram extraídas desse post aqui. Graças ao comentário do Hadson Gomes.
Eu peguei essas classes e deixei apenas o necessário. Vejamos elas abaixo.
public class Route {
  private final List<LatLng> points;
  private String polyline;

  public Route() {
    points = new ArrayList<LatLng>();
  }

  public void addPoints(final List<LatLng> points) {
    this.points.addAll(points);
  }

  public List<LatLng> getPoints() {
    return points;
  }

  public void setPolyline(String polyline) {
    this.polyline = polyline;
  }

  public String getPolyline() {
    return polyline;
  }
}
Note que a classe acima tem uma propriedade chamada polyline. Nesse JSON, a rota retorna algumas coordenadas "macro" digamos assim. Por isso os comentários abaixo informando que eram traçadas linhas retas no caminho. A rota com as coordenadas mas precisas estão codificadas em base64 no atributo polyline. A classe GoogleParser faz essa conversão de base64 para GeoPoint conforme abaixo.
public class GoogleParser {

  protected URL feedUrl;

  public GoogleParser(String feedUrl) {
    try {
      this.feedUrl = new URL(feedUrl);
    } catch (MalformedURLException e) {
    }
  }
 
  public Route parse() {
    // Cria uma rota vazia
    final Route route = new Route();
    try {
      // Obtém a String do JSON
      final String result = 
        convertStreamToString(
          feedUrl.openConnection()
            .getInputStream());

      // Transforma a string em JSON
      JSONObject json = new JSONObject(result);
      // Pega a primeira rota retornada
      JSONObject jsonRoute = 
        json.getJSONArray("routes")
          .getJSONObject(0);
      JSONObject leg = jsonRoute
        .getJSONArray("legs").getJSONObject(0);

      // Obtém os passos do caminho
      JSONArray steps = leg.getJSONArray("steps");

      final int numSteps = steps.length();
      /*
       * Itera através dos passos, decodificando 
       * a polyline e adicionando à rota.
       */
      JSONObject step;
      for (int i = 0; i < numSteps; i++) {
        // Obtém o passo corrente
        step = steps.getJSONObject(i);
        // Decodifica a polyline e adiciona à rota
        route.addPoints(
          decodePolyLine(
            step.getJSONObject("polyline")
              .getString("points")));
      }
    } catch (Exception e) {
    }
    return route;
  }

  private String convertStreamToString(
    final InputStream input) {

    final BufferedReader reader = 
      new BufferedReader(
        new InputStreamReader(input));
    final StringBuilder sBuf = new StringBuilder();

    String line = null;
    try {
      while ((line = reader.readLine()) != null) {
        sBuf.append(line);
      }
    } catch (IOException e) {
    } finally {
      try {
        input.close();
      } catch (IOException e) {
      }
    }
    return sBuf.toString();
  }

  private List<LatLng> decodePolyLine(final String poly) {
    int len = poly.length();
    int index = 0;
    List<LatLng> decoded = 
      new ArrayList<LatLng>();

    int lat = 0;
    int lng = 0;

    while (index < len) {
      int b;
      int shift = 0;
      int result = 0;
      do {
        b = poly.charAt(index++) - 63;
        result |= (b & 0x1f) << shift;
        shift += 5;
      } while (b >= 0x20);
      int dlat = 
        ((result & 1) != 0 ? 
          ~(result >> 1) : 
          (result >> 1));
      lat += dlat;

      shift = 0;
      result = 0;
      do {
        b = poly.charAt(index++) - 63;
        result |= (b & 0x1f) << shift;
        shift += 5;
      } while (b >= 0x20);

      int dlng = ((result & 1) != 0 ? 
        ~(result >> 1) : 
        (result >> 1));
      lng += dlng;

      decoded.add(
        new LatLng(
          (float)(lat / 1E5),
          (float)(lng / 1E5)));
    }
    return decoded;
  }
}
Quem viu o post anterior deve lembrar que a classe RouteOverlay não existe mais. Quem faz seu papel é a classe PolyLine da nova versão da Google Maps Api.
A imagem abaixo mostra nossa aplicação em execução.



Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 21 de novembro de 2011

Google Maps e GPS

Olá povo,

Nesse post de 2009 mostrei como fazer um "Hello World" com a API do Google Maps. Agora vamos incrementar um pouco esse exemplo colocando uma imagem para indicar o seu local baseado na posição GPS.

Pré-requisitos
A configuração inicial para obter a chave e exibir o mapa na tela é idêntica a apresentada no post de dois anos atrás, mas caso não consiga gerar a chave, dê uma olhada nesse post aqui (Dica 5). Outra ressalva é que, caso você esteja usando o emulador para testar sua aplicação, certifique-se de que ele esteja usando a "Google APIs" no "Android AVD Manager" conforme a figura abaixo:



O código do Overlay
Uma coordenada geográfica é representada pela classe GeoPoint. Ela tem a latitude e longitude de um ponto no mapa. Entretanto essa classe não é visual, ou seja, ela não serve para enxergarmos o ponto no mapa. Quando queremos adicionar no mapa alguma indicação para uma coordenada geográfica, devemos utilizar a classe Overlay, que é uma subclasse de View. O código abaixo cria um Overlay que desenhará uma imagem em um dado ponto no mapa (representado por um GeoPoint).
public class MeuOverlay extends Overlay {

private Bitmap imagem;
private GeoPoint geopoint;

public MeuOverlay(Bitmap img ) {
imagem = img;
}

public void setGeopoint(GeoPoint geopoint) {
this.geopoint = geopoint;
}

@Override
public void draw(Canvas canvas, MapView mapView,
boolean shadow) {
super.draw(canvas, mapView, shadow);

if (geopoint != null){
Point pontoNaTela = mapView.getProjection()
.toPixels(geopoint, null);

canvas.drawBitmap(imagem,
pontoNaTela.x - (imagem.getWidth() / 2),
pontoNaTela.y - (imagem.getHeight()), null);
}
}
}

Essa classe tem dois atributos: um Bitmap e um GeoPoint. O Bitmap será a imagem que desenharemos para representar a o ponto no mapa; já o GeoPoint, como o próprio nome diz, é um ponto geográfico, que obteremos a partir da posição do GPS.
No método draw() é desenhado o ponto no mapa. Inicialmente checamos se o GeoPoint foi setado, em caso positivo, convertemos essa coordenada geográfica em um ponto na tela, que é representado pela classe Point. Quem faz essa conversão é a classe Projection obtida através do método getProjection() do MapView recebido como parâmetro do método. Depois, é só desenhar a imagem na tela. O cálculo feito na chamada do método é para que a imagem fique imediatamente acima do ponto geográfico.

O arquivo de layout
O arquivo de layout não tem nada de mais, apenas a declaração da MapView com a API key (veja no post de 2009 como obter a sua API key).
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">

<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Exemplo GPS e MapActivity" />

<com.google.android.maps.MapView
android:id="@+id/mapa"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="true"
android:apiKey="SUA_API_KEY" />
</LinearLayout>

A Activity
O código abaixo representa a Activity da aplicação e está todo comentado e só vou fazer alguns comentários no final. A nossa classe herda de MapActivity e implementa a interface LocationListener, que é utilizada para receber as notificações do GPS.
public class Aula09Activity extends MapActivity
// Interface que tem os métodos para tratar
// os eventos do GPS
implements LocationListener {

private MapView mapa;
private MapController controller;
private MeuOverlay overlay;
private LocationManager locationManager;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// O LocationManager vai registrar e desregistrar
// a classe para ouvir eventos do GPS
locationManager = (LocationManager)
getSystemService(LOCATION_SERVICE);

// Configurando a MapView
mapa = (MapView)findViewById(R.id.mapa);
// Habilita os botões de Zoom
mapa.setBuiltInZoomControls(true);
// Mostra em modo satélite
mapa.setSatellite(true);

// O MapController 'controla' o mapa :)
controller = mapa.getController();
controller.setZoom(17);

// Cria a imagem que vai representar o Overlay
Bitmap marcador = BitmapFactory.decodeResource(
getResources(), R.drawable.ponto);

// Cria o Overlay e adiciona ao mapView
overlay = new MeuOverlay(marcador);
mapa.getOverlays().add(overlay);

// Determinando um ponto inicial
int latitude = (int)(-8.058698 * 1E6);
int longitude = (int)(-34.872129 * 1E6);

GeoPoint geopoint = new GeoPoint(latitude, longitude);
controller.setCenter(geopoint);
overlay.setGeopoint(geopoint);
}

@Override
protected void onResume() {
super.onResume();
// Registrando a Activity para receber notificações
// de mudança na posição GPS
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 0, 0, this);

locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER, 0, 0, this);
}

@Override
protected void onPause() {
super.onPause();
// Desregistrando a Activity para receber
// notificações de mudança na posição GPS
locationManager.removeUpdates(this);
}

@Override
protected boolean isRouteDisplayed() {
return false;
}

@Override
public void onLocationChanged(Location location) {
// Método chamado quando a posição GPS muda
int latitude = (int)(location.getLatitude() * 1E6);
int longitude = (int)(location.getLongitude() * 1E6);

GeoPoint geopoint = new GeoPoint(latitude, longitude);
overlay.setGeopoint(geopoint);
controller.animateTo(geopoint);
}

@Override
public void onProviderDisabled(String provider) {
// Método chamado quando o GPS é desabilitado
}

@Override
public void onProviderEnabled(String provider) {
// Método chamado quando o GPS é habilitado
}

@Override
public void onStatusChanged(String provider,
int status, Bundle extras) {
// Método chamado quando o status do GPS muda.
// Pode ser: OUT_OF_SERVICE,
// TEMPORARILY_UNAVAILABLE e AVAILABLE
}
}

No código acima fazemos algumas inicializações no método onCreate. A classe LocationManager é responsável por registrar uma classe para "ouvir" os eventos de GPS (definidos na interface LocationListener que nossa Activity está implementando). Para criar um GeoPoint devemos passar a latitude e longitude em microdegrees, para tal devemos multiplicar os valores por 1.000.000 ou em notação científica 1E6 (10 elevado a 6).
No método onResume registramos nossa classe para ouvir os eventos do GPS. O método requestLocationUpdates recebe como parâmetros:
- o provedor de informações de posição GPS: aqui podemos utilizar o GPS do telefone, que demora mais a obter os dados, porém eles são mais precisos; e o da rede de dados, que obtém a posição baseado no esquema de triangulação de antenas da operadora de telefonia;
- a distância mínima em metros na posição GPS;
- o intervalo de tempo em milisegundos para receber a atualização;
- o objeto de uma classe que implemente a interface LocationListener (no nosso caso, a própria Activity).
No método onPause, desregistramos nossa classe para não ouvir mais os eventos de GPS. Isso evitará que ao sair da aplicação o Android tente ficar enviando as coordenadas mesmo com a aplicação inativa.
O método onLocationChanged é o mais importante, pois a cada vez que a posição GPS muda, esse método é chamado pelo Android. A partir do objeto Location que vem como parâmetro, obtemos a latitude e longitude para atualizarmos a posição do Overlay.
AndroidManifest.xml
No manifest, adicionamos as permissões INTERNET e ACCESS_FINE_LOCATION para podermos utilizar o Google Maps e obter a posição GPS respectivamente. Outra informação relevante é a tag <uses-library> que adicionamos para usar a API do Google Maps, uma vez que ela não é "padrão" da plataforma Android.
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="br.edu.cesar.aula09"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk android:minSdkVersion="10" />

<uses-permission
android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION" />

<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:label="@string/app_name"
android:name=".Aula09Activity" >
<intent-filter >
<action
android:name="android.intent.action.MAIN" />
<category
android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<uses-library
android:name="com.google.android.maps"/>
</application>
</manifest>

Para testar o código da aplicação, você deve habilitar as opções de GPS no dispositivo através da opção Settings > Location & Security > My Location. Notem que temos duas opções nessa seção: GPS satélites e redes móveis. A primeira é mais precisa, porém demora mais; já a segunda obtém a posição baseada triangulação entre as distâncias das redes de telefonia ou wireless.

Mas se você estiver usando o emulador, use a aba "Emulator control" da perspectiva DDMS do Eclipse (para exibir, acesse Window > Open perspective > DDMS).

Digite uma coordena válida e clique em "Send". Para ober uma posição, vá no Google Maps, clique com o botão direito e selecione "O que há aqui? | What's here?". Na barra de busca, ficará a latitude e a longitude, aí é só copiar e testar.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

domingo, 25 de outubro de 2009

Intents nativas do Android

Olá povo,

Um grande benefício que a plataforma Android trouxe para nós desenvolvedores foi a capacidade de nos comunicar com aplicações "nativas" através de intenções (Intents).
Sendo assim, resolvi publicar algumas das intents nativas mais comuns do Android. Segue abaixo o código-fonte.


public class ExemploIntents extends ListActivity {

private static final String[] OPCOES = {
"Browser",
"Realizando uma chamada",
"Visualizar contato",
"Todos os contatos",
"Mapa",
"Tocar música",
"SMS",
"Sair"
};

@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
ArrayAdapter<String> adapter =
new ArrayAdapter<String>(
this,
android.R.layout.simple_list_item_1,
OPCOES);

setListAdapter(adapter);
}

@Override
protected void onListItemClick(ListView l, View v,
int position, long id) {

super.onListItemClick(l, v, position, id);
Uri uri = null;
Intent intent = null;

switch (position) {
// Abrindo uma URL
case 0:
uri = Uri.parse("http://nglauber.blogspot.com");
intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
break;

// Realiza uma chamada
case 1:
uri = Uri.parse("tel:99887766");
intent = new Intent(Intent.ACTION_DIAL, uri);
startActivity(intent);
break;

// Visualiza um contato específico
// da lista de contatos
case 2:
uri = Uri.parse(
"content://contacts/people/5");
intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
break;

// Visualiza todos os contatos e permite
// selecionar um através do resultado
// da Activity
case 3:
uri = Uri.parse(
"content://contacts/people/");
intent = new Intent(Intent.ACTION_PICK, uri);
startActivityForResult(intent, 0);
break;

// Pesquisa uma posição do mapa
// !Seu AVD deve estar usando Google APIs!
case 4:
uri = Uri.parse(
"geo:0,0?q=Rua+Amelia,Recife");
intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
break;

// Executa uma música do SDcard
case 5:
uri = Uri.parse(
"file:///sdcard/musica.mp3");
intent = new Intent();
intent.setDataAndType(uri,"audio/mp3");
startActivity(intent);
break;

// Abrindo o editor de SMS
case 6:
uri = Uri.parse("sms:12345");
intent = new Intent(Intent.ACTION_VIEW, uri);
intent.putExtra("sms_body", "Corpo do SMS");
startActivity(intent);
break;

default:
finish();
}
}

@Override
protected void onActivityResult(int requestCode,
int resultCode, Intent data) {

super.onActivityResult(requestCode,
resultCode, data);

// Mostra um contato selecionado (case 3)
// em um Toast
if (data == null){
Toast.makeText(this, "Nenhum contato",
Toast.LENGTH_SHORT).show();
} else {
Uri uri = data.getData();
Toast.makeText(this, "Contato: "+ uri,
Toast.LENGTH_SHORT).show();
}
}
}


Vamos explicar o que foi feito acima. Criamos uma lista com várias opções de intents nativas. E quando o usuário clicar em qualquer uma dessas opções, a respectiva Intent será disparada.
Em todos os casos, utilizamos o objeto Uri para criar a Intent. Esse objeto indica um recurso do aparelho queremos acessar, e baseado no protocolo informado, ele irá disparar a atividade desejada.
Em conjunto com a Uri, associamos uma ação que queremos realizar sobre o recurso. Vamos exemplificar como o Android entende a solicitação, usando o caso do Browser: "O usuário está com a intenção (Intent) de visualizar (ACTION_VIEW) o recurso (Uri) http://nglauber.blogspot.com". Nesse momento o Android faz uma busca interna pra saber quem pode tratar essa intenção.

No exemplo de discar um número, utilizamos a ação ACTION_DIAL e o protocolo "tel:" seguido do número do telefone a ser discado. Notem que aqui há uma diferença entre "discar" (ACTION_DIAL) e "chamar" (ACTION_CALL). A primeira opção apenas disca o número não chama. Se quiséssemos chamar o número ao invés de apenas discá-lo, deveríamos utilizar a ACTION_CALL e adicionar a permissão abaixo no manifest:

<uses-permission name="android.permission.CALL_PHONE"/>

Na lista de contatos temos dois casos. O primeiro visualiza um contato utilizando o protocolo "content://" - que identifica um ContentProvider - passando o caminho do provider de contatos. Já o segundo caso está usando a ação ACTION_PICK para recuperar a Uri de um contato específico. Para tal, é utilizado o método startActivityForResult que iniciará a tela de contatos permitindo que selecionemos um da lista. Para tratar o retorno da atividade, utilizamos o método onActivityResult.
Para abrir a aplicação de Mapas em um local específico utilizamos o protocolo "geo:" seguido do do endereço do local desejado. É possível passar também a longitude e latitude (geo:lat,long).
Para executarmos o MP3 Player padrão do aparelho passando uma música, utilizamos o método setDataAndType e passamos a Uri com o protocolo "file://" e informando o MIME type do arquivo.
Mais informações aqui.

4br4ç05,
nglauber

terça-feira, 15 de setembro de 2009

Google Maps com Android 1.5

Olá povo,

Aqui vai um passo-a-passo pra quem for utilizar a API do GoogleMaps no Android.

Pré requisitos:
Android SDK 1.5
Eclipse Ganymede
Plugin ADT (Android Developer Tools) 0.9.1
JDK 1.5 ou superior com o diretório bin na variável de ambiente PATH.

Passo 1:
Obtenha o código do certificado digital que é usado para assinar as aplicações. Toda vez que o Eclipse executa uma aplicação Android ele gera um APK (Android PacKage) assinado. Essa assinatura é gerada a partir de um certificado digital de testes chamado debug.keystore que fica na pasta home do seu usuário. No meu caso, no Windows XP o caminho foi: C:\Documents and Settings\ngvl\.android\debug.keystore.

Para obter o código do certificado digital, utilizaremos a ferramenta keytool que vem com o JDK. Abra um terminal/prompt de comando e digite:

keytool -list -alias androiddebugkey -keystore "caminho do seu debug.keystore“

Será solicitado usuário e senha, digite "android" para ambos. Será gerado um resultado semelhante a esse:

androiddebugkey, 30/08/2009, PrivateKeyEntry,
Certificate fingerprint (MD5): AB:DB:39:2F:30:FC:7E:28:2B:7F:BF:54:7B:44:DF:C4


Passo 2:
Acesse o site http://code.google.com/android/maps-api-signup.html (é necessário ter uma conta do Google) digite o Certificate fingerprint no local indicado. Clique em "Generate API key". Será gerada uma chave parecida como a de baixo:

0CjoPthXnVvE9Uz3TW1d-ng82vlrdeTVBda4VPw


Pronto! agora você tem uma chave para o GoogleMaps.

Passo 3:
Vamos configurar nossa aplicação. Primeira coisa que devemos fazer é configurar na nossa aplicação o build target como "GoogleAPIs" e criar um AVD para esse target.

Depois, devemos colocar essa linha no arquivo AndroidManifest.xml, dentro da tag <application>


<uses-library
android:name="com.google.android.maps"/>


E essas duas fora da tag <application>, mas dentro da tag <manifest>.


<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission
android:name="android.permission.INTERNET" />


Agora só falta nós criarmos a classe que mostrará o mapa na tela:


import android.os.Bundle;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;
public class MapaSimples extends MapActivity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MapView mapView = new MapView(this,
"0CjoPthXnVvE9Uz3TW1d-ng82vlrdeTVBda4VPw");
setContentView(mapView);
}

protected boolean isRouteDisplayed() {
return false;
}
}


Se rodarmos nossa aplicação teremos nossa aplicação exibindo um mapa na tela do aparelho.

Depois pretendo editar esse post ou adicionar novos mostrando como manipular o mapa no Android.

4br4ç05,
nglauber