Skip to content
This repository

Home 

skalogir edited this page · 7 revisions

Pages 1

Clone this wiki locally

This application has been optimised and originally designed for the Nokia 2710 Navigator it but should work also on other devices that have GPS and 240 x 320 resolution.

imagePlaceholder  imagePlaceholder

The Compass application described here is designed to look like a normal compass. It has a completely custom-made UI and is intended for 240 x 320 resolution. When the user points the mobile device in the direction s/he is moving, the compass indicates the north direction. The compass also shows the direction to selected landmarks. There are three example landmarks included in the application: the Eiffel Tower, Mecca, and the Statue of Liberty. In addition, the application shows landmarks that the user has saved in the Ovi Maps application. Users can save their current location as a landmark.

Prerequisites

You need the following to develop and test this MIDlet:

  • Eclipse Pulsar or NetBeans with Java ME support
  • Series 40 6th Edition SDK, Feature Pack 1
  • Series 40 device with GPS and preferably 240 x 320 resolution.

For instructions on how to set up the Java ME development environment, see section Getting started.

For more information about the MIDlet, see:

  • Design for details about the design and functionality of the MIDlet
  • Implementation for instructions on how to implement the classes that make up the MIDlet

You can download the project files for the MIDlet from the download page.

Design

The user must walk for a period of time in order for the application to determine the correct direction of north. Because the compass uses only the GPS, the needle will not turn if the user simply turns but doesn't move forward in a direction. By pressing left or right, the user can change the active landmark for which the direction will be shown, or can select ‘None’ when the landmark indicator shows the same angle as the needle.

Pressing the left softkey saves the current location as a landmark. When the Save Landmark view opens, pressing up or down focuses the text field and a Write command appears for the middle softkey. When the middle softkey is pressed, a textbox appears where the user can write the name of the landmark. By selecting Done from the options menu, the user is directed back to the Save Landmark view. Pressing Save saves the landmark; pressing Cancel takes the user back to the main view.

imagePlaceholder  imagePlaceholder  imagePlaceholder

Implementation

The application has been implemented using the Java™ Platform, Micro Edition (Java™ ME) location API (JSR-179) and the SVG API (JSR-226).

Responsiveness

Java ME runs on a virtual machine, meaning that there is an overhead when compared to native application code. The easiest way to get good responsiveness is to create a simple UI loop and minimise the amount of drawing. In Java ME, an object-oriented solution is not always the best solution, because the more objects there are, the slower the MIDlet becomes. The best solution is to create objects when is absolutely necessary; otherwise usage of simple C-like solutions is preferred (in other words, it is better to add a couple of Boolean flags and integers rather than create an object that encapsulates those values).

Constructing a simple UI thread looks like this:

UIloop = new TimerTask() {

    public void run() {
        render(g);
        flushGraphics();
    }
};
mTimer = new Timer();
mTimer.schedule(UIloop, 0, 30);

This is done inside the !GameCanvas class showNotify method. The !ShowNotify method is called immediately before the Canvas is shown. The timer takes care of calling the run method every 30 milliseconds, which means that whatever you draw on the screen will be shown after 30 milliseconds.

When the UI loop runs very fast, you can directly modify the view's state with keyevents, and the user immediately sees the change. The code from the keyReleased method follows:

switch (keyCode) {
                    case KeyCodes.LEFT:
                        if (currentLandmarkIndex == 0) {
                            currentLandmarkIndex = landmarks.size();
                        } else {
                            currentLandmarkIndex--;
                        }
                        break;
                    case KeyCodes.RIGHT:
                        if (currentLandmarkIndex == (landmarks.size())) {
                            currentLandmarkIndex = 0;
                        } else {
                            currentLandmarkIndex++;
                        }
                        break;
                    case KeyCodes.LEFT_SOFTKEY:
                        addlandmark.show();
                        break;
                    case KeyCodes.RIGHT_SOFTKEY:

                        CompassMidlet.getInstance().destroyApp(true);
                        CompassMidlet.getInstance().notifyDestroyed();

                        break;
                    default:
                        break;
                }

Here only changes to simple variables are made that the UI loop checks later on when it draws. In this way the UI feels very responsive to the user.

Power consumption

Constantly rendering the UI consumes power. The Canvas has a hideNotify method that is called when the canvas is hidden but might be called in other events as well. In Nokia devices the method is always called when the backlight of the display shuts down. This is a good place to stop the UI loop until showNotify is called again. This is how hideNotify looks like:

protected void hideNotify() {
        //cleanup everything
        mTimer.cancel();

        initialized = false;
    }

Getting the angle for the compass

Currently there are two ways of getting the angle of the device's direction compared to the North. One is by directly using the Orientation class and the other is by setting up criteria that enable course value retrieval and passing the criteria as arguments when getting the location provider object. Both the Orientation and Criteria class are part of JSR-179, the Location API. The Orientation class however is optional, meaning that not all devices that support the Location API, are using it. This example does not use the Orientation class and retrieves the course value instead, a method that can be used on a wider range of devices.

The Location API provides two ways of getting the device's location. The first method is by polling the location on regular intervals within a thread by calling the getLocation method. The second is by adding a listener to the LocationProvider and receiving the location object from the locationUpdated method. Both the getLocation and setLocationListener are protected method calls. The former is placed within the polling loop so it is called as frequently as the polling occurs while the latter is called once, right after the location provider is retrieved. Depending on the security domain, the application's security access level and the platform, protected method calls cause user prompts on the screen, that interfere with the application's operation. Though it is possible to change the application's security access level or change its security domain from untrusted to trusted by signing it, these solutions require previous knowledge of Java's security framework. It is best to avoid protected method calls if and when possible. For this purpose, this example uses the setLocationListener instead of the getLocation method for location retrieval.

The LocationProvider needs criteria based on which all the location requests are made. It is a good idea to assign setCostAllowed to true, because a network assisted GPS location retrieval is attempted in this case. Network assisted GPS retrievals are faster than ordinary GPS retrievals. Furthermore, setting courseAndSpeedRequired to true is crucial here because the compass functionality depends on the course value, i.e. the current direction's angle relative to true north:

 criteria=new Criteria();
 criteria.setCostAllowed(true);
 criteria.setSpeedAndCourseRequired(true);
 criteria.setPreferredPowerConsumption(Criteria.POWER_USAGE_HIGH);

Setting a location listener requires passing as argument a LocationListener object. In this example, the CompassView class implements the LocationListener interface, therefore the CompassView class itself is passed as the LocationListener argument. The rest of the arguments are related to the location retrieval intervals, the time out value (how long to wait for a location retrieval) and a value that specifies if previously retrieved locations can be used, provided that they were received relatively recently. The value -1 for these three arguments, selects the default one, for the given device:

try  
{
            lp = LocationProvider.getInstance(criteria);
            lp.setLocationListener(this, -1, -1, -1);
 }
catch(LocationException le) 
{
            System.out.println("LOCATION ERROR:" + le.getMessage());
 }

The locationUpdated method is called periodically according to the interval defined above to provide updates to the device's current location. Not all location objects provided by this method, provide a valid set of location coordinates, therefore a validation is made. The same applies for the course value:

    public void locationUpdated(LocationProvider provider, Location location) 
    {
        if(location.isValid()) 
        {
            isValidGPS = true;
            float course = location.getCourse();
            QualifiedCoordinates coords = location.getQualifiedCoordinates();
            if(coords != null) 
            {
                setCurrentCoordinates(coords);
                latitude = coords.getLatitude();
                longitude = coords.getLongitude();
            }
            //temp = 161.35f;
            if(!Float.isNaN(course)) 
            {
                if(degree != course) 
                {
                    if (course != 0.0f) 
                    {
                        degree = course;
                        angle = degree;
                        if (coords != null) 
                        {
                            calculateLandmarkAngle(coords);
                        }
                    }   
                }
            }
        }
        else 
        {
            isValidGPS = false;
        }
    }

Drawing the needle

The needle is an SVG image. The SVG was preferred here because modifications can be made easily to the way it is displayed on the screen. Generally speaking rotating an SVG image is quite simple but some additional logic is required when rotating an image around its center. SVG images have an anchor point that is always at the top-left corner, meaning that rotating the image happens around that point. The SVG API does not provide the means to change the image's anchor point, but this problem can be circumvented by drawing the needle's center to the top-left corner of the SVG image initially, performing rotation and then translating the needle center to the center of the image. The code for the translation and rotation of the SVG image follows:

..
compass_doc = compass.getDocument();
SVGElement svge = (SVGElement) compass_doc.getElementById("Compass");
SVGMatrix matrix = svge.getMatrixTrait("transform");
matrix = matrix.mTranslate(120,120);
matrix = matrix.mScale(0.6f);
svge.setMatrixTrait("transform", matrix);
..

The course value is the device's current direction in degrees relative to the true north. The compass though should display the direction to the north relative to the current direction. This can be done by adding 180 degrees to the value of the current direction. It is also important to rotate the compass by the amount needed relative to the previous retrieval meaning that if, for example, the first retrieved course value is 100 degrees and the next is 110 degrees, the compass should be rotated by 10 degrees not 110. This is the code that performs the rotation:

..
float rotation = angle-old_angle;
rotation = (-1)*rotation;

SVGElement svge = (SVGElement) compass_doc.getElementById("Compass");
SVGMatrix matrix = svgmatrix.mRotate((rotation));
old_angle = angle;
 svge.setMatrixTrait("transform", matrix);
..

Showing the landmark direction

Landmark direction is calculated by getting the current location's coordinates and finding the values for the two angles created between the true north and the current location, and the true north and the landmark point. The Coordinates class contains the azimuthTo method, that takes a set of coordinates as argument and calculates the point's angle to the true north. Because both the current location and the landmark point, are given as angle values relative to the same reference point, i.e. the true north, in order to find the angle the compass should point to, one would simply add these two angles. This is the code for calculating the direction to a selected landmark:

..
float landmark_rotation = landmark_degree - old_landmark_degree;
landmark_rotation = rotation + landmark_rotation;
old_landmark_degree = landmark_degree;
SVGElement navi_svge = (SVGElement)navi_doc.getElementById("Layer_4");
SVGMatrix navi_matrix = navi_svge.getMatrixTrait("transform");
navi_matrix.mRotate(landmark_rotation);
navi_svge.setMatrixTrait("transform", navi_matrix);
..

Getting and saving landmarks

The Location API supports retrieval of the user's saved landmarks directly from the native maps application. The following code shows how this can be done:

..
store = LandmarkStore.getInstance(null);
Vector landmarks = new Vector();
if (store == null)
    return landmarks;

try {
    Enumeration e = store.getLandmarks();
     while (e.hasMoreElements())
     {
         Landmark landmark = (Landmark) e.nextElement();
         landmarks.addElement(landmark);
     }
}
catch (Exception ex)
{
    ex.printStackTrace();
}

First an instance of the landmarkstore is retrieved. By passing null, the default store is used, i.e. the same store used by the native maps application. The store returns an Enumeration which is mapped to a Vector for easier handling.

Saving a landmark is as easy as getting one. Creation of a new Landmark requires only two arguments for our case, a name and landmark's coordinates. This is how a Landmark can be saved:

..
Landmark landmark = new Landmark(name, null, coordinates, null);
try {
    store.addLandmark(landmark, null);
    ret = true;
}catch(IOException io) {
    System.out.println("Save landmark:" + io.getMessage());
}
..

By saving the landmark to the default store it will appear in the native maps application as well.

Conclusion

Several conclusions can be drawn from this application. The Location API is very simple to use. One should have in mind to always specify proper criteria, allowing network assisted GPS location retrieval and enabling course and speed value retrieval. Also minimizing the calls to protected methods is important as this can minimize user prompts and make the application more user friendly.

Also when image manipulation is needed on rutime, the SVG type should preferred. If images of PNG type had been used in this example, 360 images would have been required to rotate the needle, which would have increased the size of the .JAR file enormously. JSR-226, the Scalable 2D Vector Graphics API also supports animation meaning that one could use the API in order to implement the gradual rotation of the needle, and show the intermediate points the needle crosses before reaching its final position.

Something went wrong with that request. Please try again.