Anatomía de un MIDlet de posicionamiento con GPS
Una de las funcionalidades especiales de los equipos Nextel es que incluyen un chip GPS, el cual permite obtener la latitud y la longitud en la que se ubica el equipo. En este artículo, vamos a analizar en detalle una aplicación Java ME (J2ME) que hace uso no sólo de los APIs de localización disponibles en los equipos Motorola iDEN que ofrece Nextel, sino también de un mecanismo de manejo de hilos adecuado para que la experiencia de uso sea óptima.
Como por más documentación que exista, un ejemplo vale mucho, este artículo se basa casi en su totalidad en un análisis de código fuente. Si bien no se supone que conozca cómo funciona Java ME en su totalidad, en algo servirá para entender cuales funciones son parte de cualquier aplicación y cuáles son parte del proceso de localización -- por lo tanto, si requiere un resumen básico de qué es y cómo funciona Java ME, recomendamos que lea el artículo "Introducción a J2ME" de Abel González.
Localización en pocos pasos
Sigamos pues, con el código.
La primera parte, y podríamos decir la más importante, presenta las inclusiones de los paquetes Java ME que necesitamos:
import java.io.*;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.lang.System;
import java.util.*;
import com.motorola.iden.position.*;
Las seis inclusiones iniciales son en general esenciales para toda aplicación Java ME que hace algo más que mostrar una cadena de caracteres en la pantalla. La última (com.motorola.iden.position.*) es la importante para poder hacer uso de las funciones de localización en todos los equipos iDEN con aGPS. (También se podría usar javax.microedition.location en los equipos que soportan el API de localización JSR-179, en cuyo caso el código fuente sería bastante distinto y será parte de otra discusión).
Luego, viene la declaración del MIDlet propio, la cual es común a todas las aplicaciones JavaME que utilizan un modelo de respuesta a comandos:
Declaramos un objecto Calendar() para manejar las funciones de fechas:
y una lista que utilizaremos más tarde para determinar el tipo de localización aGPS que queremos y la tolerancia de demora que estamos dispuestos a tener:
String[] testList = {
"(1)Conexión: delay=no",
"(2)Conexión: delay=low",
"(3)Conexión: delay=high",
"(4)Salir",
};
Vale hablar un poco más sobre esta lista. Cada uno de los valores (delay=no, delay=low, delay=high) se utiliza para indicarle al API de localización si queremos una localización que requiere la precisión que proporciona el chip GPS o si podemos valernos con las coordenadas de la celda que está dándole servicio al equipo en el momento determinado. El parámetro delay=high le indica al API que la aplicación está dispuesta a esperar un tiempo para una coordenada precisa -- lo que indica el uso de GPS; usando delay=no determina que, al contrario, la aplicación requiere de un dato de manera inmediata y por lo tanto sólo recibirá las coordenadas de la celda de servicio. Hablaremos más del parámetro delay=low luego.
Seguimos con declaraciones del Canvas, el hilo que utilizaremos para pedir las coordenadas, variables para mantener el estado de la aplicación, los comandos que requerimos, y el constructor de la aplicación en sí.
Display myDisplay;
// Menú principal
List myList;
// Pantalla de progreso que se usa para comunicarle el estado al usuario
ProgressCanvas myPC;
// Hilo que usamos para obtener las coordenadas de GPS
Thread gpsThread;
// OK Command para el menú principal.
Command okCommand;
// Nos permite saber si la aplicación ya ha sido inicializada.
boolean inicializado = false;
/** Constructor para PosDemo. Generalmente es aquí donde se instancia el
* objeto Display para la aplicación
*/
public PosDemo() {
myDisplay = Display.getDisplay(this);
}
El constructor en sí no debe ser el mecanismo para la inicialización de variables de la aplicación, ya que el utilizarlo causa problemas en la ejecución de la aplicación. La configuración generalmente se recomienda hacerla en startApp().
En el método startApp(), a continuación, el cual es requerido por el modelo Java ME, es el que llama el equipo cada vez que la aplicación inicia o vuelve de una pausa. El método se puede llamar muchas veces, así que es importante que la inicialización que exista sólo se haga cuando se requiera.
* así que debe tener en cuenta no sólo la inicialización sino la operación
* continua. El método debe regresar rápidamente ya que el Displayable de
* cada aplicación no se muestra hasta que el método completa.
*
* @throws MIDletStateChangeException Se lanza si la aplicación no puede comenzar
* inmediatamente pero es posible que pueda iniciar más tarde.
*/
protected void startApp() throws MIDletStateChangeException {
if( !inicializado ) {
myList = new List("Elejir prueba:", List.IMPLICIT, testList, null);
okCommand = new Command("OK", Command.OK, 1);
myList.addCommand(okCommand);
myList.setCommandListener(this);
myPC = new ProgressCanvas();
myDisplay.setCurrent(myList);
}
}
La inicialización añade el comando a la pantalla, vincula la lista de opciones al comando, y establece el contenido de la pantalla.
Sigue el método pauseApp() el cual es llamado cada vez que la aplicación se suspende. Este método es esencial, ya que le permite a la aplicación soltar recursos y asegurar que pueda entrar de manera adecuada al resumir la ejecución de la aplicación:
* pausa el MIDlet debe soltar recursos que no necesite y mantenerse silencioso,
* aunque puede continuar con operaciones que no requieran la pantalla
*/
protected void pauseApp() {
System.out.println("pauseApp llamado");
inicializado = true;
}
El método final del ciclo de vida de un MIDlet, destroyApp(), viene a continuación. destroyApp() es el método que invoca el entorno Java ME en el equipo al momento de pedirle a la aplicación que termine por completo su operación y que entre al estado Destroyed. Éste se llama cuando el usuario termina la aplicación de manera forzosa (oprimiendo la tecla END dos veces, por ejemplo) o cuando la aplicación misma llama la función notifyDestroyed(), la cual inicial el proceso de término de una aplicación.
El método destroyApp() le da a la aplicación la oportunidad de terminar su operación limpiamente. En el estado Destroyed, la aplicación debe soltar todos los recursos (conexiones de red, conexiones al GPS, al sistema de memoria, hilos, etc.) y guardar cualquier información que deba persistir. Este método puede ser llamado desde los estados Paused ó Active.
* @param unconditional Si este parámetro es verdadero al ser llamado
* este método, la aplicación debe completar operaciones y soltar
* todos los recursos que esté usando. Si es falso, la aplicación puede
* lanzar una excepción tipo MIDletStateChangeException para indicar que
* no quiere ser destruída en este momento.
*
* @throws MIDletStateChangeException Lanzado si la aplicación no desea
* terminar ahora
*/
protected void destroyApp(boolean unconditional)
throws MIDletStateChangeException {
}
A seguir, el código que maneja los comandos y las actividades que éstos iniciarán.
* <B>Nota para los desarrolladores:</B> este método debe retornar de manera inmediata.
*
* @param c Un objeto tipo Command que identifica el comando. Éste debe ser o uno de los comandos que se le agregaron al Displayable con addCommand(Command) o con el comando impĺicito SELECT_COMMAND asociado con los objetos tipo List.
* @param d El objeto tipo Displayable en el que ocurrió el evento
*/
public void commandAction(Command c, Displayable d) {
if (d == myList) {
switch (((List)d).getSelectedIndex()) {
case 0:
case 1:
case 2:
// Mostrar una barra de progreso al usuario
myDisplay.setCurrent(myPC);
// Iniciar un nuevo hilo (Thread) para que no quede
// inactiva la pantalla
gpsThread = new Thread(new SRunnable(((List)d).getSelectedIndex()));
gpsThread.start();
break;
case 3:
// Salir de la aplicación.
notifyDestroyed();
break;
default:
break;
}
} else {
// Mostrar menu principal
myDisplay.setCurrent(myList);
}
}
El comando más importante para los propósitos de servicios de localización es el que inicializa un nuevo hilo (gpsTread = new Thread(...)). Las funciones de localización pueden tardar, y es importante que la aplicación continúe siendo accesible mientras las funciones de localización retornan.
El hilo recibe como parámetro el índice escogido de la lista que definimos en el encabezado del código. Como indicamos antes, este índice incluye un parametro que indica la tolerancia a demoras que tiene la aplicación al esperar una posición.
Seguimos con el corazón de la aplicación, el cual usa ese parámetro y las funciones de las clases AggregatePosition y PositionConnection para obtener las coordenadas del equipo:
* incluyen "no delay" (ninguna demora), "low delay" (poca demora), and high delay ("alta demora"). Esto indica qué tolerancia tiene la aplicación y cuánto está dispuesta a esperar por una localización.
* @param delay La demora aceptable al obtener una localización.
*/
private void getFix(int delay) {
try{
String connectionString = null;
switch(delay){
case 0:
connectionString = "mposition:delay=no";
break;
case 1:
connectionString = "mposition:delay=low";
break;
case 2:
connectionString = "mposition:delay=high";
break;
default:
connectionString = "mposition:delay=no";
break;
}
// Notificar al usuario del progreso
myPC.updateMessage("Obteniendo ubicación...");
// Obtener conexión PositionConnection y la posición AggregatePosition
AggregatePosition pos = null;
//Iniciar una conexión de localización especificando la demora aceptable
PositionConnection posCon = (PositionConnection)Connector.open(connectionString);
pos = posCon.getPosition();
...
El posCon, de tipo PositionConnection es el que nos permite obtener los datos de localización del equipo. La cadena de conexión que se le pasa al método open() de PositionConnection indica dos cosas: primero, que la conexión es de tipo mposition, lo que le indica al equipo que debe establecer una conexión con el chip GPS del equipo, y segundo, el parámetro delay, que indica qué tipos de respuestas son aceptables y qué cantidad de demora se puede esperar.
En general, el equipo siempre intentará dar los datos más posibles que tenga disponibles y que pueda obtener en tiempo menor al indicado por "delay". El parámetro "delay=no" indica que los datos tienen que estar ya en el equipo y que sea sólo cosa de leerlos de la memoria. Esto implica que la aplicación siempre recibira las coordenadas de la torre que le brinda servicio al equipo y en ningún momento le dará acceso al chip GPS. Cuando el equipo está fuera de cobertura, la latitud y longitud disponibles tendrán un valor de cero.
Cuando el parámetro es "delay=low", el equipo utilizará los datos de asistencia que haya recibido el equipo si estos existen, y los pedirá a la red nuevamente si están caducos o no existen. Este tipo de localización funciona dentro o fuera de la red de datos, pero tendrá más posibilidad de obtener una localización dentro de cobertura de red. Tiene un tiempo máximo de operación de 32 segundos.
El parámetro "delay=high" le permite al equipo intentar hasta por 180 segundos el obtener una localización GPS, sea que el equipo esté dentro o fuera de la red. El equipo en este caso utiliza los datos de asistencia sólo si estos existen y están válidos en el equipo, y si no lo están el equipo no intentará pedirlos a la red. Cuando se requiere de una localización autónoma (por ejemplo, en caso donde definitivamente no hay cobertura o donde pueda demorar más de 32 segundos obtener una localización por visibilidad limitada al firmamento), éste es el método a utilizar.
Una vez que obtenemos un resultado, debemos extraerlo del objeto AggregatePosition, donde se introducen los valores. El objeto AggregatePosition tiene un mundo de funciones, así que para manejarlo con más facilidad, definimos una función printPosition() que nos permite darle acceso a estos valores. A continuación, el final de la función getFix() y la declaración de printPosition():
// Imprimir los datos de localización
printPosition(posCon, pos);
}catch(Exception e) {
// Notificación en caso de error
myPC.updateMessage("Excepción "+e);
}
}
/**
* Imprimir posición y otra informaciǿn
*/
private void printPosition(PositionConnection posCon, AggregatePosition pos) {
// Establecer el formulario donde pondremos los valores para mostrárselos al usuario.
Form myOutput = new Form("Información de posición");
myOutput.addCommand(okCommand);
myOutput.setCommandListener(this);
myPC.updateMessage("Abriendo conexión...");
try {
// Revisa para asegurar que AggregatePosition no esté nulo.
if(pos == null) {
myDisplay.setCurrent(myOutput);
myOutput.append("Objecto de Posición nulo");
return;
}
// Verifica los permisos de GPS que el usuario estableció en su equipo
if (posCon.getStatus() == PositionConnection.POSITION_RESPONSE_RESTRICTED){
myDisplay.setCurrent(myOutput);
myOutput.append("Permisos restringidos en configuración de GPS");
return;
}
// Verifica que el usuario ha dado permiso de reemplazar el datos de alamanaque GPS
if (posCon.getStatus() == PositionConnection.POSITION_RESPONSE_NO_ALMANAC_OVERRIDE){
myDisplay.setCurrent(myOutput);
myOutput.append("Permisos restringidos de reemplazo de almanaque");
return;
}
El tema de permisos es importante. El usuario de un equipo iDEN tiene la posibilidad de restringir qué acceso tiene una aplicación a las coordenadas GPS, usando la opción de privacidad en "Menu -> GPS" en el equipo.
(En ciertos modelos, existen funciones administrativas que permiten establecer contraseñas para restringir quién puede modificar esa configuración.)
A seguir, verificamos los códigos de respuesta que genera el sistema en caso de errores:
// Actualizar indicador de progreso
myPC.updateMessage("Revisando errores");
Thread.currentThread().sleep(500);
// Verificar que la respuesta de AggregatePosition sea adecuada.
// Revisar el estado de PositionConnetion
if(posCon.getStatus() != PositionConnection.POSITION_RESPONSE_OK &&
pos.getResponseCode() != PositionDevice.POSITION_OK) {
myDisplay.setCurrent(myOutput);
switch(pos.getResponseCode()){
case PositionDevice.FIX_NOT_ATTAINABLE:
myOutput.append("No se pudo obtener localización: FIX_NOT_ATTAINABLE");
break;
case PositionDevice.ACCURACY_NOT_ATTAINABLE:
myOutput.append("No se pudo obtener grado de exactitud: ACCURACY_NOT_ATTAINABLE");
break;
case PositionDevice.FIX_NOT_ATTAIN_ASSIST_DATA_UNAV:
myOutput.append("No se pudo obtener localización; datos de asistencia no disponibles: FIX_NOT_ATTAINABLE_ASSIST_DATA_UNAVAILBLE");
break;
case PositionDevice.ACC_NOT_ATTAIN_ASSIST_DATA_UNAV:
myOutput.append("No se pudo obtener grado de exactitud; datos de asistencia no disponibles: ACC_NOT_ATTAINABLE_ASSIST_DATA_UNAVAILBLE");
break;
case PositionDevice.BATTERY_TOO_LOW:
myOutput.append("Nivel de batería muy bajo: BATTERY_TOO_LOW");
break;
case PositionDevice.GPS_CHIPSET_MALFUNCTION:
myOutput.append("Falla en el chip GPS: GPS_CHIPSET_MALFUNCTION");
break;
case PositionDevice.ALMANAC_OUT_OF_DATE:
myOutput.append("Alamanaque GPS caduco: ALMANAC_OUT_OF_DATE");
break;
case PositionDevice.UNAVAILABLE:
myOutput.append("Error desconocido. Si vé esto, por favor informar a Nextel y Motorola");
break;
default:
myOutput.append("Error improbable...");
break;
}
return;
}
Si no se observaron errores, nos queda determinar qué tipo de localización obtuvimos. Si tenemos una localización completa (es decir, una con aGPS), entonces podemos intentar extraer todos los valores que nos brinda el objeto AggregatePosition. Si tenemos una localización de celda, podemos extraer las coordenadas de la torre para, por lo menos, poder usar esa información.
// Si la posición tiene latitud y longitud significa que se inició
// la conexión con delay=low o delay=high. Imprimir datos completos
// de posición.
if(pos.hasLatLon() ) {
// Variables de instancia usadas para generar información para imprimir
int ano, mes, dia, hora, minuto, segundo, longD, latD, longM, latM;
String LAT = pos.getLatitude(Position2D.DEGREES);
String LONG = pos.getLongitude(Position2D.DEGREES);
String mihora;
String horaSys;
Date mifecha= new Date(pos.getTimeStamp());
Date fechaSys = new Date(System.currentTimeMillis());
// Obtener marca de fecha y hora de localización
myCalendar.setTime(mifecha);
ano = myCalendar.get(Calendar.YEAR);
mes = myCalendar.get(Calendar.MONTH);
dia = myCalendar.get(Calendar.DAY_OF_MONTH);
hora = myCalendar.get(Calendar.HOUR_OF_DAY);
minuto = myCalendar.get(Calendar.MINUTE);
segundo = myCalendar.get(Calendar.SECOND);
mihora = ""+dia+"/"+"/"+mes+"/"+ano+" "+hora+":"+minuto+":"+segundo;
// Obtener marca de fecha y hora actual
myCalendar.setTime(fechaSys);
ano = myCalendar.get(Calendar.YEAR);
mes = myCalendar.get(Calendar.MONTH);
dia = myCalendar.get(Calendar.DAY_OF_MONTH);
hora = myCalendar.get(Calendar.HOUR_OF_DAY);
minuto = myCalendar.get(Calendar.MINUTE);
segundo = myCalendar.get(Calendar.SECOND);
horaSys = ""+dia+"/"+"/"+mes+"/"+ano+" "+hora+":"+minuto+":"+segundo;
// Notificación de progreso
myPC.updateMessage("Verificación completa");
Thread.currentThread().sleep(500);
// Añadir datos al formulario para mostrarlos
myOutput.append("(1) Latitud " + LAT);
myOutput.append("(2) Longitud " + LONG);
myOutput.append("(3) Altitud = " + pos.getAltitude() + " metros ");
myOutput.append("(4) Velocidad = " + pos.getSpeed() + " km/h ");
myOutput.append("(5) # de satélites = " + pos.getNumberOfSatsUsed());
myOutput.append("(6) Exactitud lat/lon = " + pos.getLatLonAccuracy() + " milímetros ");
myOutput.append("(7a) TimeMillis = " + pos.getTimeStamp());
myOutput.append("(7b) Hora Localización= "+mytime);
myOutput.append("(7c) Hora Systema = "+ horaSys);
myOutput.append("(8) Rumbo = " + pos.getTravelDirection());
myOutput.append("(9) Nivel de incertidumbre de velocidad = " + pos.getSpeedUncertainty() + " km/h ");
myOutput.append("(10) Nivel de incertidumbre de altitud = " + pos.getAltitudeUncertainty() + " milímetros \n");
myOutput.append("(11) Asistencia usada = "+pos.getAssistanceUsed());
myOutput.append("(12) Latitud celda = " +pos.getServingCellLatitude(Position2D.DEGREES));
myOutput.append("(13) Longitud celda = " + pos.getServingCellLongitude(Position2D.DEGREES));
}
//si sólo obtuvimos localización de la celda de servicio
else {
// Notificación de progreso
myPC.updateMessage("Verificación completa");
Thread.currentThread().sleep(500);
myOutput.append("Latitud celda = " +pos.getServingCellLatitude(Position2D.DEGREES));
myOutput.append("Longitud celda = " + pos.getServingCellLongitude(Position2D.DEGREES));
}
}catch(Exception e) {
// Imprimir error
myOutput.append("Excepción " + e);
}finally{
// Mostrar progreso
myDisplay.setCurrent(myOutput);
}
}
Lo último que nos falta en términos de los servicios de localización es la creación de la interfaz para el hilo que ejecutará la conexión con el sistema de localización del equipo -- SRunnable.
* Hilo que usamos para obtener coordenadas de GPS.
* Usando un hilo, la interfaz permanece activa,
* evitando crear la impresión que se colgó la aplicación.
*/
class SRunnable implements Runnable {
// Modo usado para la operación del API de localización.
int mode = 0;
/** El modo que pasamos corresponde a la tolerancia de demora que usa el
* API de localización:
* 0 = no delay
* 1 = low delay
* 2 = high delay
* @param mode La tolerancia de demora del API de localización
*/
public SRunnable(int mode) {
this.mode = mode;
}
/** Llama al método adecuado para obtener coordenadas de GPS. La manera en la que llama el método getFix() depende de la variable "mode".
*/
public void run() {
switch(mode){
case 0:
case 1:
case 2:
getFix(mode);
break;
default:
break;
}
}
}
Por último, declaramos una clase interna, ProgressCanvas, que nos permite mostrar una barra de progreso al usuario cuando estamos haciendo una operación que puede tomar tiempo.
* Pantalla de progreso
*/
class ProgressCanvas extends Canvas implements Runnable{
boolean active;
private String message = null;
Thread t = null;
int x, y, limit, counter;
boolean expand;
ProgressCanvas(){
x = getWidth()/2;
y = getHeight()/3;
limit = 14;
expand = true;
}
/** Dibuja el Canvas en la pantalla
* @param g El objeto tipo Graphics usado para dibujar el Canvas en la pantalla
*/
protected void paint(Graphics g){
// Limpiar la pantalla
g.setColor(0x000000);
g.fillRect(0, 0, getWidth(), getHeight());
// Actualizar el indicador de progreso
g.setColor(0xffffff);
g.drawArc(x - counter, y - counter, counter *2, counter *2, 0, 360);
if(expand){
if(counter >= limit){
expand = false;
}
counter += 2;
}else{
if(counter < 1) {
expand = true;
}
counter -= 2;
}
// Mostrar el mensaje
g.setColor(0xffffff);
g.setFont(Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_PLAIN, Font.SIZE_SMALL));
g.drawString(message, getWidth()/2, (getHeight()* 2)/3, Graphics.HCENTER|Graphics.BASELINE);
}
/** Usado para actualizar el mensaje en el ProgressCanvas. Este método
* no brinda ninguna funcionalidad para dividir una cadena en múltiples
* líneas de manera automática.
* @param message El mensaje que debe ser mostrado.
*/
public void updateMessage(String message){
this.message = new String(message);
repaint();
serviceRepaints();
}
/** La implementación llama showNotify() inmediatamente antes que este
* Canvas sea visible en la pantalla. Las subclases de Canvas pueden
* sobrecargar este método para ejecutar ciertas oeperaciones antes
* de ser mostradas, tales como establecer animaciones, iniciar
* contadores, etc. La implementación patrón de este método en la
* clase Canvas no contiene ninguna operación.
*/
protected void showNotify(){
active = true;
t = new Thread(this);
t.start();
}
/** La implementación llama hideNotify() justo antes que el
* Canvas sea borrado de la pantalla. Las subclases de Canvas pueden
* sobrecargar este método para darle pausa a las animaciones, terminar
* contadores, etc. La implementación patrón de este método en Canvas no
* contiene ninguna operación
*/
protected void hideNotify(){
active = false;
t = null;
}
/** Cuando un objeto que implementa la interfaz Runnable es usado para
* crear un hilo, iniciar el hilo hace que el método run() de ese objeto
* sea llamado en ese hilo separado que se inició.
*/
public void run() {
while(active){
try{
/**
* Dibujar en la pantalla mientras el Canvas todavía sea
* visible
*/
t.sleep(100);
if(isShown())
repaint();
}catch(Exception e){
}
}
}
}
}
El código completo en inglés y español se adjunta, junto con un projecto listo para compilar con el SDK de Motorola.
| Adjunto | Tamaño |
|---|---|
| GPS_position_sample.zip | 39.55 KB |
- Inicie sesión o regístrese para enviar comentarios
- 10654 lecturas
- English
- Português
