For information about the design and functionality of the MIDlet, see section Design.
The MIDlet is implemented using the Location API and the Scalable 2D Vector Graphics API (M2G API).
For information about the key aspects of implementing the MIDlet, see:
Java ME runs on a virtual machine, meaning there is overhead when compared to native application code. The easiest way to get good responsiveness is to create a simple UI loop and minimize the amount of drawing. In the Java ME environment, an object-oriented solution is not always the best because the increase in the number of objects can make the MIDlet slow. To avoid this, create objects only when absolutely necessary; otherwise use simple C-like solutions (in other words, it is better to add a couple of Boolean flags and integers rather than creating an object that encapsulates those values).
The following is an example of constructing a simple UI thread:
private void initTasks() { UIloop = new TimerTask() { public void run() { render(bg); if(isLandscape) { System.out.println("Landscape"); bufferSprite.setRefPixelPosition(0, 239); bufferSprite.setTransform(Sprite.TRANS_ROT270); }else { bufferSprite.setRefPixelPosition(0, 0); bufferSprite.setTransform(Sprite.TRANS_NONE); } bufferSprite.paint(g); flushGraphics(); } }; // ... } protected void showNotify() { if (!initialized) { mTimer = new Timer(); // ... initTasks(); mTimer.schedule(UIloop, 0, 30); // ... } }
This is done inside the GameCanvas . 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 only be shown after 30 milliseconds.
When the UI loop runs very fast, you can directly modify the view state with key events, and the user immediately sees the change. The code for the keyReleased method is as 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; }
You can only do fast changes to simple variables that the UI loop then checks when it draws. This way, the UI feels very responsive to the user; however, if the MIDlet gets very complicated, the UI loop can easily become complicated as well.
Constantly rendering the UI consumes power. The canvas has a hideNotify method that is called when the canvas is hidden, but may be called for other events as well. On Nokia Asha and Series 40 software platform 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. The following is what hideNotify looks like:
protected void hideNotify() { //cleanup everything mTimer.cancel(); initialized = false; }
To get the angle of the device's direction compared to the north, you can use two different approaches:
Use the Orientation class to retrieve the device orientation.
The Orientation class is optional, which means that all devices that support the Location API do not use it.
Use the Criteria class to set up criteria that enable the course value retrieval, and pass the criteria as arguments when getting the location provider object.
The Criteria class is not optional, which means that it can be used on a wider range of devices.
Both the Orientation class and the Criteria class are part of the Location API, and this example shows how to use the Criteria class.
The Location API provides two ways of getting the device's location:
Polling the location at regular intervals within a thread by calling the getLocation method
Adding a listener to the LocationProvider class and receiving the location object from the locationUpdated method
Both 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. The latter is called only once, right after the location provider is retrieved. Depending on the security domain, the MIDlet's security access level, and the platform, the protected method calls can cause user prompts on the screen, interfering with the MIDlet's operation. Though you can modify the MIDlet'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. Consequently, avoid protected method calls when possible. As an example of avoiding protected method calls, this MIDlet retrieves the location using the setLocationListener method instead of the getLocation method.
The LocationProvider class needs criteria based on which all the location requests are made. Assign the setCostAllowed criterium to true to attempt a network-assisted GPS location retrieval. Network-assisted GPS retrievals are faster than ordinary GPS retrievals. Furthermore, set the setSpeedAndCourseRequired criterium to true, because the compass functionality depends on the course value (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 a LocationListener object as an argument. In this MIDlet, the CompassView class implements the LocationListener interface, and 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 value 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. All location objects provided by this method do not provide a valid set of location coordinates; consequently, a validation is performed. 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; } }
The needle is an SVG image that can be modified in many ways; in this MIDlet, it is rotated. Rotating an SVG image is quite simple, but problems arise when you want to rotate the image around its center. SVG images have an anchor point that is always at the top-left corner, meaning that if you rotate the image, it will rotate around that point. The M2G API does not have a way to change this anchor point, but you can circumvent the problem by drawing your needle's center to the top-left corner of the SVG image initially, and then translate the needle back to the center of the image. This way, you can rotate the needle and it will rotate around its center.
The code to translate and rotate the SVG image is as 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); svgmatrix = matrix; svge.setMatrixTrait("transform", matrix);
The course value is the device's current direction in degrees relative to the true north. However, the compass must display the direction to the north relative to the current direction. To do this, add 180 degrees to the value of the current direction. Also rotate the compass by the amount needed relative to the previous retrieval. This means that if, for example, the first retrieved course value is 100 degrees and the next is 110 degrees, the compass must be rotated only by 10 degrees. The following is the code for 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);
Landmark direction is calculated by getting the current location's coordinates and calculating the angle between the current coordinates and the true north, and the landmark's coordinates and the true north. The calculation is simple: The Coordinates class has the azimuthTo method, which takes the coordinates as a parameter and calculates the angle between them relative to true north. Because both the current location and the landmark point are given as angle values relative to the same reference point (the true north), you simply need to add the two angles to find the angle the compass must point to. The code for calculating the direction to a selected landmark is as follows:
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);
The Location API supports landmarks directly. The API is simple. The following code shows how you can get the same landmarks that are saved in Nokia Maps:
public LandmarkManager() { // Get the default landmark store (with no name specified) store = LandmarkStore.getInstance(null); } // ... public Vector getLandmarks() { Vector landmarks = new Vector(); if (null == store) return landmarks; try { Enumeration e = store.getLandmarks(); while (e.hasMoreElements()) { Landmark landmark = (Landmark) e.nextElement(); landmarks.addElement(landmark); } } catch (Exception ex) { ex.printStackTrace(); } addExampleLandmarksToVector(landmarks); return landmarks; }
First you get an instance of the landmarkstore. By passing "null", you get the default store, which is the same that Nokia Maps uses. The store returns Enumeration, which you map to a Vector for easier handling.
Saving a landmark is as easy as getting one. Create a new Landmark object and give it a name and coordinates. The following is the simple code to save a Landmark:
public boolean saveLandmark(String name, QualifiedCoordinates coordinates) { boolean ret = false; 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()); } return ret; }
By saving the landmark to the default store, it will appear on Nokia Maps as well.
Several conclusions can be drawn from this MIDlet:
The Location API is simple to use. Just remember to set proper criteria before getting the LocationProvider.
Minimize calls to protected methods, since this decreases the number of user prompts and makes the MIDlet more user-friendly.
If you have to manipulate images accurately, use SVG images, which allow more possibilities than normal bitmap images. If bitmap images had been used in this MIDlet, 360 images would have been required to rotate the needle, which would have increased the JAR package size enormously.
SVG also supports animations. If you want to experiment more with the M2G API, you can, for example, implement a gradual rotation of the needle, so that instead of moving the needle at once to the final position, all the intermediate points the needle crosses before reaching its final position are shown as well.